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