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 var childYOffset float32
347 for i := 0; i < len(n.Children); i++ {
348 v := n.Children[i]
349 v.Parent = n
350 // This is were the tainting comes from
351 n.Children[i] = *c.ComputeNodeStyle(&v, state)
352
353 cState := (*state)[n.Children[i].Properties.Id]
354 if n.Style["height"] == "" && n.Style["min-height"] == "" {
355 if v.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
356 childYOffset = cState.Y + cState.Height
357 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
358 self.Height += cState.Margin.Top
359 self.Height += cState.Margin.Bottom
360 self.Height += cState.Padding.Top
361 self.Height += cState.Padding.Bottom
362 self.Height += cState.Border.Width * 2
363 }
364 }
365 if cState.Width > self.Width {
366 self.Width = cState.Width
367 }
368 }
369
370 if n.Style["height"] == "" {
371 self.Height += self.Padding.Bottom
372 }
373
374 (*state)[n.Properties.Id] = self
375
376 // Sorting the array by the Level field
377 sort.Slice(plugins, func(i, j int) bool {
378 return plugins[i].Level < plugins[j].Level
379 })
380
381 for _, v := range plugins {
382 if v.Selector(n) {
383 v.Handler(n, state)
384 }
385 }
386
387 // CheckNode(n, state)
388 return n
389}
390
391func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
392 // Split the shorthand into components
393 borderComponents := strings.Fields(cssProperties["border"])
394
395 // Default values
396 width := "0px" // Default width
397 style := "solid"
398 borderColor := "#000000" // Default color
399
400 // Suffixes for width properties
401 widthSuffixes := []string{"px", "em", "pt", "pc", "%", "vw", "vh", "cm", "in"}
402
403 // Identify each component regardless of order
404 for _, component := range borderComponents {
405 if isWidthComponent(component, widthSuffixes) {
406 width = component
407 } else {
408 switch component {
409 case "thin", "medium", "thick":
410 width = component
411 case "none", "hidden", "dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset":
412 style = component
413 default:
414 // Handle colors
415 borderColor = component
416 }
417 }
418 }
419
420 parsedColor, _ := color.Color(borderColor)
421 w := utils.ConvertToPixels(width, self.EM, parent.Width)
422
423 return element.Border{
424 Width: w,
425 Style: style,
426 Color: parsedColor,
427 Radius: cssProperties["border-radius"],
428 }, nil
429}
430
431// Helper function to determine if a component is a width value
432func isWidthComponent(component string, suffixes []string) bool {
433 for _, suffix := range suffixes {
434 if strings.HasSuffix(component, suffix) {
435 return true
436 }
437 }
438 return false
439}
440
441func genTextNode(n *element.Node, state *map[string]element.State, css *CSS) element.State {
442 s := *state
443 self := s[n.Properties.Id]
444 parent := s[n.Parent.Properties.Id]
445
446 text := element.Text{}
447
448 bold, italic := false, false
449 // !ISSUE: needs bolder and the 100 -> 900
450 if n.Style["font-weight"] == "bold" {
451 bold = true
452 }
453
454 if n.Style["font-style"] == "italic" {
455 italic = true
456 }
457
458 if text.Font == nil {
459 if css.Fonts == nil {
460 css.Fonts = map[string]imgFont.Face{}
461 }
462 fid := n.Style["font-family"] + fmt.Sprint(self.EM, bold, italic)
463 if css.Fonts[fid] == nil {
464 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
465 css.Fonts[fid] = f
466 }
467 fnt := css.Fonts[fid]
468 text.Font = &fnt
469 }
470
471 letterSpacing := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, parent.Width)
472 wordSpacing := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, parent.Width)
473 lineHeight := utils.ConvertToPixels(n.Style["line-height"], self.EM, parent.Width)
474 if lineHeight == 0 {
475 lineHeight = self.EM + 3
476 }
477
478 text.LineHeight = int(lineHeight)
479 text.WordSpacing = int(wordSpacing)
480 text.LetterSpacing = int(letterSpacing)
481 wb := " "
482
483 if n.Style["word-wrap"] == "break-word" {
484 wb = ""
485 }
486
487 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
488 wb = ""
489 }
490
491 var dt float32
492
493 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
494 dt = self.EM / 7
495 } else {
496 dt = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, parent.Width)
497 }
498
499 col := color.Parse(n.Style, "font")
500
501 self.Color = col
502
503 text.Color = col
504 text.DecorationColor = color.Parse(n.Style, "decoration")
505 text.Align = n.Style["text-align"]
506 text.WordBreak = wb
507 text.WordSpacing = int(wordSpacing)
508 text.LetterSpacing = int(letterSpacing)
509 text.WhiteSpace = n.Style["white-space"]
510 text.DecorationThickness = int(dt)
511 text.Overlined = n.Style["text-decoration"] == "overline"
512 text.Underlined = n.Style["text-decoration"] == "underline"
513 text.LineThrough = n.Style["text-decoration"] == "linethrough"
514 text.EM = int(self.EM)
515 text.Width = int(parent.Width)
516 text.Text = n.InnerText
517 text.Last = n.GetAttribute("last") == "true"
518
519 if n.Style["word-spacing"] == "" {
520 text.WordSpacing = font.MeasureSpace(&text)
521 }
522
523 img, width := font.Render(&text)
524 self.Texture = img
525
526 if n.Style["height"] == "" && n.Style["min-height"] == "" {
527 self.Height = float32(text.LineHeight)
528 }
529
530 if n.Style["width"] == "" && n.Style["min-width"] == "" {
531 self.Width = float32(width)
532 }
533
534 return self
535}