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 "slices"
12 "sort"
13 "strconv"
14 "strings"
15)
16
17// !TODO: Make a fine selector to target tags and if it has children or not etc
18// + could copy the transformers but idk
19type Plugin struct {
20 Selector func(*element.Node) bool
21 Level int
22 Handler func(*element.Node, *map[string]element.State)
23}
24
25type Transformer struct {
26 Selector func(*element.Node) bool
27 Handler func(element.Node, *CSS) element.Node
28}
29
30type CSS struct {
31 Width float32
32 Height float32
33 StyleSheets []map[string]map[string]string
34 Plugins []Plugin
35 Transformers []Transformer
36 Document *element.Node
37}
38
39func (c *CSS) Transform(n element.Node) element.Node {
40 for _, v := range c.Transformers {
41 if v.Selector(&n) {
42 n = v.Handler(n, c)
43 }
44 }
45 for i := 0; i < len(n.Children); i++ {
46 v := n.Children[i]
47 tc := c.Transform(v)
48 n = *tc.Parent
49 n.Children[i] = tc
50 }
51
52 return n
53}
54
55func (c *CSS) StyleSheet(path string) {
56 // Parse the CSS file
57 dat, _ := os.ReadFile(path)
58 styles := parser.ParseCSS(string(dat))
59
60 c.StyleSheets = append(c.StyleSheets, styles)
61}
62
63func (c *CSS) StyleTag(css string) {
64 styles := parser.ParseCSS(css)
65 c.StyleSheets = append(c.StyleSheets, styles)
66}
67
68var inheritedProps = []string{
69 "color",
70 "cursor",
71 "font",
72 "font-family",
73 "font-size",
74 "font-style",
75 "font-weight",
76 "letter-spacing",
77 "line-height",
78 // "text-align",
79 "text-indent",
80 "text-justify",
81 "text-shadow",
82 "text-transform",
83 "text-decoration",
84 "visibility",
85 "word-spacing",
86 "display",
87}
88
89func (c *CSS) GetStyles(n element.Node) map[string]string {
90 styles := map[string]string{}
91
92 if n.Parent != nil {
93 ps := c.GetStyles(*n.Parent)
94 for _, v := range inheritedProps {
95 if ps[v] != "" {
96 styles[v] = ps[v]
97 }
98 }
99 }
100 for k, v := range n.Style {
101 styles[k] = v
102 }
103 hovered := false
104 if slices.Contains(n.ClassList.Classes, ":hover") {
105 hovered = true
106 }
107
108 for _, styleSheet := range c.StyleSheets {
109 for selector := range styleSheet {
110 // fmt.Println(selector, n.Properties.Id)
111 key := selector
112 if strings.Contains(selector, ":hover") && hovered {
113 selector = strings.Replace(selector, ":hover", "", -1)
114 }
115 if element.TestSelector(selector, &n) {
116 for k, v := range styleSheet[key] {
117 styles[k] = v
118 }
119 }
120
121 }
122 }
123
124 // This is different than node.Style
125 // temp1 = <span style=​"color:​#a6e22e">​CSS​</span>​
126 // temp1.style == CSSStyleDeclaration {0: 'color', accentColor: '', additiveSymbols: '', alignContent: '', alignItems: '', alignSelf: '', …}
127 // temp1.getAttribute("style") == 'color:#a6e22e'
128 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
129 styles = utils.Merge(styles, inline)
130 // add hover and focus css events
131
132 if n.Parent != nil {
133 if styles["z-index"] == "" && n.Parent.Style["z-index"] != "" {
134 z, _ := strconv.Atoi(n.Parent.Style["z-index"])
135 z += 1
136 styles["z-index"] = strconv.Itoa(z)
137 }
138 }
139
140 return styles
141}
142
143func (c *CSS) AddPlugin(plugin Plugin) {
144 c.Plugins = append(c.Plugins, plugin)
145}
146
147func (c *CSS) AddTransformer(transformer Transformer) {
148 c.Transformers = append(c.Transformers, transformer)
149}
150
151func CheckNode(n *element.Node, state *map[string]element.State) {
152 s := *state
153 self := s[n.Properties.Id]
154
155 fmt.Println(n.TagName, n.Properties.Id)
156 fmt.Printf("ID: %v\n", n.Id)
157 fmt.Printf("EM: %v\n", self.EM)
158 fmt.Printf("Parent: %v\n", n.Parent.TagName)
159 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
160 fmt.Printf("Text: %v\n", n.InnerText)
161 fmt.Printf("X: %v, Y: %v, Z: %v\n", self.X, self.Y, self.Z)
162 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
163 fmt.Printf("Styles: %v\n", n.Style)
164 fmt.Printf("Margin: %v\n", self.Margin)
165 fmt.Printf("Padding: %v\n", self.Padding)
166 // fmt.Printf("Background: %v\n", self.Background)
167 // fmt.Printf("Border: %v\n\n\n", self.Border)
168}
169
170func (c *CSS) ComputeNodeStyle(n *element.Node, state *map[string]element.State) *element.Node {
171
172 // Head is not renderable
173 if utils.IsParent(*n, "head") {
174 return n
175 }
176
177 plugins := c.Plugins
178
179 s := *state
180 self := s[n.Properties.Id]
181 parent := s[n.Parent.Properties.Id]
182
183 self.Background = color.Parse(n.Style, "background")
184 self.Border, _ = CompleteBorder(n.Style, self, parent)
185
186 fs := utils.ConvertToPixels(n.Style["font-size"], parent.EM, parent.Width)
187 self.EM = fs
188
189 if n.Style["display"] == "none" {
190 self.X = 0
191 self.Y = 0
192 self.Width = 0
193 self.Height = 0
194 return n
195 }
196
197 // Set Z index value to be sorted in window
198 if n.Style["z-index"] != "" {
199 z, _ := strconv.Atoi(n.Style["z-index"])
200 self.Z = float32(z)
201 }
202
203 if parent.Z > 0 {
204 self.Z = parent.Z + 1
205 }
206
207 (*state)[n.Properties.Id] = self
208
209 wh := utils.GetWH(*n, state)
210 width := wh.Width
211 height := wh.Height
212
213 x, y := parent.X, parent.Y
214 // !NOTE: Would like to consolidate all XY function into this function like WH
215 offsetX, offsetY := utils.GetXY(n, state)
216 x += offsetX
217 y += offsetY
218
219 var top, left, right, bottom bool = false, false, false, false
220
221 m := utils.GetMP(*n, wh, state, "margin")
222 p := utils.GetMP(*n, wh, state, "padding")
223
224 self.Margin = m
225 self.Padding = p
226
227 if n.Style["position"] == "absolute" {
228 bas := utils.GetPositionOffsetNode(n)
229 base := s[bas.Properties.Id]
230 if n.Style["top"] != "" {
231 v := utils.ConvertToPixels(n.Style["top"], self.EM, parent.Width)
232 y = v + base.Y
233 top = true
234 }
235 if n.Style["left"] != "" {
236 v := utils.ConvertToPixels(n.Style["left"], self.EM, parent.Width)
237 x = v + base.X
238 left = true
239 }
240 if n.Style["right"] != "" {
241 v := utils.ConvertToPixels(n.Style["right"], self.EM, parent.Width)
242 x = (base.Width - width) - v
243 right = true
244 }
245 if n.Style["bottom"] != "" {
246 v := utils.ConvertToPixels(n.Style["bottom"], self.EM, parent.Width)
247 y = (base.Height - height) - v
248 bottom = true
249 }
250
251 } else {
252 for i, v := range n.Parent.Children {
253 if v.Style["position"] != "absolute" {
254 if v.Properties.Id == n.Properties.Id {
255 if i-1 > -1 {
256 sib := n.Parent.Children[i-1]
257 sibling := s[sib.Properties.Id]
258 if sib.Style["position"] != "absolute" {
259 if n.Style["display"] == "inline" {
260 if sib.Style["display"] == "inline" {
261 y = sibling.Y
262 } else {
263 y = sibling.Y + sibling.Height
264 }
265 } else {
266 y = sibling.Y + sibling.Height + (sibling.Border.Width * 2) + sibling.Margin.Bottom
267 }
268 }
269
270 }
271 break
272 } else if n.Style["display"] != "inline" {
273 vState := s[v.Properties.Id]
274 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (self.Border.Width)
275 }
276 }
277 }
278 }
279
280 // Display modes need to be calculated here
281
282 relPos := !top && !left && !right && !bottom
283
284 if left || relPos {
285 x += m.Left
286 }
287 if top || relPos {
288 y += m.Top
289 }
290 if right {
291 x -= m.Right
292 }
293 if bottom {
294 y -= m.Bottom
295 }
296
297 self.X = x
298 self.Y = y
299 self.Width = width
300 self.Height = height
301 (*state)[n.Properties.Id] = self
302
303 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
304 // Confirm text exists
305 n.InnerText = strings.TrimSpace(n.InnerText)
306 self = genTextNode(n, state)
307 }
308
309 (*state)[n.Properties.Id] = self
310 (*state)[n.Parent.Properties.Id] = parent
311
312 // Call children here
313
314 // Check to see if node is in fov
315 // if self.Y < c.Height {
316 var childYOffset float32
317 for i := 0; i < len(n.Children); i++ {
318 v := n.Children[i]
319 v.Parent = n
320 // This is were the tainting comes from
321 n.Children[i] = *c.ComputeNodeStyle(&v, state)
322
323 cState := (*state)[n.Children[i].Properties.Id]
324 if n.Style["height"] == "" {
325 if v.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
326 childYOffset = cState.Y + cState.Height
327 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
328 self.Height += cState.Margin.Top
329 self.Height += cState.Margin.Bottom
330 self.Height += cState.Padding.Top
331 self.Height += cState.Padding.Bottom
332 }
333 }
334 if cState.Width > self.Width {
335 self.Width = cState.Width
336 }
337 }
338 // } else {
339 // return n
340 // }
341
342 self.Height += self.Padding.Bottom
343
344 (*state)[n.Properties.Id] = self
345
346 // Sorting the array by the Level field
347 sort.Slice(plugins, func(i, j int) bool {
348 return plugins[i].Level < plugins[j].Level
349 })
350
351 for _, v := range plugins {
352 if v.Selector(n) {
353 v.Handler(n, state)
354 }
355 }
356
357 // CheckNode(n, state)
358
359 return n
360}
361
362func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
363 // Split the shorthand into components
364 borderComponents := strings.Fields(cssProperties["border"])
365
366 // Ensure there are at least 1 component (width or style or color)
367 if len(borderComponents) >= 1 {
368 width := "0px" // Default width
369 style := "solid"
370 borderColor := "#000000" // Default color
371
372 // Extract style and color if available
373 if len(borderComponents) >= 1 {
374 width = borderComponents[0]
375 }
376
377 // Extract style and color if available
378 if len(borderComponents) >= 2 {
379 style = borderComponents[1]
380 }
381 if len(borderComponents) >= 3 {
382 borderColor = borderComponents[2]
383 }
384
385 parsedColor, _ := color.Color(borderColor)
386
387 w := utils.ConvertToPixels(width, self.EM, parent.Width)
388
389 return element.Border{
390 Width: w,
391 Style: style,
392 Color: parsedColor,
393 Radius: cssProperties["border-radius"],
394 }, nil
395 }
396
397 return element.Border{}, fmt.Errorf("invalid border shorthand format")
398}
399
400func genTextNode(n *element.Node, state *map[string]element.State) element.State {
401 s := *state
402 self := s[n.Properties.Id]
403 parent := s[n.Parent.Properties.Id]
404
405 text := element.Text{}
406
407 bold, italic := false, false
408
409 if n.Style["font-weight"] == "bold" {
410 bold = true
411 }
412
413 if n.Style["font-style"] == "italic" {
414 italic = true
415 }
416
417 if text.Font == nil {
418 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
419 text.Font = f
420 }
421
422 letterSpacing := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, parent.Width)
423 wordSpacing := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, parent.Width)
424 lineHeight := utils.ConvertToPixels(n.Style["line-height"], self.EM, parent.Width)
425 if lineHeight == 0 {
426 lineHeight = self.EM + 3
427 }
428
429 text.LineHeight = int(lineHeight)
430 text.WordSpacing = int(wordSpacing)
431 text.LetterSpacing = int(letterSpacing)
432 wb := " "
433
434 if n.Style["word-wrap"] == "break-word" {
435 wb = ""
436 }
437
438 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
439 wb = ""
440 }
441
442 var dt float32
443
444 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
445 dt = self.EM / 7
446 } else {
447 dt = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, parent.Width)
448 }
449
450 col := color.Parse(n.Style, "font")
451
452 self.Color = col
453
454 text.Color = col
455 text.DecorationColor = color.Parse(n.Style, "decoration")
456 text.Align = n.Style["text-align"]
457 text.WordBreak = wb
458 text.WordSpacing = int(wordSpacing)
459 text.LetterSpacing = int(letterSpacing)
460 text.WhiteSpace = n.Style["white-space"]
461 text.DecorationThickness = int(dt)
462 text.Overlined = n.Style["text-decoration"] == "overline"
463 text.Underlined = n.Style["text-decoration"] == "underline"
464 text.LineThrough = n.Style["text-decoration"] == "linethrough"
465 text.EM = int(self.EM)
466 text.Width = int(parent.Width)
467 text.Text = n.InnerText
468
469 if n.Style["word-spacing"] == "" {
470 text.WordSpacing = font.MeasureSpace(&text)
471 }
472
473 img, width := font.Render(&text)
474 self.Texture = img
475
476 if n.Style["height"] == "" {
477 self.Height = float32(text.LineHeight)
478 }
479
480 if n.Style["width"] == "" {
481 self.Width = float32(width)
482 }
483
484 return self
485}