CStyle
CStyle is a single pass style computer.
Transformers are for injecting elements at runtime and plugins are for modifying element properties.
WARNING: if you are building a transformer, use c.QuickStyles to get the styles of the element as it speed it up by over 50%. However, QuickStyles does not add any style sheet (master.css) styles to the tags, you will have to add them manually.
# StyleSheet?(go)
# StyleTag?(go)
# GetStyles?(go)
# AddPlugin?(go)
See /cstyle/plugins/
# ComputeNodeStyle?(go)
if !utils.ChildrenHaveText(n) The utils.ChildrenHaveText function is called here instead of checking if the node directly has text is because in the case below
1<em><b>Text</b></em>
The em
element does not have text but it has a element with text insid, but the element will still need to be rendered as text.
# parseBorderShorthand?(go)
# CompleteBorder?(go)
# genTextNode?(go)
1package cstyle
2
3import (
4 "fmt"
5 "gui/color"
6 "gui/element"
7 "gui/font"
8 "gui/parser"
9 "gui/utils"
10 "os"
11 "sort"
12 "strconv"
13 "strings"
14
15 imgFont "golang.org/x/image/font"
16)
17
18// !TODO: Make a fine selector to target tags and if it has children or not etc
19// + could copy the transformers but idk
20type Plugin struct {
21 Selector func(*element.Node) bool
22 Level int
23 Handler func(*element.Node, *map[string]element.State)
24}
25
26type Transformer struct {
27 Selector func(*element.Node) bool
28 Handler func(*element.Node, *CSS) *element.Node
29}
30
31type CSS struct {
32 Width float32
33 Height float32
34 StyleSheets []map[string]map[string]string
35 Plugins []Plugin
36 Transformers []Transformer
37 Document *element.Node
38 Fonts map[string]imgFont.Face
39}
40
41func (c *CSS) Transform(n *element.Node) *element.Node {
42 for _, v := range c.Transformers {
43 if v.Selector(n) {
44 n = v.Handler(n, c)
45 }
46 }
47
48 for i := 0; i < len(n.Children); i++ {
49 tc := c.Transform(n.Children[i])
50 // Removing this causes text to break, what does this do??????
51 // its because we are using the dom methods to inject on text, not like ulol, prob need to get those working bc they effect user dom as well
52 // todo: dom fix, inline-block, text align vert (poss by tgting parent node instead), scroll
53 // n = tc.Parent
54 n.Children[i] = tc
55 }
56
57 return n
58}
59
60func (c *CSS) StyleSheet(path string) {
61 // Parse the CSS file
62 dat, _ := os.ReadFile(path)
63 styles := parser.ParseCSS(string(dat))
64
65 c.StyleSheets = append(c.StyleSheets, styles)
66}
67
68func (c *CSS) StyleTag(css string) {
69 styles := parser.ParseCSS(css)
70 c.StyleSheets = append(c.StyleSheets, styles)
71}
72
73var inheritedProps = []string{
74 "color",
75 "cursor",
76 "font",
77 "font-family",
78 "font-size",
79 "font-style",
80 "font-weight",
81 "letter-spacing",
82 "line-height",
83 // "text-align",
84 "text-indent",
85 "text-justify",
86 "text-shadow",
87 "text-transform",
88 "text-decoration",
89 "visibility",
90 "word-spacing",
91 "display",
92}
93
94func (c *CSS) QuickStyles(n *element.Node) map[string]string {
95 styles := make(map[string]string)
96
97 // Inherit styles from parent
98 if n.Parent != nil {
99 ps := n.Parent.Style
100 for _, prop := range inheritedProps {
101 if value, ok := ps[prop]; ok && value != "" {
102 styles[prop] = value
103 }
104 }
105 }
106
107 // Add node's own styles
108 for k, v := range n.Style {
109 styles[k] = v
110 }
111
112 return styles
113}
114
115func (c *CSS) GetStyles(n *element.Node) map[string]string {
116 styles := make(map[string]string)
117
118 // Inherit styles from parent
119 if n.Parent != nil {
120 ps := n.Parent.Style
121 for _, prop := range inheritedProps {
122 if value, ok := ps[prop]; ok && value != "" {
123 styles[prop] = value
124 }
125 }
126 }
127
128 // Add node's own styles
129 for k, v := range n.Style {
130 styles[k] = v
131 }
132
133 // Check if node is hovered
134 hovered := false
135 for _, class := range n.ClassList.Classes {
136 if class == ":hover" {
137 hovered = true
138 break
139 }
140 }
141
142 // Apply styles from style sheets
143 for _, styleSheet := range c.StyleSheets {
144 for selector, rules := range styleSheet {
145 originalSelector := selector
146
147 if hovered && strings.Contains(selector, ":hover") {
148 selector = strings.Replace(selector, ":hover", "", -1)
149 }
150
151 if element.TestSelector(selector, n) {
152 for k, v := range rules {
153 styles[k] = v
154 }
155 }
156
157 selector = originalSelector // Restore original selector
158 }
159 }
160
161 // Parse inline styles
162 inlineStyles := parser.ParseStyleAttribute(n.GetAttribute("style"))
163 for k, v := range inlineStyles {
164 styles[k] = v
165 }
166
167 // Handle z-index inheritance
168 if n.Parent != nil && styles["z-index"] == "" {
169 if parentZIndex, ok := n.Parent.Style["z-index"]; ok && parentZIndex != "" {
170 z, _ := strconv.Atoi(parentZIndex)
171 z += 1
172 styles["z-index"] = strconv.Itoa(z)
173 }
174 }
175
176 return styles
177}
178
179func (c *CSS) AddPlugin(plugin Plugin) {
180 c.Plugins = append(c.Plugins, plugin)
181}
182
183func (c *CSS) AddTransformer(transformer Transformer) {
184 c.Transformers = append(c.Transformers, transformer)
185}
186
187func CheckNode(n *element.Node, state *map[string]element.State) {
188 s := *state
189 self := s[n.Properties.Id]
190
191 fmt.Println(n.TagName, n.Properties.Id)
192 fmt.Printf("ID: %v\n", n.Id)
193 fmt.Printf("EM: %v\n", self.EM)
194 fmt.Printf("Parent: %v\n", n.Parent.TagName)
195 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
196 fmt.Printf("Text: %v\n", n.InnerText)
197 fmt.Printf("X: %v, Y: %v, Z: %v\n", self.X, self.Y, self.Z)
198 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
199 fmt.Printf("Styles: %v\n", n.Style)
200 fmt.Printf("Margin: %v\n", self.Margin)
201 fmt.Printf("Padding: %v\n", self.Padding)
202 // fmt.Printf("Background: %v\n", self.Background)
203 // fmt.Printf("Border: %v\n\n\n", self.Border)
204}
205
206func (c *CSS) ComputeNodeStyle(n *element.Node, state *map[string]element.State) *element.Node {
207
208 // Head is not renderable
209 if utils.IsParent(*n, "head") {
210 return n
211 }
212
213 plugins := c.Plugins
214
215 s := *state
216 self := s[n.Properties.Id]
217 parent := s[n.Parent.Properties.Id]
218
219 self.Background = color.Parse(n.Style, "background")
220 self.Border, _ = CompleteBorder(n.Style, self, parent)
221
222 fs := utils.ConvertToPixels(n.Style["font-size"], parent.EM, parent.Width)
223 self.EM = fs
224
225 if n.Style["display"] == "none" {
226 self.X = 0
227 self.Y = 0
228 self.Width = 0
229 self.Height = 0
230 return n
231 }
232
233 // Set Z index value to be sorted in window
234 if n.Style["z-index"] != "" {
235 z, _ := strconv.Atoi(n.Style["z-index"])
236 self.Z = float32(z)
237 }
238
239 if parent.Z > 0 {
240 self.Z = parent.Z + 1
241 }
242
243 (*state)[n.Properties.Id] = self
244
245 wh := utils.GetWH(*n, state)
246 width := wh.Width
247 height := wh.Height
248
249 x, y := parent.X, parent.Y
250 // !NOTE: Would like to consolidate all XY function into this function like WH
251 offsetX, offsetY := utils.GetXY(n, state)
252 x += offsetX
253 y += offsetY
254
255 var top, left, right, bottom bool = false, false, false, false
256
257 m := utils.GetMP(*n, wh, state, "margin")
258 p := utils.GetMP(*n, wh, state, "padding")
259
260 self.Margin = m
261 self.Padding = p
262
263 if n.Style["position"] == "absolute" {
264 bas := utils.GetPositionOffsetNode(n)
265 base := s[bas.Properties.Id]
266 if n.Style["top"] != "" {
267 v := utils.ConvertToPixels(n.Style["top"], self.EM, parent.Width)
268 y = v + base.Y
269 top = true
270 }
271 if n.Style["left"] != "" {
272 v := utils.ConvertToPixels(n.Style["left"], self.EM, parent.Width)
273 x = v + base.X
274 left = true
275 }
276 if n.Style["right"] != "" {
277 v := utils.ConvertToPixels(n.Style["right"], self.EM, parent.Width)
278 x = (base.Width - width) - v
279 right = true
280 }
281 if n.Style["bottom"] != "" {
282 v := utils.ConvertToPixels(n.Style["bottom"], self.EM, parent.Width)
283 y = (base.Height - height) - v
284 bottom = true
285 }
286
287 } else {
288 for i, v := range n.Parent.Children {
289 if v.Style["position"] != "absolute" {
290 if v.Properties.Id == n.Properties.Id {
291 if i > 0 {
292 sib := n.Parent.Children[i-1]
293 sibling := s[sib.Properties.Id]
294 if sib.Style["position"] != "absolute" {
295 if n.Style["display"] == "inline" {
296 if sib.Style["display"] == "inline" {
297 y = sibling.Y
298 } else {
299 y = sibling.Y + sibling.Height
300 }
301 } else {
302 y = sibling.Y + sibling.Height + (sibling.Border.Width * 2) + sibling.Margin.Bottom
303 }
304 }
305 }
306 break
307 } else if n.Style["display"] != "inline" {
308 vState := s[v.Properties.Id]
309 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (self.Border.Width)
310 }
311 }
312 }
313 }
314
315 // Display modes need to be calculated here
316
317 relPos := !top && !left && !right && !bottom
318
319 if left || relPos {
320 x += m.Left
321 }
322 if top || relPos {
323 y += m.Top
324 }
325 if right {
326 x -= m.Right
327 }
328 if bottom {
329 y -= m.Bottom
330 }
331
332 self.X = x
333 self.Y = y
334 self.Width = width
335 self.Height = height
336 (*state)[n.Properties.Id] = self
337
338 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
339 // Confirm text exists
340 n.InnerText = strings.TrimSpace(n.InnerText)
341 self = genTextNode(n, state, c)
342 }
343
344 (*state)[n.Properties.Id] = self
345 (*state)[n.Parent.Properties.Id] = parent
346 // Call children here
347
348 var childYOffset float32
349 for i := 0; i < len(n.Children); i++ {
350 v := n.Children[i]
351 v.Parent = n
352 // This is were the tainting comes from
353 n.Children[i] = c.ComputeNodeStyle(v, state)
354
355 cState := (*state)[n.Children[i].Properties.Id]
356 if n.Style["height"] == "" && n.Style["min-height"] == "" {
357 if v.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
358 childYOffset = cState.Y + cState.Height
359 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
360 self.Height += cState.Margin.Top
361 self.Height += cState.Margin.Bottom
362 self.Height += cState.Padding.Top
363 self.Height += cState.Padding.Bottom
364 self.Height += cState.Border.Width * 2
365 }
366 }
367 if cState.Width > self.Width {
368 self.Width = cState.Width
369 }
370 }
371
372 if n.Style["height"] == "" {
373 self.Height += self.Padding.Bottom
374 }
375
376 (*state)[n.Properties.Id] = self
377
378 // Sorting the array by the Level field
379 sort.Slice(plugins, func(i, j int) bool {
380 return plugins[i].Level < plugins[j].Level
381 })
382
383 for _, v := range plugins {
384 if v.Selector(n) {
385 v.Handler(n, state)
386 }
387 }
388
389 // CheckNode(n, state)
390 return n
391}
392
393func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
394 // Split the shorthand into components
395 borderComponents := strings.Fields(cssProperties["border"])
396
397 // Default values
398 width := "0px" // Default width
399 style := "solid"
400 borderColor := "#000000" // Default color
401
402 // Suffixes for width properties
403 widthSuffixes := []string{"px", "em", "pt", "pc", "%", "vw", "vh", "cm", "in"}
404
405 // Identify each component regardless of order
406 for _, component := range borderComponents {
407 if isWidthComponent(component, widthSuffixes) {
408 width = component
409 } else {
410 switch component {
411 case "thin", "medium", "thick":
412 width = component
413 case "none", "hidden", "dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset":
414 style = component
415 default:
416 // Handle colors
417 borderColor = component
418 }
419 }
420 }
421
422 parsedColor, _ := color.Color(borderColor)
423 w := utils.ConvertToPixels(width, self.EM, parent.Width)
424
425 return element.Border{
426 Width: w,
427 Style: style,
428 Color: parsedColor,
429 Radius: cssProperties["border-radius"],
430 }, nil
431}
432
433// Helper function to determine if a component is a width value
434func isWidthComponent(component string, suffixes []string) bool {
435 for _, suffix := range suffixes {
436 if strings.HasSuffix(component, suffix) {
437 return true
438 }
439 }
440 return false
441}
442
443func genTextNode(n *element.Node, state *map[string]element.State, css *CSS) element.State {
444 s := *state
445 self := s[n.Properties.Id]
446 parent := s[n.Parent.Properties.Id]
447
448 text := element.Text{}
449
450 bold, italic := false, false
451 // !ISSUE: needs bolder and the 100 -> 900
452 if n.Style["font-weight"] == "bold" {
453 bold = true
454 }
455
456 if n.Style["font-style"] == "italic" {
457 italic = true
458 }
459
460 if text.Font == nil {
461 if css.Fonts == nil {
462 css.Fonts = map[string]imgFont.Face{}
463 }
464 fid := n.Style["font-family"] + fmt.Sprint(self.EM, bold, italic)
465 if css.Fonts[fid] == nil {
466 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
467 css.Fonts[fid] = f
468 }
469 fnt := css.Fonts[fid]
470 text.Font = &fnt
471 }
472
473 letterSpacing := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, parent.Width)
474 wordSpacing := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, parent.Width)
475 lineHeight := utils.ConvertToPixels(n.Style["line-height"], self.EM, parent.Width)
476 if lineHeight == 0 {
477 lineHeight = self.EM + 3
478 }
479
480 text.LineHeight = int(lineHeight)
481 text.WordSpacing = int(wordSpacing)
482 text.LetterSpacing = int(letterSpacing)
483 wb := " "
484
485 if n.Style["word-wrap"] == "break-word" {
486 wb = ""
487 }
488
489 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
490 wb = ""
491 }
492
493 var dt float32
494
495 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
496 dt = self.EM / 7
497 } else {
498 dt = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, parent.Width)
499 }
500
501 col := color.Parse(n.Style, "font")
502
503 self.Color = col
504
505 text.Color = col
506 text.DecorationColor = color.Parse(n.Style, "decoration")
507 text.Align = n.Style["text-align"]
508 text.WordBreak = wb
509 text.WordSpacing = int(wordSpacing)
510 text.LetterSpacing = int(letterSpacing)
511 text.WhiteSpace = n.Style["white-space"]
512 text.DecorationThickness = int(dt)
513 text.Overlined = n.Style["text-decoration"] == "overline"
514 text.Underlined = n.Style["text-decoration"] == "underline"
515 text.LineThrough = n.Style["text-decoration"] == "linethrough"
516 text.EM = int(self.EM)
517 text.Width = int(parent.Width)
518 text.Text = n.InnerText
519 text.Last = n.GetAttribute("last") == "true"
520
521 if n.Style["word-spacing"] == "" {
522 text.WordSpacing = font.MeasureSpace(&text)
523 }
524
525 img, width := font.Render(&text)
526 self.Texture = img
527
528 if n.Style["height"] == "" && n.Style["min-height"] == "" {
529 self.Height = float32(text.LineHeight)
530 }
531
532 if n.Style["width"] == "" && n.Style["min-width"] == "" {
533 self.Width = float32(width)
534 }
535
536 return self
537}