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