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 words := strings.Split(n.InnerText, " ")
246 if len(words) != 1 && !strings.Contains(n.Properties.Id, "copy") && n.Style["display"] == "inline" {
247 n.InnerText = words[0]
248 n.Properties.Id = "copy" + n.Properties.Id
249 // slices.Reverse(words)
250 // !ISSUE: I thinkthis is a good route to take, might make a style prop for text that a plugins can be used to turn the groups
251 // + into psuedo elements but make them inline
252 // + that would take care of wrapping and it would also work for display block
253 for _, v := range words {
254 if len(strings.TrimSpace(v)) > 0 {
255 el := *n
256 el.InnerText = v
257 for k, val := range n.Style {
258 el.Style[k] = val
259 }
260 n.Parent.InsertAfter(el, *n)
261 }
262 }
263 // !ISSUE: can probally use flex to do it need to group text so it doesn't include block element
264 // + if there is a block element break then go on
265 // fmt.Println(n.Parent.Children)
266 }
267 innerWidth := width
268 innerHeight := height
269 (*state)[n.Properties.Id] = self
270 // !ISSUE: change genTextNode to basically a image generator for a word.
271 // + make it so this api can be used to also do images
272 self = genTextNode(n, &innerWidth, &innerHeight, p, state)
273 width = innerWidth + p.Left + p.Right
274 height = innerHeight
275 }
276 }
277
278 // if !utils.ChildrenHaveText(n) {
279 // // Confirm text exists
280 // if len(n.InnerText) > 0 {
281 // innerWidth := width
282 // innerHeight := height
283 // (*state)[n.Properties.Id] = self
284 // self = genTextNode(n, &innerWidth, &innerHeight, p, state)
285 // width = innerWidth + p.Left + p.Right
286 // height = innerHeight
287 // }
288 // }
289
290 self.X = x
291 self.Y = y
292 self.Width = width
293 self.Height = height
294
295 (*state)[n.Properties.Id] = self
296 (*state)[n.Parent.Properties.Id] = parent
297
298 // Call children here
299
300 var childYOffset float32
301 for i, v := range n.Children {
302 v.Parent = n
303 n.Children[i] = *c.ComputeNodeStyle(&v, state)
304 cState := (*state)[n.Children[i].Properties.Id]
305 if n.Style["height"] == "" {
306 if n.Children[i].Style["position"] != "absolute" && cState.Y > childYOffset {
307 childYOffset = cState.Y
308 self.Height += cState.Height
309 self.Height += cState.Margin.Top
310 self.Height += cState.Margin.Bottom
311 self.Height += cState.Padding.Top
312 self.Height += cState.Padding.Bottom
313 }
314 }
315 // fmt.Println(n.TagName, self.Width, v.TagName, cState.Width)
316 if cState.Width > self.Width {
317 self.Width = cState.Width
318 }
319 }
320
321 (*state)[n.Properties.Id] = self
322
323 // Sorting the array by the Level field
324 sort.Slice(plugins, func(i, j int) bool {
325 return plugins[i].Level < plugins[j].Level
326 })
327
328 for _, v := range plugins {
329 matches := true
330 for name, value := range v.Styles {
331 if n.Style[name] != value && !(value == "*") {
332 matches = false
333 }
334 }
335 if matches {
336 v.Handler(n, state)
337 }
338 }
339
340 CheckNode(n, state)
341
342 return n
343}
344
345func parseBorderShorthand(borderShorthand string) (element.Border, error) {
346 // Split the shorthand into components
347 borderComponents := strings.Fields(borderShorthand)
348
349 // Ensure there are at least 1 component (width or style or color)
350 if len(borderComponents) >= 1 {
351 width := "0px" // Default width
352 style := "solid"
353 borderColor := "#000000" // Default color
354
355 // Extract style and color if available
356 if len(borderComponents) >= 1 {
357 width = borderComponents[0]
358 }
359
360 // Extract style and color if available
361 if len(borderComponents) >= 2 {
362 style = borderComponents[1]
363 }
364 if len(borderComponents) >= 3 {
365 borderColor = borderComponents[2]
366 }
367
368 parsedColor, _ := color.Color(borderColor)
369
370 return element.Border{
371 Width: width,
372 Style: style,
373 Color: parsedColor,
374 Radius: "", // Default radius
375 }, nil
376 }
377
378 return element.Border{}, fmt.Errorf("invalid border shorthand format")
379}
380
381func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
382 border, err := parseBorderShorthand(cssProperties["border"])
383 border.Radius = cssProperties["border-radius"]
384
385 return border, err
386}
387
388func genTextNode(n *element.Node, width, height *float32, p element.MarginPadding, state *map[string]element.State) element.State {
389 s := *state
390 self := s[n.Properties.Id]
391 parent := s[n.Parent.Properties.Id]
392
393 bold, italic := false, false
394
395 if n.Style["font-weight"] == "bold" {
396 bold = true
397 }
398
399 if n.Style["font-style"] == "italic" {
400 italic = true
401 }
402
403 if self.Text.Font == nil {
404 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
405 self.Text.Font = f
406 }
407
408 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, *width)
409 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, *width)
410 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], self.EM, *width)
411 if lineHeight == 0 {
412 lineHeight = self.EM + 3
413 }
414
415 self.Text.LineHeight = int(lineHeight)
416 self.Text.WordSpacing = int(wordSpacing)
417 self.Text.LetterSpacing = int(letterSpacing)
418 wb := " "
419
420 if n.Style["word-wrap"] == "break-word" {
421 wb = ""
422 }
423
424 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
425 wb = ""
426 }
427
428 var dt float32
429
430 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
431 dt = 3
432 } else {
433 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, *width)
434 }
435
436 col := color.Parse(n.Style, "font")
437
438 self.Text.Color = col
439 self.Text.DecorationColor = color.Parse(n.Style, "decoration")
440 self.Text.Align = n.Style["text-align"]
441 self.Text.WordBreak = wb
442 self.Text.WordSpacing = int(wordSpacing)
443 self.Text.LetterSpacing = int(letterSpacing)
444 self.Text.WhiteSpace = n.Style["white-space"]
445 self.Text.DecorationThickness = int(dt)
446 self.Text.Overlined = n.Style["text-decoration"] == "overline"
447 self.Text.Underlined = n.Style["text-decoration"] == "underline"
448 self.Text.LineThrough = n.Style["text-decoration"] == "linethrough"
449 self.Text.EM = int(self.EM)
450 self.Text.Width = int(parent.Width)
451 self.Text.Text = n.InnerText
452
453 if n.Style["word-spacing"] == "" {
454 self.Text.WordSpacing = font.MeasureSpace(&self.Text)
455 }
456 if parent.Width != 0 && n.Style["display"] != "inline" && n.Style["width"] == "" {
457 *width = (parent.Width - p.Right) - p.Left
458 } else if n.Style["width"] == "" {
459 *width = utils.Max(*width, float32(font.MeasureLongest(&self)))
460 } else if n.Style["width"] != "" {
461 *width, _ = utils.ConvertToPixels(n.Style["width"], self.EM, parent.Width)
462 }
463
464 self.Text.Width = int(*width)
465 self.Width = *width
466 h := font.Render(&self)
467 if n.Style["height"] == "" {
468 *height = h
469 }
470
471 return self
472
473}