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.CreateElement("notaspan")
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 // This is were the tainting comes from
340 n.Children[i] = *c.ComputeNodeStyle(&v, state)
341
342 cState := (*state)[n.Children[i].Properties.Id]
343 if self.Style["height"] == "" {
344 if cState.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
345 childYOffset = cState.Y + cState.Height
346 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
347 self.Height += cState.Margin.Top
348 self.Height += cState.Margin.Bottom
349 self.Height += cState.Padding.Top
350 self.Height += cState.Padding.Bottom
351 }
352 }
353 if cState.Width > self.Width {
354 self.Width = cState.Width
355 }
356 }
357
358 self.Height += self.Padding.Bottom
359
360 (*state)[n.Properties.Id] = self
361
362 // Sorting the array by the Level field
363 sort.Slice(plugins, func(i, j int) bool {
364 return plugins[i].Level < plugins[j].Level
365 })
366
367 for _, v := range plugins {
368 matches := true
369 for name, value := range v.Styles {
370 if self.Style[name] != value && !(value == "*") && self.Style[name] != "" {
371 matches = false
372 }
373 }
374 if matches {
375 // !NOTE: Might save memory by making a state map tree and passing that instead of the node it's self
376 v.Handler(n, state)
377 }
378 }
379
380 // !IMPORTAINT: Tomorrow the way textt should work is all free standing text should be in text elements, then the words should be notaspan
381 // + so in theory the inner/outerhtml methods can clean those and after the notspans are rendered (I don't know if removing them will do the same)
382 // + (thing as below) but the text should be a text element so it shows childNodes bc just children is repetitive
383 n.InnerHTML = utils.InnerHTML(*n)
384 tag, closing := utils.NodeToHTML(*n)
385 n.OuterHTML = tag + n.InnerHTML + closing
386
387 // !NOTE: I think that .Children can just act like .childNodes but the text needs to be joined into one "text" node for each line
388 // + So it is ok to modifey the DOM but only to make text nodes and do the innerHTML
389 // + also I think innerHTML should be the main source of truth, but if innerHTML == "" then generate the html and if it changes update the node
390 // + but if the DOM under it changes then you would need to update it aswell
391
392 // CheckNode(n, state)
393 // toRemove := make([]int, 0)
394 // for i := len(n.Children) - 1; i >= 1; i-- {
395 // v := n.Children[i]
396 // next := n.Children[i-1]
397 // if v.TagName == next.TagName {
398 // matches := true
399 // for k, t := range v.Style {
400 // if next.Style[k] != t {
401 // matches = false
402 // }
403 // }
404 // if matches {
405 // // fmt.Println(n.Properties.Id)
406 // n.Children[i-1].InnerText = n.Children[i-1].InnerText + " " + v.InnerText
407
408 // toRemove = append(toRemove, i)
409 // }
410 // }
411 // }
412 // for _, index := range toRemove {
413 // n.Children = append(n.Children[:index], n.Children[index+1:]...)
414 // }
415 // if len(toRemove) > 0 {
416 // for _, v := range n.Children {
417 // fmt.Println(v.InnerText)
418 // }
419 // }
420
421 return n
422}
423
424func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
425 // Split the shorthand into components
426 borderComponents := strings.Fields(cssProperties["border"])
427
428 // Ensure there are at least 1 component (width or style or color)
429 if len(borderComponents) >= 1 {
430 width := "0px" // Default width
431 style := "solid"
432 borderColor := "#000000" // Default color
433
434 // Extract style and color if available
435 if len(borderComponents) >= 1 {
436 width = borderComponents[0]
437 }
438
439 // Extract style and color if available
440 if len(borderComponents) >= 2 {
441 style = borderComponents[1]
442 }
443 if len(borderComponents) >= 3 {
444 borderColor = borderComponents[2]
445 }
446
447 parsedColor, _ := color.Color(borderColor)
448
449 w, _ := utils.ConvertToPixels(width, self.EM, parent.Width)
450
451 return element.Border{
452 Width: w,
453 Style: style,
454 Color: parsedColor,
455 Radius: cssProperties["border-radius"],
456 }, nil
457 }
458
459 return element.Border{}, fmt.Errorf("invalid border shorthand format")
460}
461
462func genTextNode(n *element.Node, state *map[string]element.State) element.State {
463 s := *state
464 self := s[n.Properties.Id]
465 parent := s[n.Parent.Properties.Id]
466
467 text := element.Text{}
468
469 bold, italic := false, false
470
471 if self.Style["font-weight"] == "bold" {
472 bold = true
473 }
474
475 if self.Style["font-style"] == "italic" {
476 italic = true
477 }
478
479 if text.Font == nil {
480 f, _ := font.LoadFont(self.Style["font-family"], int(self.EM), bold, italic)
481 text.Font = f
482 }
483
484 letterSpacing, _ := utils.ConvertToPixels(self.Style["letter-spacing"], self.EM, parent.Width)
485 wordSpacing, _ := utils.ConvertToPixels(self.Style["word-spacing"], self.EM, parent.Width)
486 lineHeight, _ := utils.ConvertToPixels(self.Style["line-height"], self.EM, parent.Width)
487 if lineHeight == 0 {
488 lineHeight = self.EM + 3
489 }
490
491 text.LineHeight = int(lineHeight)
492 text.WordSpacing = int(wordSpacing)
493 text.LetterSpacing = int(letterSpacing)
494 wb := " "
495
496 if self.Style["word-wrap"] == "break-word" {
497 wb = ""
498 }
499
500 if self.Style["text-wrap"] == "wrap" || self.Style["text-wrap"] == "balance" {
501 wb = ""
502 }
503
504 var dt float32
505
506 if self.Style["text-decoration-thickness"] == "auto" || self.Style["text-decoration-thickness"] == "" {
507 dt = self.EM / 7
508 } else {
509 dt, _ = utils.ConvertToPixels(self.Style["text-decoration-thickness"], self.EM, parent.Width)
510 }
511
512 col := color.Parse(self.Style, "font")
513
514 self.Color = col
515
516 text.Color = col
517 text.DecorationColor = color.Parse(self.Style, "decoration")
518 text.Align = self.Style["text-align"]
519 text.WordBreak = wb
520 text.WordSpacing = int(wordSpacing)
521 text.LetterSpacing = int(letterSpacing)
522 text.WhiteSpace = self.Style["white-space"]
523 text.DecorationThickness = int(dt)
524 text.Overlined = self.Style["text-decoration"] == "overline"
525 text.Underlined = self.Style["text-decoration"] == "underline"
526 text.LineThrough = self.Style["text-decoration"] == "linethrough"
527 text.EM = int(self.EM)
528 text.Width = int(parent.Width)
529 text.Text = n.InnerText
530
531 if self.Style["word-spacing"] == "" {
532 text.WordSpacing = font.MeasureSpace(&text)
533 }
534
535 img, width := font.Render(&text)
536 self.Texture = img
537
538 if self.Style["height"] == "" {
539 self.Height = float32(text.LineHeight)
540 }
541
542 if self.Style["width"] == "" {
543 self.Width = float32(width)
544 }
545
546 return self
547}