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, *map[string]element.State)
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, state *map[string]element.State) {
16 s := *state
17 self := s[n.Properties.Id]
18 parent := s[n.Parent.Properties.Id]
19
20 // If the element is display block and the width is unset then make it 100%
21
22 if n.Style["width"] == "" {
23 self.Width, _ = utils.ConvertToPixels("100%", self.EM, parent.Width)
24 }
25 m := utils.GetMP(*n, "margin")
26 self.Width -= (m.Right + m.Left)
27 self.Height -= (m.Top + m.Bottom)
28
29 p := utils.GetMP(*n, "padding")
30 self.Width += (p.Right + p.Left)
31 self.Height += (p.Top + p.Bottom)
32
33 (*state)[n.Properties.Id] = self
34 },
35 }
36}
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 // !FLAG: why is this needed, the "attribute" is n.Style that should be mapped during init
105 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
106 styles = utils.Merge(styles, inline)
107 // add hover and focus css events
108
109 return styles
110}
111
112func (c *CSS) AddPlugin(plugin Plugin) {
113 c.Plugins = append(c.Plugins, plugin)
114}
115
116func CheckNode(n *element.Node, state *map[string]element.State) {
117 s := *state
118 self := s[n.Properties.Id]
119
120 fmt.Println(n.TagName, n.Properties.Id)
121 fmt.Printf("ID: %v\n", n.Id)
122 fmt.Printf("Parent: %v\n", n.Parent.TagName)
123 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
124 fmt.Printf("Text: %v\n", self.Text.Text)
125 fmt.Printf("X: %v, Y: %v\n", self.X, self.Y)
126 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
127 fmt.Printf("Styles: %v\n", n.Style)
128 fmt.Printf("Background: %v\n", self.Background)
129 fmt.Printf("Border: %v\n\n\n", self.Border)
130}
131
132func (c *CSS) ComputeNodeStyle(n *element.Node, state *map[string]element.State) *element.Node {
133 // Head is not renderable
134 if utils.IsParent(*n, "head") {
135 return n
136 }
137 plugins := c.Plugins
138 // !FLAG: This should add to state.Style instead as the element.Node should be un effected by the engine
139 // currently this adds styles to the style attribute that the use did not explisitly set
140
141 n.Style = c.GetStyles(*n)
142 s := *state
143 self := s[n.Properties.Id]
144 parent := s[n.Parent.Properties.Id]
145
146 self.Background = color.Parse(n.Style, "background")
147 self.Border, _ = CompleteBorder(n.Style)
148
149 fs, _ := utils.ConvertToPixels(n.Style["font-size"], parent.EM, parent.Width)
150 self.EM = fs
151
152 if n.Style["display"] == "none" {
153 self.X = 0
154 self.Y = 0
155 self.Width = 0
156 self.Height = 0
157 return n
158 }
159
160 wh := utils.GetWH(*n)
161 width := wh.Width
162 height := wh.Height
163
164 x, y := parent.X, parent.Y
165
166 var top, left, right, bottom bool = false, false, false, false
167
168 m := utils.GetMP(*n, "margin")
169 p := utils.GetMP(*n, "padding")
170
171 self.Margin = m
172 self.Padding = p
173
174 if n.Style["position"] == "absolute" {
175 bas := utils.GetPositionOffsetNode(n)
176 base := s[bas.Properties.Id]
177 if n.Style["top"] != "" {
178 v, _ := utils.ConvertToPixels(n.Style["top"], self.EM, parent.Width)
179 y = v + base.Y
180 top = true
181 }
182 if n.Style["left"] != "" {
183 v, _ := utils.ConvertToPixels(n.Style["left"], self.EM, parent.Width)
184 x = v + base.X
185 left = true
186 }
187 if n.Style["right"] != "" {
188 v, _ := utils.ConvertToPixels(n.Style["right"], self.EM, parent.Width)
189 x = (base.Width - width) - v
190 right = true
191 }
192 if n.Style["bottom"] != "" {
193 v, _ := utils.ConvertToPixels(n.Style["bottom"], self.EM, parent.Width)
194 y = (base.Height - height) - v
195 bottom = true
196 }
197 } else {
198 for i, v := range n.Parent.Children {
199 if v.Properties.Id == n.Properties.Id {
200 if i-1 > 0 {
201 sib := n.Parent.Children[i-1]
202 sibling := s[sib.Properties.Id]
203 if n.Style["display"] == "inline" {
204 if sib.Style["display"] == "inline" {
205 y = sibling.Y
206 } else {
207 y = sibling.Y + sibling.Height
208 }
209 } else {
210 y = sibling.Y + sibling.Height
211 }
212 }
213 break
214 } else if n.Style["display"] != "inline" {
215 vState := s[v.Properties.Id]
216 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height
217 }
218 }
219 }
220
221 // Display modes need to be calculated here
222
223 relPos := !top && !left && !right && !bottom
224
225 if left || relPos {
226 x += m.Left
227 }
228 if top || relPos {
229 y += m.Top
230 }
231 if right {
232 x -= m.Right
233 }
234 if bottom {
235 y -= m.Bottom
236 }
237
238 // fmt.Println(n.InnerText, len(n.Children))
239
240 if !utils.ChildrenHaveText(n) {
241 // Confirm text exists
242 if len(n.InnerText) > 0 {
243 innerWidth := width
244 innerHeight := height
245 (*state)[n.Properties.Id] = self
246 self = genTextNode(n, &innerWidth, &innerHeight, p, state)
247 width = innerWidth + p.Left + p.Right
248 height = innerHeight
249 }
250 }
251
252 self.X = x
253 self.Y = y
254 self.Width = width
255 self.Height = height
256
257 (*state)[n.Properties.Id] = self
258 (*state)[n.Parent.Properties.Id] = parent
259
260 // CheckNode(n, state)
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 if n.Style["height"] == "" {
269 cState := s[n.Children[i].Properties.Id]
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 }
280 }
281
282 (*state)[n.Properties.Id] = self
283
284 // Sorting the array by the Level field
285 sort.Slice(plugins, func(i, j int) bool {
286 return plugins[i].Level < plugins[j].Level
287 })
288
289 for _, v := range plugins {
290 matches := true
291 for name, value := range v.Styles {
292 if n.Style[name] != value && !(value == "*") {
293 matches = false
294 }
295 }
296 if matches {
297 v.Handler(n, state)
298 }
299 }
300
301 return n
302}
303
304func parseBorderShorthand(borderShorthand string) (element.Border, error) {
305 // Split the shorthand into components
306 borderComponents := strings.Fields(borderShorthand)
307
308 // Ensure there are at least 1 component (width or style or color)
309 if len(borderComponents) >= 1 {
310 width := "0px" // Default width
311 style := "solid"
312 borderColor := "#000000" // Default color
313
314 // Extract style and color if available
315 if len(borderComponents) >= 1 {
316 width = borderComponents[0]
317 }
318
319 // Extract style and color if available
320 if len(borderComponents) >= 2 {
321 style = borderComponents[1]
322 }
323 if len(borderComponents) >= 3 {
324 borderColor = borderComponents[2]
325 }
326
327 parsedColor, _ := color.Color(borderColor)
328
329 return element.Border{
330 Width: width,
331 Style: style,
332 Color: parsedColor,
333 Radius: "", // Default radius
334 }, nil
335 }
336
337 return element.Border{}, fmt.Errorf("invalid border shorthand format")
338}
339
340func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
341 border, err := parseBorderShorthand(cssProperties["border"])
342 border.Radius = cssProperties["border-radius"]
343
344 return border, err
345}
346
347func genTextNode(n *element.Node, width, height *float32, p element.MarginPadding, state *map[string]element.State) element.State {
348 s := *state
349 self := s[n.Properties.Id]
350 parent := s[n.Parent.Properties.Id]
351
352 bold, italic := false, false
353
354 if n.Style["font-weight"] == "bold" {
355 bold = true
356 }
357
358 if n.Style["font-style"] == "italic" {
359 italic = true
360 }
361
362 if self.Text.Font == nil {
363 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
364 self.Text.Font = f
365 }
366
367 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, *width)
368 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, *width)
369 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], self.EM, *width)
370 if lineHeight == 0 {
371 lineHeight = self.EM + 3
372 }
373
374 self.Text.LineHeight = int(lineHeight)
375 self.Text.WordSpacing = int(wordSpacing)
376 self.Text.LetterSpacing = int(letterSpacing)
377 wb := " "
378
379 if n.Style["word-wrap"] == "break-word" {
380 wb = ""
381 }
382
383 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
384 wb = ""
385 }
386
387 var dt float32
388
389 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
390 dt = 3
391 } else {
392 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, *width)
393 }
394
395 col := color.Parse(n.Style, "font")
396
397 self.Text.Color = col
398 self.Text.DecorationColor = color.Parse(n.Style, "decoration")
399 self.Text.Align = n.Style["text-align"]
400 self.Text.WordBreak = wb
401 self.Text.WordSpacing = int(wordSpacing)
402 self.Text.LetterSpacing = int(letterSpacing)
403 self.Text.WhiteSpace = n.Style["white-space"]
404 self.Text.DecorationThickness = int(dt)
405 self.Text.Overlined = n.Style["text-decoration"] == "overline"
406 self.Text.Underlined = n.Style["text-decoration"] == "underline"
407 self.Text.LineThrough = n.Style["text-decoration"] == "linethrough"
408 self.Text.EM = int(self.EM)
409 self.Text.Width = int(parent.Width)
410 self.Text.Text = n.InnerText
411
412 if n.Style["word-spacing"] == "" {
413 self.Text.WordSpacing = font.MeasureSpace(&self.Text)
414 }
415 if parent.Width != 0 && n.Style["display"] != "inline" && n.Style["width"] == "" {
416 *width = (parent.Width - p.Right) - p.Left
417 } else if n.Style["width"] == "" {
418 *width = utils.Max(*width, float32(font.MeasureLongest(&self)))
419 } else if n.Style["width"] != "" {
420 *width, _ = utils.ConvertToPixels(n.Style["width"], self.EM, parent.Width)
421 }
422
423 self.Text.Width = int(*width)
424 self.Width = *width
425 fmt.Println(n.TagName, n.Style["width"], *width)
426 h := font.Render(&self)
427 if n.Style["height"] == "" {
428 *height = h
429 }
430
431 return self
432
433}