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 // !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}