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