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