CStyle Plugins
Plugins add the ability to choose what parts of the HTML/CSS spec you add to your application. If you are trying to keep compile sizes small you can remove as many as you need to reach your target size. Here we will go over the basics of how they work and how to use them.
1type Plugin struct {
2 Styles map[string]string
3 Level int
4 Handler func(*element.Node)
5}
A plugin is a struct defined in CStyle, in contains three properties:
- Styles
- A map with CSS properties and values that the plugin should match on. There is also support for wildcard properties by setting the value to "*"
- Level
- Level of priority in which to execute
- All library level should be between 0 and 2
- All others should be greater than 2
- Handler
- Callback function that provides a pointer to a element that matches the properties of Styles
# AddPlugin?(go)
Add Plugin is the CStyle function that add the plugin to the top level cstyle.Plugins array where it is used within the ComputeNodeStyle
function.
# Usage
1css.AddPlugin(block.Init())
The first step in processing the plugins before running there handler functions is to sort them by their levels. We need to sort the plugins by their level because in the example of flex, it relys on a parent element being block position so it can compute styles based of the positioning of its parent elements. If flex was ran before block then it would have nothing to build apon. This is also the reason that if you are building a custom plugin it is reccomended to keep the level above 2 as anything after 2 will have the assumed styles to build apon.
// Sorting the array by the Level field sort.Slice(plugins, func(i, j int) bool { return plugins[i].Level < plugins[j].Level })
After we have the sorted plugins we can check if the current element matches the Styles
of the plugin. The matching is a all of nothing matching system, if one property is missing then the plugin wil not be ran. If it does match then a pointer to the element.Node
(n) is passed to the handler.
for _, v := range plugins { matches := true for name, value := range v.Styles { if styleMap[name] != value && !(value == "*") { matches = false } } if matches { v.Handler(n) } }
# plugins/block
Here is the code for the block styling plugin, it is recommended to keep this standard format.
All other plugins can be found in the cstyle/plugins folder
1package block
2
3import (
4 "gui/cstyle"
5 "gui/element"
6 "gui/utils"
7)
8
9func Init() cstyle.Plugin {
10 return cstyle.Plugin{
11 Styles: map[string]string{
12 "display": "block",
13 },
14 Level: 1,
15 Handler: func(n *element.Node) {
16 // If the element is display block and the width is unset then make it 100%
17
18 if n.Style["width"] == "" {
19 n.Properties.Computed["width"], _ = utils.ConvertToPixels("100%", n.Properties.EM, n.Parent.Properties.Computed["width"])
20 m := utils.GetMP(*n, "margin")
21 n.Properties.Computed["width"] -= m.Right + m.Left
22 } else {
23 p := utils.GetMP(*n, "padding")
24 n.Properties.Computed["width"] += p.Right + p.Left
25 }
26 },
27 }
28}
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)
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
69// need to get rid of the .props for the most part all styles should be computed dynamically
70// can keep like focusable and stuff that describes the element
71
72// currently the append child does not work due to the props and other stuff not existing so it fails
73// moving to a real time style compute would fix that
74
75// :hover is parsed correctly but because the hash func doesn't invalidate it becuase the val
76// is updated in the props. change to append :hover to style to create the effect
77// or merge the class with the styles? idk have to think more
78
79func (c *CSS) GetStyles(n element.Node) map[string]string {
80 styles := map[string]string{}
81
82 if n.Parent != nil {
83 ps := c.GetStyles(*n.Parent)
84 for _, v := range inheritedProps {
85 if ps[v] != "" {
86 styles[v] = ps[v]
87 }
88 }
89 }
90 for k, v := range n.Style {
91 styles[k] = v
92 }
93 hovered := false
94 if slices.Contains(n.ClassList.Classes, ":hover") {
95 hovered = true
96 }
97
98 for _, styleSheet := range c.StyleSheets {
99 for selector := range styleSheet {
100 // fmt.Println(selector, n.Properties.Id)
101 key := selector
102 if strings.Contains(selector, ":hover") && hovered {
103 selector = strings.Replace(selector, ":hover", "", -1)
104 }
105 if element.TestSelector(selector, &n) {
106 for k, v := range styleSheet[key] {
107 styles[k] = v
108 }
109 }
110
111 }
112 }
113 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
114 styles = utils.Merge(styles, inline)
115 // add hover and focus css events
116
117 return styles
118}
119
120func (c *CSS) Render(doc element.Node) []element.Node {
121 return flatten(doc)
122}
123
124func (c *CSS) AddPlugin(plugin Plugin) {
125 c.Plugins = append(c.Plugins, plugin)
126}
127
128func CheckNode(n *element.Node) {
129 fmt.Println(n.TagName, n.Properties.Id)
130 fmt.Printf("ID: %v\n", n.Id)
131 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
132 fmt.Printf("Text: %v\n", n.InnerText)
133 fmt.Printf("X: %v, Y: %v\n", n.Properties.X, n.Properties.Y)
134 fmt.Printf("Width: %v, Height: %v\n", n.Properties.Computed["width"], n.Properties.Computed["height"])
135 fmt.Printf("Styles: %v\n\n\n", n.Style)
136 w := utils.GetWH(*n)
137 fmt.Printf("Calc WH: %v, %v\n\n\n", w.Width, w.Height)
138}
139
140func (c *CSS) ComputeNodeStyle(n *element.Node) *element.Node {
141 plugins := c.Plugins
142 n.Style = c.GetStyles(*n)
143
144 if n.Style["display"] == "none" {
145 n.Properties.X = 0
146 n.Properties.Y = 0
147 n.Properties.Computed["width"] = 0
148 n.Properties.Computed["height"] = 0
149 return n
150 }
151
152 width, height := n.Properties.Computed["width"], n.Properties.Computed["height"]
153
154 x, y := n.Parent.Properties.X, n.Parent.Properties.Y
155
156 var top, left, right, bottom bool = false, false, false, false
157
158 m := utils.GetMP(*n, "margin")
159 p := utils.GetMP(*n, "padding")
160
161 if n.Style["position"] == "absolute" {
162 base := utils.GetPositionOffsetNode(n)
163 if n.Style["top"] != "" {
164 v, _ := utils.ConvertToPixels(n.Style["top"], float32(n.Properties.EM), n.Parent.Properties.Computed["width"])
165 y = v + base.Properties.Y
166 top = true
167 }
168 if n.Style["left"] != "" {
169 v, _ := utils.ConvertToPixels(n.Style["left"], float32(n.Properties.EM), n.Parent.Properties.Computed["width"])
170 x = v + base.Properties.X
171 left = true
172 }
173 if n.Style["right"] != "" {
174 v, _ := utils.ConvertToPixels(n.Style["right"], float32(n.Properties.EM), n.Parent.Properties.Computed["width"])
175 x = (base.Properties.Computed["width"] - width) - v
176 right = true
177 }
178 if n.Style["bottom"] != "" {
179 v, _ := utils.ConvertToPixels(n.Style["bottom"], float32(n.Properties.EM), n.Parent.Properties.Computed["width"])
180 y = (base.Properties.Computed["height"] - height) - v
181 bottom = true
182 }
183 } else {
184 for i, v := range n.Parent.Children {
185 if v.Properties.Id == n.Properties.Id {
186 if i-1 > 0 {
187 sibling := n.Parent.Children[i-1]
188 if n.Style["display"] == "inline" {
189 if sibling.Style["display"] == "inline" {
190 y = sibling.Properties.Y
191 } else {
192 y = sibling.Properties.Y + sibling.Properties.Computed["height"]
193 }
194 } else {
195 y = sibling.Properties.Y + sibling.Properties.Computed["height"]
196 }
197 }
198 break
199 } else if n.Style["display"] != "inline" {
200 mc := utils.GetMP(v, "margin")
201 pc := utils.GetMP(v, "padding")
202 y += mc.Top + mc.Bottom + pc.Top + pc.Bottom + v.Properties.Computed["height"]
203 }
204 }
205 }
206
207 // Display modes need to be calculated here
208
209 relPos := !top && !left && !right && !bottom
210
211 if left || relPos {
212 x += m.Left
213 }
214 if top || relPos {
215 y += m.Top
216 }
217 if right {
218 x -= m.Right
219 }
220 if bottom {
221 y -= m.Bottom
222 }
223
224 bold, italic := false, false
225
226 if n.Style["font-weight"] == "bold" {
227 bold = true
228 }
229
230 if n.Style["font-style"] == "italic" {
231 italic = true
232 }
233
234 if n.Properties.Text.Font == nil {
235 f, _ := font.LoadFont(n.Style["font-family"], int(n.Properties.EM), bold, italic)
236 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], n.Properties.EM, width)
237 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], n.Properties.EM, width)
238 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], n.Properties.EM, width)
239 if lineHeight == 0 {
240 lineHeight = n.Properties.EM + 3
241 }
242
243 n.Properties.Text.LineHeight = int(lineHeight)
244 n.Properties.Text.Font = f
245 n.Properties.Text.WordSpacing = int(wordSpacing)
246 n.Properties.Text.LetterSpacing = int(letterSpacing)
247 }
248
249 if len(n.Children) == 0 {
250 // Confirm text exists
251 if len(n.InnerText) > 0 && !utils.IsParent(*n, "head") {
252 innerWidth := width
253 innerHeight := height
254 genTextNode(n, &innerWidth, &innerHeight, p)
255 width = innerWidth + p.Left + p.Right
256 height = innerHeight
257 }
258 }
259
260 n.Properties.X = x
261 n.Properties.Y = y
262 n.Properties.Computed["width"] = width
263 n.Properties.Computed["height"] = height
264
265 CheckNode(n)
266
267 // Call children here
268
269 var childYOffset float32
270 for i, v := range n.Children {
271 v.Parent = n
272 n.Children[i] = *c.ComputeNodeStyle(&v)
273 if n.Style["height"] == "" {
274 if n.Children[i].Style["position"] != "absolute" && n.Children[i].Properties.Y > childYOffset {
275 childYOffset = n.Children[i].Properties.Y
276 m := utils.GetMP(n.Children[i], "margin")
277 p := utils.GetMP(n.Children[i], "padding")
278 n.Properties.Computed["height"] += n.Children[i].Properties.Computed["height"]
279 n.Properties.Computed["height"] += m.Top
280 n.Properties.Computed["height"] += m.Bottom
281 n.Properties.Computed["height"] += p.Top
282 n.Properties.Computed["height"] += p.Bottom
283 }
284
285 }
286 }
287
288 // Sorting the array by the Level field
289 sort.Slice(plugins, func(i, j int) bool {
290 return plugins[i].Level < plugins[j].Level
291 })
292
293 for _, v := range plugins {
294 matches := true
295 for name, value := range v.Styles {
296 if n.Style[name] != value && !(value == "*") {
297 matches = false
298 }
299 }
300 if matches {
301 v.Handler(n)
302 }
303 }
304
305 return n
306}
307
308func InitNode(n *element.Node, c CSS) *element.Node {
309 n.Style = c.GetStyles(*n)
310 border, err := CompleteBorder(n.Style)
311 if err == nil {
312 n.Properties.Border = border
313 }
314
315 fs, _ := utils.ConvertToPixels(n.Style["font-size"], n.Parent.Properties.EM, n.Parent.Properties.Computed["width"])
316 n.Properties.EM = fs
317
318 width, _ := utils.ConvertToPixels(n.Style["width"], n.Properties.EM, n.Parent.Properties.Computed["width"])
319 if n.Style["min-width"] != "" {
320 minWidth, _ := utils.ConvertToPixels(n.Style["min-width"], n.Properties.EM, n.Parent.Properties.Computed["width"])
321 width = utils.Max(width, minWidth)
322 }
323
324 if n.Style["max-width"] != "" {
325 maxWidth, _ := utils.ConvertToPixels(n.Style["max-width"], n.Properties.EM, n.Parent.Properties.Computed["width"])
326 width = utils.Min(width, maxWidth)
327 }
328
329 height, _ := utils.ConvertToPixels(n.Style["height"], n.Properties.EM, n.Parent.Properties.Computed["height"])
330 if n.Style["min-height"] != "" {
331 minHeight, _ := utils.ConvertToPixels(n.Style["min-height"], n.Properties.EM, n.Parent.Properties.Computed["height"])
332 height = utils.Max(height, minHeight)
333 }
334
335 if n.Style["max-height"] != "" {
336 maxHeight, _ := utils.ConvertToPixels(n.Style["max-height"], n.Properties.EM, n.Parent.Properties.Computed["height"])
337 height = utils.Min(height, maxHeight)
338 }
339
340 n.Properties.Computed["width"] = width
341 n.Properties.Computed["height"] = height
342
343 bold, italic := false, false
344
345 if n.Style["font-weight"] == "bold" {
346 bold = true
347 }
348
349 if n.Style["font-style"] == "italic" {
350 italic = true
351 }
352
353 f, _ := font.LoadFont(n.Style["font-family"], int(n.Properties.EM), bold, italic)
354 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], n.Properties.EM, width)
355 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], n.Properties.EM, width)
356 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], n.Properties.EM, width)
357 if lineHeight == 0 {
358 lineHeight = n.Properties.EM + 3
359 }
360
361 n.Properties.Text.LineHeight = int(lineHeight)
362 n.Properties.Text.Font = f
363 n.Properties.Text.WordSpacing = int(wordSpacing)
364 n.Properties.Text.LetterSpacing = int(letterSpacing)
365 return n
366}
367
368func parseBorderShorthand(borderShorthand string) (element.Border, error) {
369 // Split the shorthand into components
370 borderComponents := strings.Fields(borderShorthand)
371
372 // Ensure there are at least 1 component (width or style or color)
373 if len(borderComponents) >= 1 {
374 width := "0px" // Default width
375 style := "solid"
376 borderColor := "#000000" // Default color
377
378 // Extract style and color if available
379 if len(borderComponents) >= 1 {
380 width = borderComponents[0]
381 }
382
383 // Extract style and color if available
384 if len(borderComponents) >= 2 {
385 style = borderComponents[1]
386 }
387 if len(borderComponents) >= 3 {
388 borderColor = borderComponents[2]
389 }
390
391 parsedColor, _ := color.Color(borderColor)
392
393 return element.Border{
394 Width: width,
395 Style: style,
396 Color: parsedColor,
397 Radius: "", // Default radius
398 }, nil
399 }
400
401 return element.Border{}, fmt.Errorf("invalid border shorthand format")
402}
403
404func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
405 border, err := parseBorderShorthand(cssProperties["border"])
406 border.Radius = cssProperties["border-radius"]
407
408 return border, err
409}
410
411func flatten(n element.Node) []element.Node {
412 var nodes []element.Node
413 nodes = append(nodes, n)
414
415 children := n.Children
416 if len(children) > 0 {
417 for _, ch := range children {
418 chNodes := flatten(ch)
419 nodes = append(nodes, chNodes...)
420 }
421 }
422 return nodes
423}
424
425func genTextNode(n *element.Node, width, height *float32, p utils.MarginPadding) {
426 wb := " "
427
428 if n.Style["word-wrap"] == "break-word" {
429 wb = ""
430 }
431
432 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
433 wb = ""
434 }
435
436 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], n.Properties.EM, *width)
437 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], n.Properties.EM, *width)
438
439 var dt float32
440
441 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
442 dt = 2
443 } else {
444 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], n.Properties.EM, *width)
445 }
446
447 col := color.Parse(n.Style, "font")
448
449 n.Properties.Text.Color = col
450 n.Properties.Text.Align = n.Style["text-align"]
451 n.Properties.Text.WordBreak = wb
452 n.Properties.Text.WordSpacing = int(wordSpacing)
453 n.Properties.Text.LetterSpacing = int(letterSpacing)
454 n.Properties.Text.WhiteSpace = n.Style["white-space"]
455 n.Properties.Text.DecorationThickness = int(dt)
456 n.Properties.Text.Overlined = n.Style["text-decoration"] == "overline"
457 n.Properties.Text.Underlined = n.Style["text-decoration"] == "underline"
458 n.Properties.Text.LineThrough = n.Style["text-decoration"] == "linethrough"
459 n.Properties.Text.EM = int(n.Properties.EM)
460 n.Properties.Text.Width = int(n.Parent.Properties.Computed["width"])
461
462 if n.Style["word-spacing"] == "" {
463 n.Properties.Text.WordSpacing = font.MeasureSpace(&n.Properties.Text)
464 }
465 if n.Parent.Properties.Computed["width"] != 0 && n.Style["display"] != "inline" && n.Style["width"] == "" {
466 *width = (n.Parent.Properties.Computed["width"] - p.Right) - p.Left
467 } else if n.Style["width"] == "" {
468 *width = utils.Max(*width, float32(font.MeasureLongest(n)))
469 } else if n.Style["width"] != "" {
470 *width, _ = utils.ConvertToPixels(n.Style["width"], n.Properties.EM, n.Parent.Properties.Computed["width"])
471 }
472
473 n.Properties.Text.Width = int(*width)
474 h := font.Render(n)
475 if n.Style["height"] == "" {
476 *height = h
477 }
478
479}