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(n *element.Node, state *map[string]element.State) *element.Node {
143
144 // Head is not renderable
145 if utils.IsParent(*n, "head") {
146 return n
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[n.Properties.Id]
155 parent := s[n.Parent.Properties.Id]
156 self.Style = c.GetStyles(*n)
157
158 self.Background = color.Parse(self.Style, "background")
159 self.Border, _ = CompleteBorder(self.Style, self, parent)
160
161 fs, _ := utils.ConvertToPixels(self.Style["font-size"], parent.EM, parent.Width)
162 self.EM = fs
163
164 if self.Style["display"] == "none" {
165 self.X = 0
166 self.Y = 0
167 self.Width = 0
168 self.Height = 0
169 return n
170 }
171
172 if self.Style["width"] == "" && self.Style["display"] == "block" {
173 self.Style["width"] = "100%"
174 }
175
176 // Set Z index value to be sorted in window
177 if self.Style["z-index"] != "" {
178 z, _ := strconv.Atoi(self.Style["z-index"])
179 self.Z = float32(z)
180 }
181
182 if parent.Z > 0 {
183 self.Z = parent.Z + 1
184 }
185
186 (*state)[n.Properties.Id] = self
187
188 wh := utils.GetWH(*n, state)
189 width := wh.Width
190 height := wh.Height
191
192 x, y := parent.X, parent.Y
193 // !NOTE: Would like to consolidate all XY function into this function like WH
194 offsetX, offsetY := utils.GetXY(n, state)
195 x += offsetX
196 y += offsetY
197
198 var top, left, right, bottom bool = false, false, false, false
199
200 m := utils.GetMP(*n, wh, state, "margin")
201 p := utils.GetMP(*n, wh, state, "padding")
202
203 self.Margin = m
204 self.Padding = p
205
206 if self.Style["position"] == "absolute" {
207 bas := utils.GetPositionOffsetNode(n, state)
208 base := s[bas.Properties.Id]
209 if self.Style["top"] != "" {
210 v, _ := utils.ConvertToPixels(self.Style["top"], self.EM, parent.Width)
211 y = v + base.Y
212 top = true
213 }
214 if self.Style["left"] != "" {
215 v, _ := utils.ConvertToPixels(self.Style["left"], self.EM, parent.Width)
216 x = v + base.X
217 left = true
218 }
219 if self.Style["right"] != "" {
220 v, _ := utils.ConvertToPixels(self.Style["right"], self.EM, parent.Width)
221 x = (base.Width - width) - v
222 right = true
223 }
224 if self.Style["bottom"] != "" {
225 v, _ := utils.ConvertToPixels(self.Style["bottom"], self.EM, parent.Width)
226 y = (base.Height - height) - v
227 bottom = true
228 }
229
230 } else {
231 for i, v := range n.Parent.Children {
232 vState := s[v.Properties.Id]
233 if vState.Style["position"] != "absolute" {
234 if v.Properties.Id == n.Properties.Id {
235 if i-1 > 0 {
236 sib := n.Parent.Children[i-1]
237 sibling := s[sib.Properties.Id]
238 if sibling.Style["position"] != "absolute" {
239 if self.Style["display"] == "inline" {
240 if sibling.Style["display"] == "inline" {
241 y = sibling.Y
242 } else {
243 y = sibling.Y + sibling.Height
244 }
245 } else {
246 y = sibling.Y + sibling.Height + (sibling.Border.Width * 2) + sibling.Margin.Bottom
247 }
248 }
249
250 }
251 break
252 } else if self.Style["display"] != "inline" {
253 vState := s[v.Properties.Id]
254 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (self.Border.Width)
255 }
256 }
257 }
258 }
259
260 // Display modes need to be calculated here
261
262 relPos := !top && !left && !right && !bottom
263
264 if left || relPos {
265 x += m.Left
266 }
267 if top || relPos {
268 y += m.Top
269 }
270 if right {
271 x -= m.Right
272 }
273 if bottom {
274 y -= m.Bottom
275 }
276
277 self.X = x
278 self.Y = y
279 self.Width = width
280 self.Height = height
281 (*state)[n.Properties.Id] = self
282
283 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
284 // Confirm text exists
285 words := strings.Split(strings.TrimSpace(n.InnerText), " ")
286 if len(words) != 1 {
287 if self.Style["display"] == "inline" {
288 n.InnerText = words[0]
289 n.Style["inlineText"] = "true"
290 el := *n
291 el.InnerText = strings.Join(words[1:], " ")
292 n.Parent.InsertAfter(el, *n)
293 } else {
294 el := n.CreateElement("notaspan")
295 el.InnerText = n.InnerText
296 n.AppendChild(el)
297 self.Style["font-size"] = parent.Style["font-size"]
298 self.EM = parent.EM
299 n.InnerText = ""
300 }
301 (*state)[n.Properties.Id] = self
302 }
303 if len(strings.TrimSpace(n.InnerText)) > 0 {
304 n.InnerText = strings.TrimSpace(n.InnerText)
305 self = genTextNode(n, state)
306 }
307 }
308
309 (*state)[n.Properties.Id] = self
310 (*state)[n.Parent.Properties.Id] = parent
311
312 // Call children here
313
314 var childYOffset float32
315 for i := 0; i < len(n.Children); i++ {
316 v := n.Children[i]
317 v.Parent = n
318 // This is were the tainting comes from
319 n.Children[i] = *c.ComputeNodeStyle(&v, state)
320
321 cState := (*state)[n.Children[i].Properties.Id]
322 if self.Style["height"] == "" {
323 if cState.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
324 childYOffset = cState.Y + cState.Height
325 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
326 self.Height += cState.Margin.Top
327 self.Height += cState.Margin.Bottom
328 self.Height += cState.Padding.Top
329 self.Height += cState.Padding.Bottom
330 }
331 }
332 if cState.Width > self.Width {
333 self.Width = cState.Width
334 }
335 }
336
337 self.Height += self.Padding.Bottom
338
339 (*state)[n.Properties.Id] = self
340
341 // Sorting the array by the Level field
342 sort.Slice(plugins, func(i, j int) bool {
343 return plugins[i].Level < plugins[j].Level
344 })
345
346 for _, v := range plugins {
347 matches := true
348 for name, value := range v.Styles {
349 if self.Style[name] != value && !(value == "*") && self.Style[name] != "" {
350 matches = false
351 }
352 }
353 if matches {
354 // !NOTE: Might save memory by making a state map tree and passing that instead of the node it's self
355 v.Handler(n, state)
356 }
357 }
358
359 // n.InnerHTML = utils.InnerHTML(*n)
360 // tag, closing := utils.NodeToHTML(*n)
361 // n.OuterHTML = tag + n.InnerHTML + closing
362
363 return n
364}
365
366func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
367 // Split the shorthand into components
368 borderComponents := strings.Fields(cssProperties["border"])
369
370 // Ensure there are at least 1 component (width or style or color)
371 if len(borderComponents) >= 1 {
372 width := "0px" // Default width
373 style := "solid"
374 borderColor := "#000000" // Default color
375
376 // Extract style and color if available
377 if len(borderComponents) >= 1 {
378 width = borderComponents[0]
379 }
380
381 // Extract style and color if available
382 if len(borderComponents) >= 2 {
383 style = borderComponents[1]
384 }
385 if len(borderComponents) >= 3 {
386 borderColor = borderComponents[2]
387 }
388
389 parsedColor, _ := color.Color(borderColor)
390
391 w, _ := utils.ConvertToPixels(width, self.EM, parent.Width)
392
393 return element.Border{
394 Width: w,
395 Style: style,
396 Color: parsedColor,
397 Radius: cssProperties["border-radius"],
398 }, nil
399 }
400
401 return element.Border{}, fmt.Errorf("invalid border shorthand format")
402}
403
404func genTextNode(n *element.Node, state *map[string]element.State) element.State {
405 s := *state
406 self := s[n.Properties.Id]
407 parent := s[n.Parent.Properties.Id]
408
409 text := element.Text{}
410
411 bold, italic := false, false
412
413 if self.Style["font-weight"] == "bold" {
414 bold = true
415 }
416
417 if self.Style["font-style"] == "italic" {
418 italic = true
419 }
420
421 if text.Font == nil {
422 f, _ := font.LoadFont(self.Style["font-family"], int(self.EM), bold, italic)
423 text.Font = f
424 }
425
426 letterSpacing, _ := utils.ConvertToPixels(self.Style["letter-spacing"], self.EM, parent.Width)
427 wordSpacing, _ := utils.ConvertToPixels(self.Style["word-spacing"], self.EM, parent.Width)
428 lineHeight, _ := utils.ConvertToPixels(self.Style["line-height"], self.EM, parent.Width)
429 if lineHeight == 0 {
430 lineHeight = self.EM + 3
431 }
432
433 text.LineHeight = int(lineHeight)
434 text.WordSpacing = int(wordSpacing)
435 text.LetterSpacing = int(letterSpacing)
436 wb := " "
437
438 if self.Style["word-wrap"] == "break-word" {
439 wb = ""
440 }
441
442 if self.Style["text-wrap"] == "wrap" || self.Style["text-wrap"] == "balance" {
443 wb = ""
444 }
445
446 var dt float32
447
448 if self.Style["text-decoration-thickness"] == "auto" || self.Style["text-decoration-thickness"] == "" {
449 dt = self.EM / 7
450 } else {
451 dt, _ = utils.ConvertToPixels(self.Style["text-decoration-thickness"], self.EM, parent.Width)
452 }
453
454 col := color.Parse(self.Style, "font")
455
456 self.Color = col
457
458 text.Color = col
459 text.DecorationColor = color.Parse(self.Style, "decoration")
460 text.Align = self.Style["text-align"]
461 text.WordBreak = wb
462 text.WordSpacing = int(wordSpacing)
463 text.LetterSpacing = int(letterSpacing)
464 text.WhiteSpace = self.Style["white-space"]
465 text.DecorationThickness = int(dt)
466 text.Overlined = self.Style["text-decoration"] == "overline"
467 text.Underlined = self.Style["text-decoration"] == "underline"
468 text.LineThrough = self.Style["text-decoration"] == "linethrough"
469 text.EM = int(self.EM)
470 text.Width = int(parent.Width)
471 text.Text = n.InnerText
472
473 if self.Style["word-spacing"] == "" {
474 text.WordSpacing = font.MeasureSpace(&text)
475 }
476
477 img, width := font.Render(&text)
478 self.Texture = img
479
480 if self.Style["height"] == "" {
481 self.Height = float32(text.LineHeight)
482 }
483
484 if self.Style["width"] == "" {
485 self.Width = float32(width)
486 }
487
488 return self
489}