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