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", self.Text.Text)
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 for i, v := range n.Parent.Children {
201 if v.Properties.Id == n.Properties.Id {
202 if i-1 > 0 {
203 sib := n.Parent.Children[i-1]
204 sibling := s[sib.Properties.Id]
205 if n.Style["display"] == "inline" {
206 if sib.Style["display"] == "inline" {
207 y = sibling.Y
208 } else {
209 y = sibling.Y + sibling.Height
210 }
211 } else {
212 y = sibling.Y + sibling.Height
213 }
214 }
215 break
216 } else if n.Style["display"] != "inline" {
217 vState := s[v.Properties.Id]
218 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height
219 }
220 }
221 }
222
223 // Display modes need to be calculated here
224
225 relPos := !top && !left && !right && !bottom
226
227 if left || relPos {
228 x += m.Left
229 }
230 if top || relPos {
231 y += m.Top
232 }
233 if right {
234 x -= m.Right
235 }
236 if bottom {
237 y -= m.Bottom
238 }
239
240 // fmt.Println(n.InnerText, len(n.Children))
241
242 if !utils.ChildrenHaveText(n) {
243 // Confirm text exists
244 if len(n.InnerText) > 0 {
245 innerWidth := width
246 innerHeight := height
247 (*state)[n.Properties.Id] = self
248 self = genTextNode(n, &innerWidth, &innerHeight, p, state)
249 width = innerWidth + p.Left + p.Right
250 height = innerHeight
251 }
252 }
253
254 self.X = x
255 self.Y = y
256 self.Width = width
257 self.Height = height
258
259 (*state)[n.Properties.Id] = self
260 (*state)[n.Parent.Properties.Id] = parent
261
262 // Call children here
263
264 var childYOffset float32
265 for i, v := range n.Children {
266 v.Parent = n
267 n.Children[i] = *c.ComputeNodeStyle(&v, state)
268 cState := (*state)[n.Children[i].Properties.Id]
269 if n.Style["height"] == "" {
270 if n.Children[i].Style["position"] != "absolute" && cState.Y > childYOffset {
271 childYOffset = cState.Y
272 self.Height += cState.Height
273 self.Height += cState.Margin.Top
274 self.Height += cState.Margin.Bottom
275 self.Height += cState.Padding.Top
276 self.Height += cState.Padding.Bottom
277 }
278 }
279 // fmt.Println(n.TagName, self.Width, v.TagName, cState.Width)
280 if cState.Width > self.Width {
281 self.Width = cState.Width
282 }
283 }
284
285 (*state)[n.Properties.Id] = self
286
287 // Sorting the array by the Level field
288 sort.Slice(plugins, func(i, j int) bool {
289 return plugins[i].Level < plugins[j].Level
290 })
291
292 for _, v := range plugins {
293 matches := true
294 for name, value := range v.Styles {
295 if n.Style[name] != value && !(value == "*") {
296 matches = false
297 }
298 }
299 if matches {
300 v.Handler(n, state)
301 }
302 }
303
304 CheckNode(n, state)
305
306 return n
307}
308
309func parseBorderShorthand(borderShorthand string) (element.Border, error) {
310 // Split the shorthand into components
311 borderComponents := strings.Fields(borderShorthand)
312
313 // Ensure there are at least 1 component (width or style or color)
314 if len(borderComponents) >= 1 {
315 width := "0px" // Default width
316 style := "solid"
317 borderColor := "#000000" // Default color
318
319 // Extract style and color if available
320 if len(borderComponents) >= 1 {
321 width = borderComponents[0]
322 }
323
324 // Extract style and color if available
325 if len(borderComponents) >= 2 {
326 style = borderComponents[1]
327 }
328 if len(borderComponents) >= 3 {
329 borderColor = borderComponents[2]
330 }
331
332 parsedColor, _ := color.Color(borderColor)
333
334 return element.Border{
335 Width: width,
336 Style: style,
337 Color: parsedColor,
338 Radius: "", // Default radius
339 }, nil
340 }
341
342 return element.Border{}, fmt.Errorf("invalid border shorthand format")
343}
344
345func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
346 border, err := parseBorderShorthand(cssProperties["border"])
347 border.Radius = cssProperties["border-radius"]
348
349 return border, err
350}
351
352func genTextNode(n *element.Node, width, height *float32, p element.MarginPadding, state *map[string]element.State) element.State {
353 s := *state
354 self := s[n.Properties.Id]
355 parent := s[n.Parent.Properties.Id]
356
357 bold, italic := false, false
358
359 if n.Style["font-weight"] == "bold" {
360 bold = true
361 }
362
363 if n.Style["font-style"] == "italic" {
364 italic = true
365 }
366
367 if self.Text.Font == nil {
368 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
369 self.Text.Font = f
370 }
371
372 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, *width)
373 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, *width)
374 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], self.EM, *width)
375 if lineHeight == 0 {
376 lineHeight = self.EM + 3
377 }
378
379 self.Text.LineHeight = int(lineHeight)
380 self.Text.WordSpacing = int(wordSpacing)
381 self.Text.LetterSpacing = int(letterSpacing)
382 wb := " "
383
384 if n.Style["word-wrap"] == "break-word" {
385 wb = ""
386 }
387
388 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
389 wb = ""
390 }
391
392 var dt float32
393
394 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
395 dt = 3
396 } else {
397 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, *width)
398 }
399
400 col := color.Parse(n.Style, "font")
401
402 self.Text.Color = col
403 self.Text.DecorationColor = color.Parse(n.Style, "decoration")
404 self.Text.Align = n.Style["text-align"]
405 self.Text.WordBreak = wb
406 self.Text.WordSpacing = int(wordSpacing)
407 self.Text.LetterSpacing = int(letterSpacing)
408 self.Text.WhiteSpace = n.Style["white-space"]
409 self.Text.DecorationThickness = int(dt)
410 self.Text.Overlined = n.Style["text-decoration"] == "overline"
411 self.Text.Underlined = n.Style["text-decoration"] == "underline"
412 self.Text.LineThrough = n.Style["text-decoration"] == "linethrough"
413 self.Text.EM = int(self.EM)
414 self.Text.Width = int(parent.Width)
415 self.Text.Text = n.InnerText
416
417 if n.Style["word-spacing"] == "" {
418 self.Text.WordSpacing = font.MeasureSpace(&self.Text)
419 }
420 if parent.Width != 0 && n.Style["display"] != "inline" && n.Style["width"] == "" {
421 *width = (parent.Width - p.Right) - p.Left
422 } else if n.Style["width"] == "" {
423 *width = utils.Max(*width, float32(font.MeasureLongest(&self)))
424 } else if n.Style["width"] != "" {
425 *width, _ = utils.ConvertToPixels(n.Style["width"], self.EM, parent.Width)
426 }
427
428 self.Text.Width = int(*width)
429 self.Width = *width
430 h := font.Render(&self)
431 if n.Style["height"] == "" {
432 *height = h
433 }
434
435 return self
436
437}