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