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