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(n *element.Node, state *map[string]element.State) *element.Node {
143
144 // Head is not renderable
145 if utils.IsParent(*n, "head") {
146 return n
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[n.Properties.Id]
155 parent := s[n.Parent.Properties.Id]
156 self.Style = c.GetStyles(*n)
157
158 self.Background = color.Parse(self.Style, "background")
159 self.Border, _ = CompleteBorder(self.Style, self, parent)
160
161 fs, _ := utils.ConvertToPixels(self.Style["font-size"], parent.EM, parent.Width)
162 self.EM = fs
163
164 if self.Style["display"] == "none" {
165 self.X = 0
166 self.Y = 0
167 self.Width = 0
168 self.Height = 0
169 return n
170 }
171
172 if self.Style["width"] == "" && self.Style["display"] == "block" {
173 self.Style["width"] = "100%"
174 }
175
176 // Set Z index value to be sorted in window
177 if self.Style["z-index"] != "" {
178 z, _ := strconv.Atoi(self.Style["z-index"])
179 self.Z = float32(z)
180 }
181
182 if parent.Z > 0 {
183 self.Z = parent.Z + 1
184 }
185
186 (*state)[n.Properties.Id] = self
187
188 wh := utils.GetWH(*n, state)
189 width := wh.Width
190 height := wh.Height
191
192 x, y := parent.X, parent.Y
193 // !NOTE: Would like to consolidate all XY function into this function like WH
194 offsetX, offsetY := utils.GetXY(n, state)
195 x += offsetX
196 y += offsetY
197
198 var top, left, right, bottom bool = false, false, false, false
199
200 m := utils.GetMP(*n, wh, state, "margin")
201 p := utils.GetMP(*n, wh, state, "padding")
202
203 self.Margin = m
204 self.Padding = p
205
206 if self.Style["position"] == "absolute" {
207 bas := utils.GetPositionOffsetNode(n, state)
208 base := s[bas.Properties.Id]
209 if self.Style["top"] != "" {
210 v, _ := utils.ConvertToPixels(self.Style["top"], self.EM, parent.Width)
211 y = v + base.Y
212 top = true
213 }
214 if self.Style["left"] != "" {
215 v, _ := utils.ConvertToPixels(self.Style["left"], self.EM, parent.Width)
216 x = v + base.X
217 left = true
218 }
219 if self.Style["right"] != "" {
220 v, _ := utils.ConvertToPixels(self.Style["right"], self.EM, parent.Width)
221 x = (base.Width - width) - v
222 right = true
223 }
224 if self.Style["bottom"] != "" {
225 v, _ := utils.ConvertToPixels(self.Style["bottom"], self.EM, parent.Width)
226 y = (base.Height - height) - v
227 bottom = true
228 }
229
230 } else {
231 for i, v := range n.Parent.Children {
232 vState := s[v.Properties.Id]
233 if vState.Style["position"] != "absolute" {
234 if v.Properties.Id == n.Properties.Id {
235 if i-1 > 0 {
236 sib := n.Parent.Children[i-1]
237 sibling := s[sib.Properties.Id]
238 if sibling.Style["position"] != "absolute" {
239 if self.Style["display"] == "inline" {
240 if sibling.Style["display"] == "inline" {
241 y = sibling.Y
242 } else {
243 y = sibling.Y + sibling.Height
244 }
245 } else {
246 y = sibling.Y + sibling.Height + (sibling.Border.Width * 2) + sibling.Margin.Bottom
247 }
248 }
249
250 }
251 break
252 } else if self.Style["display"] != "inline" {
253 vState := s[v.Properties.Id]
254 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (self.Border.Width)
255 }
256 }
257 }
258 }
259
260 // Display modes need to be calculated here
261
262 relPos := !top && !left && !right && !bottom
263
264 if left || relPos {
265 x += m.Left
266 }
267 if top || relPos {
268 y += m.Top
269 }
270 if right {
271 x -= m.Right
272 }
273 if bottom {
274 y -= m.Bottom
275 }
276
277 self.X = x
278 self.Y = y
279 self.Width = width
280 self.Height = height
281 (*state)[n.Properties.Id] = self
282
283 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
284 // Confirm text exists
285 words := strings.Split(strings.TrimSpace(n.InnerText), " ")
286 if len(words) != 1 {
287 if self.Style["display"] == "inline" {
288 n.InnerText = words[0]
289 n.Style["inlineText"] = "true"
290 el := *n
291 el.InnerText = strings.Join(words[1:], " ")
292 n.Parent.InsertAfter(el, *n)
293 } else {
294 el := n.CreateElement("notaspan")
295 el.InnerText = n.InnerText
296 n.AppendChild(el)
297 self.Style["font-size"] = parent.Style["font-size"]
298 self.EM = parent.EM
299 n.InnerText = ""
300 }
301 (*state)[n.Properties.Id] = self
302 }
303 if len(strings.TrimSpace(n.InnerText)) > 0 {
304 n.InnerText = strings.TrimSpace(n.InnerText)
305 self = genTextNode(n, state)
306 }
307 }
308
309 (*state)[n.Properties.Id] = self
310 (*state)[n.Parent.Properties.Id] = parent
311
312 // Call children here
313
314 var childYOffset float32
315 for i := 0; i < len(n.Children); i++ {
316 v := n.Children[i]
317 v.Parent = n
318 // This is were the tainting comes from
319 n.Children[i] = *c.ComputeNodeStyle(&v, state)
320
321 cState := (*state)[n.Children[i].Properties.Id]
322 if self.Style["height"] == "" {
323 if cState.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
324 childYOffset = cState.Y + cState.Height
325 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
326 self.Height += cState.Margin.Top
327 self.Height += cState.Margin.Bottom
328 self.Height += cState.Padding.Top
329 self.Height += cState.Padding.Bottom
330 }
331 }
332 if cState.Width > self.Width {
333 self.Width = cState.Width
334 }
335 }
336
337 self.Height += self.Padding.Bottom
338
339 (*state)[n.Properties.Id] = self
340
341 // Sorting the array by the Level field
342 sort.Slice(plugins, func(i, j int) bool {
343 return plugins[i].Level < plugins[j].Level
344 })
345
346 for _, v := range plugins {
347 matches := true
348 for name, value := range v.Styles {
349 if self.Style[name] != value && !(value == "*") && self.Style[name] != "" {
350 matches = false
351 }
352 }
353 if matches {
354 // !NOTE: Might save memory by making a state map tree and passing that instead of the node it's self
355 v.Handler(n, state)
356 }
357 }
358
359 // n.InnerHTML = utils.InnerHTML(*n)
360 // tag, closing := utils.NodeToHTML(*n)
361 // n.OuterHTML = tag + n.InnerHTML + closing
362
363 return n
364}
365
366func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
367 // Split the shorthand into components
368 borderComponents := strings.Fields(cssProperties["border"])
369
370 // Ensure there are at least 1 component (width or style or color)
371 if len(borderComponents) >= 1 {
372 width := "0px" // Default width
373 style := "solid"
374 borderColor := "#000000" // Default color
375
376 // Extract style and color if available
377 if len(borderComponents) >= 1 {
378 width = borderComponents[0]
379 }
380
381 // Extract style and color if available
382 if len(borderComponents) >= 2 {
383 style = borderComponents[1]
384 }
385 if len(borderComponents) >= 3 {
386 borderColor = borderComponents[2]
387 }
388
389 parsedColor, _ := color.Color(borderColor)
390
391 w, _ := utils.ConvertToPixels(width, self.EM, parent.Width)
392
393 return element.Border{
394 Width: w,
395 Style: style,
396 Color: parsedColor,
397 Radius: cssProperties["border-radius"],
398 }, nil
399 }
400
401 return element.Border{}, fmt.Errorf("invalid border shorthand format")
402}
403
404func genTextNode(n *element.Node, state *map[string]element.State) element.State {
405 s := *state
406 self := s[n.Properties.Id]
407 parent := s[n.Parent.Properties.Id]
408
409 text := element.Text{}
410
411 bold, italic := false, false
412
413 if self.Style["font-weight"] == "bold" {
414 bold = true
415 }
416
417 if self.Style["font-style"] == "italic" {
418 italic = true
419 }
420
421 if text.Font == nil {
422 f, _ := font.LoadFont(self.Style["font-family"], int(self.EM), bold, italic)
423 text.Font = f
424 }
425
426 letterSpacing, _ := utils.ConvertToPixels(self.Style["letter-spacing"], self.EM, parent.Width)
427 wordSpacing, _ := utils.ConvertToPixels(self.Style["word-spacing"], self.EM, parent.Width)
428 lineHeight, _ := utils.ConvertToPixels(self.Style["line-height"], self.EM, parent.Width)
429 if lineHeight == 0 {
430 lineHeight = self.EM + 3
431 }
432
433 text.LineHeight = int(lineHeight)
434 text.WordSpacing = int(wordSpacing)
435 text.LetterSpacing = int(letterSpacing)
436 wb := " "
437
438 if self.Style["word-wrap"] == "break-word" {
439 wb = ""
440 }
441
442 if self.Style["text-wrap"] == "wrap" || self.Style["text-wrap"] == "balance" {
443 wb = ""
444 }
445
446 var dt float32
447
448 if self.Style["text-decoration-thickness"] == "auto" || self.Style["text-decoration-thickness"] == "" {
449 dt = self.EM / 7
450 } else {
451 dt, _ = utils.ConvertToPixels(self.Style["text-decoration-thickness"], self.EM, parent.Width)
452 }
453
454 col := color.Parse(self.Style, "font")
455
456 self.Color = col
457
458 text.Color = col
459 text.DecorationColor = color.Parse(self.Style, "decoration")
460 text.Align = self.Style["text-align"]
461 text.WordBreak = wb
462 text.WordSpacing = int(wordSpacing)
463 text.LetterSpacing = int(letterSpacing)
464 text.WhiteSpace = self.Style["white-space"]
465 text.DecorationThickness = int(dt)
466 text.Overlined = self.Style["text-decoration"] == "overline"
467 text.Underlined = self.Style["text-decoration"] == "underline"
468 text.LineThrough = self.Style["text-decoration"] == "linethrough"
469 text.EM = int(self.EM)
470 text.Width = int(parent.Width)
471 text.Text = n.InnerText
472
473 if self.Style["word-spacing"] == "" {
474 text.WordSpacing = font.MeasureSpace(&text)
475 }
476
477 img, width := font.Render(&text)
478 self.Texture = img
479
480 if self.Style["height"] == "" {
481 self.Height = float32(text.LineHeight)
482 }
483
484 if self.Style["width"] == "" {
485 self.Width = float32(width)
486 }
487
488 return self
489}