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
3// package aui/goldie
4// https://pkg.go.dev/automated.sh/goldie
5// https://pkg.go.dev/automated.sh/aui
6// https://pkg.go.dev/automated.sh/oat
7
8import (
9 "fmt"
10 "gui/color"
11 "gui/element"
12 "gui/font"
13 "gui/parser"
14 "gui/utils"
15 "os"
16 "slices"
17 "sort"
18 "strings"
19)
20
21type Plugin struct {
22 Styles map[string]string
23 Level int
24 Handler func(*element.Node, *map[string]element.State)
25}
26
27type CSS struct {
28 Width float32
29 Height float32
30 StyleSheets []map[string]map[string]string
31 Plugins []Plugin
32 Document *element.Node
33}
34
35func (c *CSS) StyleSheet(path string) {
36 // Parse the CSS file
37 dat, err := os.ReadFile(path)
38 utils.Check(err)
39 styles := parser.ParseCSS(string(dat))
40
41 c.StyleSheets = append(c.StyleSheets, styles)
42}
43
44func (c *CSS) StyleTag(css string) {
45 styles := parser.ParseCSS(css)
46 c.StyleSheets = append(c.StyleSheets, styles)
47}
48
49var inheritedProps = []string{
50 "color",
51 "cursor",
52 "font",
53 "font-family",
54 "font-size",
55 "font-style",
56 "font-weight",
57 "letter-spacing",
58 "line-height",
59 "text-align",
60 "text-indent",
61 "text-justify",
62 "text-shadow",
63 "text-transform",
64 "visibility",
65 "word-spacing",
66 "display",
67}
68
69func (c *CSS) GetStyles(n element.Node) map[string]string {
70 styles := map[string]string{}
71
72 if n.Parent != nil {
73 ps := c.GetStyles(*n.Parent)
74 for _, v := range inheritedProps {
75 if ps[v] != "" {
76 styles[v] = ps[v]
77 }
78 }
79 }
80 for k, v := range n.Style {
81 styles[k] = v
82 }
83 hovered := false
84 if slices.Contains(n.ClassList.Classes, ":hover") {
85 hovered = true
86 }
87
88 for _, styleSheet := range c.StyleSheets {
89 for selector := range styleSheet {
90 // fmt.Println(selector, n.Properties.Id)
91 key := selector
92 if strings.Contains(selector, ":hover") && hovered {
93 selector = strings.Replace(selector, ":hover", "", -1)
94 }
95 if element.TestSelector(selector, &n) {
96 for k, v := range styleSheet[key] {
97 styles[k] = v
98 }
99 }
100
101 }
102 }
103
104 // !ISSUE: why is this needed, the "attribute" is n.Style that should be mapped during init
105 // + when a user adds a style via the style attirbute it will just be in the .Style prop...
106 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
107 styles = utils.Merge(styles, inline)
108 // add hover and focus css events
109
110 return styles
111}
112
113func (c *CSS) AddPlugin(plugin Plugin) {
114 c.Plugins = append(c.Plugins, plugin)
115}
116
117func CheckNode(n *element.Node, state *map[string]element.State) {
118 s := *state
119 self := s[n.Properties.Id]
120
121 fmt.Println(n.TagName, n.Properties.Id)
122 fmt.Printf("ID: %v\n", n.Id)
123 fmt.Printf("Parent: %v\n", n.Parent.TagName)
124 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
125 fmt.Printf("Text: %v\n", n.InnerText)
126 fmt.Printf("X: %v, Y: %v\n", self.X, self.Y)
127 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
128 fmt.Printf("Styles: %v\n", n.Style)
129 fmt.Printf("Background: %v\n", self.Background)
130 fmt.Printf("Border: %v\n\n\n", self.Border)
131}
132
133func (c *CSS) ComputeNodeStyle(n *element.Node, state *map[string]element.State) *element.Node {
134 // Head is not renderable
135 if utils.IsParent(*n, "head") {
136 return n
137 }
138 plugins := c.Plugins
139 // !ISSUE: This should add to state.Style instead as the element.Node should be un effected by the engine
140 // + currently this adds styles to the style attribute that the use did not explisitly set
141 // + this also applies to the margin/padding and border completer functions
142
143 n.Style = c.GetStyles(*n)
144 s := *state
145 self := s[n.Properties.Id]
146 parent := s[n.Parent.Properties.Id]
147
148 self.Background = color.Parse(n.Style, "background")
149 self.Border, _ = CompleteBorder(n.Style)
150
151 fs, _ := utils.ConvertToPixels(n.Style["font-size"], parent.EM, parent.Width)
152 self.EM = fs
153
154 if n.Style["display"] == "none" {
155 self.X = 0
156 self.Y = 0
157 self.Width = 0
158 self.Height = 0
159 return n
160 }
161
162 wh := utils.GetWH(*n)
163 width := wh.Width
164 height := wh.Height
165
166 x, y := parent.X, parent.Y
167
168 var top, left, right, bottom bool = false, false, false, false
169
170 m := utils.GetMP(*n, "margin")
171 p := utils.GetMP(*n, "padding")
172
173 self.Margin = m
174 self.Padding = p
175
176 if n.Style["position"] == "absolute" {
177 bas := utils.GetPositionOffsetNode(n)
178 base := s[bas.Properties.Id]
179 if n.Style["top"] != "" {
180 v, _ := utils.ConvertToPixels(n.Style["top"], self.EM, parent.Width)
181 y = v + base.Y
182 top = true
183 }
184 if n.Style["left"] != "" {
185 v, _ := utils.ConvertToPixels(n.Style["left"], self.EM, parent.Width)
186 x = v + base.X
187 left = true
188 }
189 if n.Style["right"] != "" {
190 v, _ := utils.ConvertToPixels(n.Style["right"], self.EM, parent.Width)
191 x = (base.Width - width) - v
192 right = true
193 }
194 if n.Style["bottom"] != "" {
195 v, _ := utils.ConvertToPixels(n.Style["bottom"], self.EM, parent.Width)
196 y = (base.Height - height) - v
197 bottom = true
198 }
199 } else {
200 // !ISSUE: Blue square goes under a absoulutly positioned element when it should pass through
201 for i, v := range n.Parent.Children {
202 if v.Properties.Id == n.Properties.Id {
203 if i-1 > 0 {
204 sib := n.Parent.Children[i-1]
205 sibling := s[sib.Properties.Id]
206 if n.Style["display"] == "inline" {
207 if sib.Style["display"] == "inline" {
208 y = sibling.Y
209 } else {
210 y = sibling.Y + sibling.Height
211 }
212 } else {
213 y = sibling.Y + sibling.Height
214 }
215 }
216 break
217 } else if n.Style["display"] != "inline" {
218 vState := s[v.Properties.Id]
219 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height
220 }
221 }
222 }
223
224 // Display modes need to be calculated here
225
226 relPos := !top && !left && !right && !bottom
227
228 if left || relPos {
229 x += m.Left
230 }
231 if top || relPos {
232 y += m.Top
233 }
234 if right {
235 x -= m.Right
236 }
237 if bottom {
238 y -= m.Bottom
239 }
240
241 // fmt.Println(n.InnerText, len(n.Children))
242 self.X = x + parent.Padding.Left
243 self.Y = y
244 self.Width = width
245 self.Height = height + self.Padding.Bottom
246 (*state)[n.Properties.Id] = self
247
248 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
249 // Confirm text exists
250 words := strings.Split(strings.TrimSpace(n.InnerText), " ")
251 if len(words) != 1 {
252 if n.Style["display"] == "inline" {
253 // !ISSUE: this works great and it is how I want it to work but it does modifiy the dom which is a no go
254 n.InnerText = words[0]
255 el := *n
256 el.InnerText = strings.Join(words[1:], " ")
257 n.Parent.InsertAfter(el, *n)
258 }
259 }
260 if len(strings.TrimSpace(n.InnerText)) > 0 {
261 n.InnerText = strings.TrimSpace(n.InnerText)
262 self = genTextNode(n, state)
263 }
264 }
265
266 (*state)[n.Properties.Id] = self
267 (*state)[n.Parent.Properties.Id] = parent
268
269 // Call children here
270
271 var childYOffset float32
272 for i := 0; i < len(n.Children); i++ {
273 v := n.Children[i]
274 v.Parent = n
275 n.Children[i] = *c.ComputeNodeStyle(&v, state)
276 cState := (*state)[n.Children[i].Properties.Id]
277 if n.Style["height"] == "" {
278 if n.Children[i].Style["position"] != "absolute" && cState.Y > childYOffset {
279 childYOffset = cState.Y
280 self.Height += cState.Height
281 self.Height += cState.Margin.Top
282 self.Height += cState.Margin.Bottom
283 self.Height += cState.Padding.Top
284 self.Height += cState.Padding.Bottom
285 }
286 }
287 // fmt.Println(n.TagName, self.Width, v.TagName, cState.Width)
288 if cState.Width > self.Width {
289 self.Width = cState.Width
290 }
291 }
292
293 (*state)[n.Properties.Id] = self
294
295 // Sorting the array by the Level field
296 sort.Slice(plugins, func(i, j int) bool {
297 return plugins[i].Level < plugins[j].Level
298 })
299
300 for _, v := range plugins {
301 matches := true
302 for name, value := range v.Styles {
303 if n.Style[name] != value && !(value == "*") {
304 matches = false
305 }
306 }
307 if matches {
308 v.Handler(n, state)
309 }
310 }
311
312 for i := range n.Children {
313 cState := (*state)[n.Children[i].Properties.Id]
314 cState.Y += self.Padding.Top
315 (*state)[n.Children[i].Properties.Id] = cState
316 }
317
318 // CheckNode(n, state)
319
320 return n
321}
322
323func parseBorderShorthand(borderShorthand string) (element.Border, error) {
324 // Split the shorthand into components
325 borderComponents := strings.Fields(borderShorthand)
326
327 // Ensure there are at least 1 component (width or style or color)
328 if len(borderComponents) >= 1 {
329 width := "0px" // Default width
330 style := "solid"
331 borderColor := "#000000" // Default color
332
333 // Extract style and color if available
334 if len(borderComponents) >= 1 {
335 width = borderComponents[0]
336 }
337
338 // Extract style and color if available
339 if len(borderComponents) >= 2 {
340 style = borderComponents[1]
341 }
342 if len(borderComponents) >= 3 {
343 borderColor = borderComponents[2]
344 }
345
346 parsedColor, _ := color.Color(borderColor)
347
348 return element.Border{
349 Width: width,
350 Style: style,
351 Color: parsedColor,
352 Radius: "", // Default radius
353 }, nil
354 }
355
356 return element.Border{}, fmt.Errorf("invalid border shorthand format")
357}
358
359func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
360 border, err := parseBorderShorthand(cssProperties["border"])
361 border.Radius = cssProperties["border-radius"]
362
363 return border, err
364}
365
366func genTextNode(n *element.Node, state *map[string]element.State) element.State {
367 s := *state
368 self := s[n.Properties.Id]
369 parent := s[n.Parent.Properties.Id]
370
371 text := element.Text{}
372
373 bold, italic := false, false
374
375 if n.Style["font-weight"] == "bold" {
376 bold = true
377 }
378
379 if n.Style["font-style"] == "italic" {
380 italic = true
381 }
382
383 if text.Font == nil {
384 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
385 text.Font = f
386 }
387
388 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, parent.Width)
389 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, parent.Width)
390 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], self.EM, parent.Width)
391 if lineHeight == 0 {
392 lineHeight = self.EM + 3
393 }
394
395 text.LineHeight = int(lineHeight)
396 text.WordSpacing = int(wordSpacing)
397 text.LetterSpacing = int(letterSpacing)
398 wb := " "
399
400 if n.Style["word-wrap"] == "break-word" {
401 wb = ""
402 }
403
404 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
405 wb = ""
406 }
407
408 var dt float32
409
410 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
411 dt = 3
412 } else {
413 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, parent.Width)
414 }
415
416 col := color.Parse(n.Style, "font")
417
418 self.Color = col
419
420 text.Color = col
421 text.DecorationColor = color.Parse(n.Style, "decoration")
422 text.Align = n.Style["text-align"]
423 text.WordBreak = wb
424 text.WordSpacing = int(wordSpacing)
425 text.LetterSpacing = int(letterSpacing)
426 text.WhiteSpace = n.Style["white-space"]
427 text.DecorationThickness = int(dt)
428 text.Overlined = n.Style["text-decoration"] == "overline"
429 text.Underlined = n.Style["text-decoration"] == "underline"
430 text.LineThrough = n.Style["text-decoration"] == "linethrough"
431 text.EM = int(self.EM)
432 text.Width = int(parent.Width)
433 text.Text = n.InnerText
434
435 if n.Style["word-spacing"] == "" {
436 text.WordSpacing = font.MeasureSpace(&text)
437 }
438
439 img, width := font.Render(&text)
440 self.Texture = img
441 // self.Text.Width = int(width)
442 self.Width = float32(width)
443
444 if n.Style["height"] == "" {
445 self.Height = float32(text.LineHeight)
446 }
447
448 return self
449}