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, 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"] != "inline" {
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 // !TODO: Make something that stops rendering things out of fov
319
320 if self.Y < c.Height {
321 var childYOffset float32
322 for i := 0; i < len(n.Children); i++ {
323 v := n.Children[i]
324 v.Parent = n
325 // This is were the tainting comes from
326 n.Children[i] = *c.ComputeNodeStyle(&v, state)
327
328 cState := (*state)[n.Children[i].Properties.Id]
329 if n.Style["height"] == "" {
330 if v.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
331 childYOffset = cState.Y + cState.Height
332 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
333 self.Height += cState.Margin.Top
334 self.Height += cState.Margin.Bottom
335 self.Height += cState.Padding.Top
336 self.Height += cState.Padding.Bottom
337 }
338 }
339 if cState.Width > self.Width {
340 self.Width = cState.Width
341 }
342 }
343 } else {
344 return n
345 }
346
347 self.Height += self.Padding.Bottom
348
349 (*state)[n.Properties.Id] = self
350
351 // Sorting the array by the Level field
352 sort.Slice(plugins, func(i, j int) bool {
353 return plugins[i].Level < plugins[j].Level
354 })
355
356 for _, v := range plugins {
357 if v.Selector(n) {
358 v.Handler(n, state)
359 }
360 }
361
362 // CheckNode(n, state)
363
364 return n
365}
366
367func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
368 // Split the shorthand into components
369 borderComponents := strings.Fields(cssProperties["border"])
370
371 // Ensure there are at least 1 component (width or style or color)
372 if len(borderComponents) >= 1 {
373 width := "0px" // Default width
374 style := "solid"
375 borderColor := "#000000" // Default color
376
377 // Extract style and color if available
378 if len(borderComponents) >= 1 {
379 width = borderComponents[0]
380 }
381
382 // Extract style and color if available
383 if len(borderComponents) >= 2 {
384 style = borderComponents[1]
385 }
386 if len(borderComponents) >= 3 {
387 borderColor = borderComponents[2]
388 }
389
390 parsedColor, _ := color.Color(borderColor)
391
392 w, _ := utils.ConvertToPixels(width, self.EM, parent.Width)
393
394 return element.Border{
395 Width: w,
396 Style: style,
397 Color: parsedColor,
398 Radius: cssProperties["border-radius"],
399 }, nil
400 }
401
402 return element.Border{}, fmt.Errorf("invalid border shorthand format")
403}
404
405func genTextNode(n *element.Node, state *map[string]element.State) element.State {
406 s := *state
407 self := s[n.Properties.Id]
408 parent := s[n.Parent.Properties.Id]
409
410 text := element.Text{}
411
412 bold, italic := false, false
413
414 if n.Style["font-weight"] == "bold" {
415 bold = true
416 }
417
418 if n.Style["font-style"] == "italic" {
419 italic = true
420 }
421
422 if text.Font == nil {
423 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
424 text.Font = f
425 }
426
427 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, parent.Width)
428 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, parent.Width)
429 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], self.EM, parent.Width)
430 if lineHeight == 0 {
431 lineHeight = self.EM + 3
432 }
433
434 text.LineHeight = int(lineHeight)
435 text.WordSpacing = int(wordSpacing)
436 text.LetterSpacing = int(letterSpacing)
437 wb := " "
438
439 if n.Style["word-wrap"] == "break-word" {
440 wb = ""
441 }
442
443 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
444 wb = ""
445 }
446
447 var dt float32
448
449 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
450 dt = self.EM / 7
451 } else {
452 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, parent.Width)
453 }
454
455 col := color.Parse(n.Style, "font")
456
457 self.Color = col
458
459 text.Color = col
460 text.DecorationColor = color.Parse(n.Style, "decoration")
461 text.Align = n.Style["text-align"]
462 text.WordBreak = wb
463 text.WordSpacing = int(wordSpacing)
464 text.LetterSpacing = int(letterSpacing)
465 text.WhiteSpace = n.Style["white-space"]
466 text.DecorationThickness = int(dt)
467 text.Overlined = n.Style["text-decoration"] == "overline"
468 text.Underlined = n.Style["text-decoration"] == "underline"
469 text.LineThrough = n.Style["text-decoration"] == "linethrough"
470 text.EM = int(self.EM)
471 text.Width = int(parent.Width)
472 text.Text = n.InnerText
473
474 if n.Style["word-spacing"] == "" {
475 text.WordSpacing = font.MeasureSpace(&text)
476 }
477
478 img, width := font.Render(&text)
479 self.Texture = img
480
481 if n.Style["height"] == "" {
482 self.Height = float32(text.LineHeight)
483 }
484
485 if n.Style["width"] == "" {
486 self.Width = float32(width)
487 }
488
489 return self
490}