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", 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 self.X = x
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 n.InnerText = words[0]
254 el := *n
255 el.InnerText = strings.Join(words[1:], " ")
256 n.Parent.InsertAfter(el, *n)
257 }
258 // !ISSUE: change genTextNode to basically a image generator for a word.
259 // + make it so this api can be used to also do images
260 // + element.State.Texture?
261 }
262 if len(strings.TrimSpace(n.InnerText)) > 0 {
263 n.InnerText = strings.TrimSpace(n.InnerText)
264 self = genTextNode(n, state)
265 }
266 }
267
268 (*state)[n.Properties.Id] = self
269 (*state)[n.Parent.Properties.Id] = parent
270
271 // Call children here
272
273 var childYOffset float32
274 for i := 0; i < len(n.Children); i++ {
275 v := n.Children[i]
276 v.Parent = n
277 n.Children[i] = *c.ComputeNodeStyle(&v, state)
278 cState := (*state)[n.Children[i].Properties.Id]
279 if n.Style["height"] == "" {
280 if n.Children[i].Style["position"] != "absolute" && cState.Y > childYOffset {
281 childYOffset = cState.Y
282 self.Height += cState.Height
283 self.Height += cState.Margin.Top
284 self.Height += cState.Margin.Bottom
285 self.Height += cState.Padding.Top
286 self.Height += cState.Padding.Bottom
287 }
288 }
289 // fmt.Println(n.TagName, self.Width, v.TagName, cState.Width)
290 if cState.Width > self.Width {
291 self.Width = cState.Width
292 }
293 }
294
295 (*state)[n.Properties.Id] = self
296
297 // Sorting the array by the Level field
298 sort.Slice(plugins, func(i, j int) bool {
299 return plugins[i].Level < plugins[j].Level
300 })
301
302 for _, v := range plugins {
303 matches := true
304 for name, value := range v.Styles {
305 if n.Style[name] != value && !(value == "*") {
306 matches = false
307 }
308 }
309 if matches {
310 v.Handler(n, state)
311 }
312 }
313
314 for i := range n.Children {
315 cState := (*state)[n.Children[i].Properties.Id]
316 cState.Y += self.Padding.Top
317 (*state)[n.Children[i].Properties.Id] = cState
318 }
319
320 // CheckNode(n, state)
321
322 return n
323}
324
325func parseBorderShorthand(borderShorthand string) (element.Border, error) {
326 // Split the shorthand into components
327 borderComponents := strings.Fields(borderShorthand)
328
329 // Ensure there are at least 1 component (width or style or color)
330 if len(borderComponents) >= 1 {
331 width := "0px" // Default width
332 style := "solid"
333 borderColor := "#000000" // Default color
334
335 // Extract style and color if available
336 if len(borderComponents) >= 1 {
337 width = borderComponents[0]
338 }
339
340 // Extract style and color if available
341 if len(borderComponents) >= 2 {
342 style = borderComponents[1]
343 }
344 if len(borderComponents) >= 3 {
345 borderColor = borderComponents[2]
346 }
347
348 parsedColor, _ := color.Color(borderColor)
349
350 return element.Border{
351 Width: width,
352 Style: style,
353 Color: parsedColor,
354 Radius: "", // Default radius
355 }, nil
356 }
357
358 return element.Border{}, fmt.Errorf("invalid border shorthand format")
359}
360
361func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
362 border, err := parseBorderShorthand(cssProperties["border"])
363 border.Radius = cssProperties["border-radius"]
364
365 return border, err
366}
367
368func genTextNode(n *element.Node, state *map[string]element.State) element.State {
369 s := *state
370 self := s[n.Properties.Id]
371 parent := s[n.Parent.Properties.Id]
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 self.Text.Font == nil {
384 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
385 self.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 self.Text.LineHeight = int(lineHeight)
396 self.Text.WordSpacing = int(wordSpacing)
397 self.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.Text.Color = col
419 self.Text.DecorationColor = color.Parse(n.Style, "decoration")
420 self.Text.Align = n.Style["text-align"]
421 self.Text.WordBreak = wb
422 self.Text.WordSpacing = int(wordSpacing)
423 self.Text.LetterSpacing = int(letterSpacing)
424 self.Text.WhiteSpace = n.Style["white-space"]
425 self.Text.DecorationThickness = int(dt)
426 self.Text.Overlined = n.Style["text-decoration"] == "overline"
427 self.Text.Underlined = n.Style["text-decoration"] == "underline"
428 self.Text.LineThrough = n.Style["text-decoration"] == "linethrough"
429 self.Text.EM = int(self.EM)
430 self.Text.Width = int(parent.Width)
431 self.Text.Text = n.InnerText
432
433 if n.Style["word-spacing"] == "" {
434 self.Text.WordSpacing = font.MeasureSpace(&self.Text)
435 }
436
437 img, width := font.Render(&self)
438 self.Text.Image = img
439 self.Text.Width = int(width)
440 self.Width = float32(width)
441
442 if n.Style["height"] == "" {
443 self.Height = float32(self.Text.LineHeight)
444 }
445
446 return self
447}