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.
The difference between plugins and transformers is, plugins have a order when they need to be executed and run after the state has been initialised. Transformers on the other hand require nothing but the DOM and the init style definitions. Transformers are used to transform things like an ul tag into a simpler div structure and plugins are used mainly to compute styles outside of the main function (see flex, inline, etc...).
1type Plugin struct {
2 Selector func(*element.Node) bool
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:
- Selector
- A function that determines if the plugin should run.
- 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)
7
8func Init() cstyle.Plugin {
9 return cstyle.Plugin{
10 Selector: func(n *element.Node) bool {
11 styles := map[string]string{
12 "display": "block",
13 }
14 matches := true
15 for name, value := range styles {
16 if n.Style[name] != value && !(value == "*") && n.Style[name] != "" {
17 matches = false
18 }
19 }
20 return matches
21 },
22 Level: 0,
23 Handler: func(n *element.Node, state *map[string]element.State) {
24 s := *state
25 self := s[n.Properties.Id]
26 // parent := s[n.Parent.Properties.Id]
27
28 // If the element is display block and the width is unset then make it 100%
29
30 // if self.Style["width"] == "" {
31 // self.Width, _ = utils.ConvertToPixels("100%", self.EM, parent.Width)
32 // fmt.Println(self.Margin)
33
34 // // self.Width -= (self.Padding.Right + self.Padding.Left)
35 // // self.Height -= (self.Padding.Top + self.Padding.Bottom)
36 // }
37
38 // if self.X+self.Width+(self.Border.Width*2) > parent.Width {
39 // self.Width = parent.Width
40 // self.Width -= (self.Margin.Right + self.Margin.Left)
41 // self.Width -= (self.Border.Width * 2)
42 // self.Height -= (self.Margin.Top + self.Margin.Bottom)
43 // }
44
45 (*state)[n.Properties.Id] = self
46 },
47 }
48}
1package cstyle
2
3import (
4 "fmt"
5 "gui/color"
6 "gui/element"
7 "gui/font"
8 "gui/parser"
9 "gui/utils"
10 "os"
11 "slices"
12 "sort"
13 "strconv"
14 "strings"
15)
16
17// !TODO: Make a fine selector to target tags and if it has children or not etc
18// + could copy the transformers but idk
19type Plugin struct {
20 Selector func(*element.Node) bool
21 Level int
22 Handler func(*element.Node, *map[string]element.State)
23}
24
25type Transformer struct {
26 Selector func(*element.Node) bool
27 Handler func(element.Node, *CSS) element.Node
28}
29
30type CSS struct {
31 Width float32
32 Height float32
33 StyleSheets []map[string]map[string]string
34 Plugins []Plugin
35 Transformers []Transformer
36 Document *element.Node
37}
38
39func (c *CSS) Transform(n element.Node) element.Node {
40 for _, v := range c.Transformers {
41 if v.Selector(&n) {
42 n = v.Handler(n, c)
43 }
44 }
45 for i := 0; i < len(n.Children); i++ {
46 v := n.Children[i]
47 tc := c.Transform(v)
48 n = *tc.Parent
49 n.Children[i] = tc
50 }
51
52 return n
53}
54
55func (c *CSS) StyleSheet(path string) {
56 // Parse the CSS file
57 dat, err := os.ReadFile(path)
58 utils.Check(err)
59 styles := parser.ParseCSS(string(dat))
60
61 c.StyleSheets = append(c.StyleSheets, styles)
62}
63
64func (c *CSS) StyleTag(css string) {
65 styles := parser.ParseCSS(css)
66 c.StyleSheets = append(c.StyleSheets, styles)
67}
68
69var inheritedProps = []string{
70 "color",
71 "cursor",
72 "font",
73 "font-family",
74 "font-size",
75 "font-style",
76 "font-weight",
77 "letter-spacing",
78 "line-height",
79 // "text-align",
80 "text-indent",
81 "text-justify",
82 "text-shadow",
83 "text-transform",
84 "text-decoration",
85 "visibility",
86 "word-spacing",
87 "display",
88}
89
90func (c *CSS) GetStyles(n element.Node) map[string]string {
91 styles := map[string]string{}
92
93 if n.Parent != nil {
94 ps := c.GetStyles(*n.Parent)
95 for _, v := range inheritedProps {
96 if ps[v] != "" {
97 styles[v] = ps[v]
98 }
99 }
100 }
101 for k, v := range n.Style {
102 styles[k] = v
103 }
104 hovered := false
105 if slices.Contains(n.ClassList.Classes, ":hover") {
106 hovered = true
107 }
108
109 for _, styleSheet := range c.StyleSheets {
110 for selector := range styleSheet {
111 // fmt.Println(selector, n.Properties.Id)
112 key := selector
113 if strings.Contains(selector, ":hover") && hovered {
114 selector = strings.Replace(selector, ":hover", "", -1)
115 }
116 if element.TestSelector(selector, &n) {
117 for k, v := range styleSheet[key] {
118 styles[k] = v
119 }
120 }
121
122 }
123 }
124
125 // This is different than node.Style
126 // temp1 = <span style=​"color:​#a6e22e">​CSS​</span>​
127 // temp1.style == CSSStyleDeclaration {0: 'color', accentColor: '', additiveSymbols: '', alignContent: '', alignItems: '', alignSelf: '', …}
128 // temp1.getAttribute("style") == 'color:#a6e22e'
129 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
130 styles = utils.Merge(styles, inline)
131 // add hover and focus css events
132
133 if n.Parent != nil {
134 if styles["z-index"] == "" && n.Parent.Style["z-index"] != "" {
135 z, _ := strconv.Atoi(n.Parent.Style["z-index"])
136 z += 1
137 styles["z-index"] = strconv.Itoa(z)
138 }
139 }
140
141 return styles
142}
143
144func (c *CSS) AddPlugin(plugin Plugin) {
145 c.Plugins = append(c.Plugins, plugin)
146}
147
148func (c *CSS) AddTransformer(transformer Transformer) {
149 c.Transformers = append(c.Transformers, transformer)
150}
151
152func CheckNode(n *element.Node, state *map[string]element.State) {
153 s := *state
154 self := s[n.Properties.Id]
155
156 fmt.Println(n.TagName, n.Properties.Id)
157 fmt.Printf("ID: %v\n", n.Id)
158 fmt.Printf("EM: %v\n", self.EM)
159 fmt.Printf("Parent: %v\n", n.Parent.TagName)
160 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
161 fmt.Printf("Text: %v\n", n.InnerText)
162 fmt.Printf("X: %v, Y: %v, Z: %v\n", self.X, self.Y, self.Z)
163 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
164 fmt.Printf("Styles: %v\n", n.Style)
165 // fmt.Printf("Background: %v\n", self.Background)
166 // fmt.Printf("Border: %v\n\n\n", self.Border)
167}
168
169func (c *CSS) ComputeNodeStyle(n *element.Node, state *map[string]element.State) *element.Node {
170
171 // Head is not renderable
172 if utils.IsParent(*n, "head") {
173 return n
174 }
175
176 plugins := c.Plugins
177
178 s := *state
179 self := s[n.Properties.Id]
180 parent := s[n.Parent.Properties.Id]
181
182 self.Background = color.Parse(n.Style, "background")
183 self.Border, _ = CompleteBorder(n.Style, self, parent)
184
185 fs, _ := utils.ConvertToPixels(n.Style["font-size"], parent.EM, parent.Width)
186 self.EM = fs
187
188 if n.Style["display"] == "none" {
189 self.X = 0
190 self.Y = 0
191 self.Width = 0
192 self.Height = 0
193 return n
194 }
195
196 if n.Style["width"] == "" && n.Style["display"] != "inline" {
197 n.Style["width"] = "100%"
198 }
199
200 // Set Z index value to be sorted in window
201 if n.Style["z-index"] != "" {
202 z, _ := strconv.Atoi(n.Style["z-index"])
203 self.Z = float32(z)
204 }
205
206 if parent.Z > 0 {
207 self.Z = parent.Z + 1
208 }
209
210 (*state)[n.Properties.Id] = self
211
212 wh := utils.GetWH(*n, state)
213 width := wh.Width
214 height := wh.Height
215
216 x, y := parent.X, parent.Y
217 // !NOTE: Would like to consolidate all XY function into this function like WH
218 offsetX, offsetY := utils.GetXY(n, state)
219 x += offsetX
220 y += offsetY
221
222 var top, left, right, bottom bool = false, false, false, false
223
224 m := utils.GetMP(*n, wh, state, "margin")
225 p := utils.GetMP(*n, wh, state, "padding")
226
227 self.Margin = m
228 self.Padding = p
229
230 if n.Style["position"] == "absolute" {
231 bas := utils.GetPositionOffsetNode(n)
232 base := s[bas.Properties.Id]
233 if n.Style["top"] != "" {
234 v, _ := utils.ConvertToPixels(n.Style["top"], self.EM, parent.Width)
235 y = v + base.Y
236 top = true
237 }
238 if n.Style["left"] != "" {
239 v, _ := utils.ConvertToPixels(n.Style["left"], self.EM, parent.Width)
240 x = v + base.X
241 left = true
242 }
243 if n.Style["right"] != "" {
244 v, _ := utils.ConvertToPixels(n.Style["right"], self.EM, parent.Width)
245 x = (base.Width - width) - v
246 right = true
247 }
248 if n.Style["bottom"] != "" {
249 v, _ := utils.ConvertToPixels(n.Style["bottom"], self.EM, parent.Width)
250 y = (base.Height - height) - v
251 bottom = true
252 }
253
254 } else {
255 for i, v := range n.Parent.Children {
256 if v.Style["position"] != "absolute" {
257 if v.Properties.Id == n.Properties.Id {
258 if i-1 > 0 {
259 sib := n.Parent.Children[i-1]
260 sibling := s[sib.Properties.Id]
261 if sib.Style["position"] != "absolute" {
262 if n.Style["display"] == "inline" {
263 if sib.Style["display"] == "inline" {
264 y = sibling.Y
265 } else {
266 y = sibling.Y + sibling.Height
267 }
268 } else {
269 y = sibling.Y + sibling.Height + (sibling.Border.Width * 2) + sibling.Margin.Bottom
270 }
271 }
272
273 }
274 break
275 } else if n.Style["display"] != "inline" {
276 vState := s[v.Properties.Id]
277 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (self.Border.Width)
278 }
279 }
280 }
281 }
282
283 // Display modes need to be calculated here
284
285 relPos := !top && !left && !right && !bottom
286
287 if left || relPos {
288 x += m.Left
289 }
290 if top || relPos {
291 y += m.Top
292 }
293 if right {
294 x -= m.Right
295 }
296 if bottom {
297 y -= m.Bottom
298 }
299
300 self.X = x
301 self.Y = y
302 self.Width = width
303 self.Height = height
304 (*state)[n.Properties.Id] = self
305
306 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
307 // Confirm text exists
308 if len(strings.TrimSpace(n.InnerText)) > 0 {
309 n.InnerText = strings.TrimSpace(n.InnerText)
310 self = genTextNode(n, state)
311 }
312 }
313
314 (*state)[n.Properties.Id] = self
315 (*state)[n.Parent.Properties.Id] = parent
316
317 // Call children here
318 // !TODO: Make something that stops rendering things out of fov
319
320 if self.Y < c.Height {
321 var childYOffset float32
322 for i := 0; i < len(n.Children); i++ {
323 v := n.Children[i]
324 v.Parent = n
325 // This is were the tainting comes from
326 n.Children[i] = *c.ComputeNodeStyle(&v, state)
327
328 cState := (*state)[n.Children[i].Properties.Id]
329 if n.Style["height"] == "" {
330 if v.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
331 childYOffset = cState.Y + cState.Height
332 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
333 self.Height += cState.Margin.Top
334 self.Height += cState.Margin.Bottom
335 self.Height += cState.Padding.Top
336 self.Height += cState.Padding.Bottom
337 }
338 }
339 if cState.Width > self.Width {
340 self.Width = cState.Width
341 }
342 }
343 } else {
344 return n
345 }
346
347 self.Height += self.Padding.Bottom
348
349 (*state)[n.Properties.Id] = self
350
351 // Sorting the array by the Level field
352 sort.Slice(plugins, func(i, j int) bool {
353 return plugins[i].Level < plugins[j].Level
354 })
355
356 for _, v := range plugins {
357 if v.Selector(n) {
358 v.Handler(n, state)
359 }
360 }
361
362 // CheckNode(n, state)
363
364 return n
365}
366
367func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
368 // Split the shorthand into components
369 borderComponents := strings.Fields(cssProperties["border"])
370
371 // Ensure there are at least 1 component (width or style or color)
372 if len(borderComponents) >= 1 {
373 width := "0px" // Default width
374 style := "solid"
375 borderColor := "#000000" // Default color
376
377 // Extract style and color if available
378 if len(borderComponents) >= 1 {
379 width = borderComponents[0]
380 }
381
382 // Extract style and color if available
383 if len(borderComponents) >= 2 {
384 style = borderComponents[1]
385 }
386 if len(borderComponents) >= 3 {
387 borderColor = borderComponents[2]
388 }
389
390 parsedColor, _ := color.Color(borderColor)
391
392 w, _ := utils.ConvertToPixels(width, self.EM, parent.Width)
393
394 return element.Border{
395 Width: w,
396 Style: style,
397 Color: parsedColor,
398 Radius: cssProperties["border-radius"],
399 }, nil
400 }
401
402 return element.Border{}, fmt.Errorf("invalid border shorthand format")
403}
404
405func genTextNode(n *element.Node, state *map[string]element.State) element.State {
406 s := *state
407 self := s[n.Properties.Id]
408 parent := s[n.Parent.Properties.Id]
409
410 text := element.Text{}
411
412 bold, italic := false, false
413
414 if n.Style["font-weight"] == "bold" {
415 bold = true
416 }
417
418 if n.Style["font-style"] == "italic" {
419 italic = true
420 }
421
422 if text.Font == nil {
423 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
424 text.Font = f
425 }
426
427 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, parent.Width)
428 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, parent.Width)
429 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], self.EM, parent.Width)
430 if lineHeight == 0 {
431 lineHeight = self.EM + 3
432 }
433
434 text.LineHeight = int(lineHeight)
435 text.WordSpacing = int(wordSpacing)
436 text.LetterSpacing = int(letterSpacing)
437 wb := " "
438
439 if n.Style["word-wrap"] == "break-word" {
440 wb = ""
441 }
442
443 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
444 wb = ""
445 }
446
447 var dt float32
448
449 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
450 dt = self.EM / 7
451 } else {
452 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, parent.Width)
453 }
454
455 col := color.Parse(n.Style, "font")
456
457 self.Color = col
458
459 text.Color = col
460 text.DecorationColor = color.Parse(n.Style, "decoration")
461 text.Align = n.Style["text-align"]
462 text.WordBreak = wb
463 text.WordSpacing = int(wordSpacing)
464 text.LetterSpacing = int(letterSpacing)
465 text.WhiteSpace = n.Style["white-space"]
466 text.DecorationThickness = int(dt)
467 text.Overlined = n.Style["text-decoration"] == "overline"
468 text.Underlined = n.Style["text-decoration"] == "underline"
469 text.LineThrough = n.Style["text-decoration"] == "linethrough"
470 text.EM = int(self.EM)
471 text.Width = int(parent.Width)
472 text.Text = n.InnerText
473
474 if n.Style["word-spacing"] == "" {
475 text.WordSpacing = font.MeasureSpace(&text)
476 }
477
478 img, width := font.Render(&text)
479 self.Texture = img
480
481 if n.Style["height"] == "" {
482 self.Height = float32(text.LineHeight)
483 }
484
485 if n.Style["width"] == "" {
486 self.Width = float32(width)
487 }
488
489 return self
490}