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