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 "gui/utils"
7)
8
9func Init() cstyle.Plugin {
10 return cstyle.Plugin{
11 Styles: map[string]string{
12 "display": "block",
13 },
14 Level: 1,
15 Handler: func(n *element.Node, state *map[string]element.State) {
16 s := *state
17 self := s[n.Properties.Id]
18 parent := s[n.Parent.Properties.Id]
19
20 // If the element is display block and the width is unset then make it 100%
21
22 if n.Style["width"] == "" {
23 self.Width, _ = utils.ConvertToPixels("100%", self.EM, parent.Width)
24 }
25 m := utils.GetMP(*n, "margin")
26 self.Width -= (m.Right + m.Left)
27 self.Height -= (m.Top + m.Bottom)
28
29 p := utils.GetMP(*n, "padding")
30 self.Width += (p.Right + p.Left)
31 self.Height += (p.Top + p.Bottom)
32
33 (*state)[n.Properties.Id] = self
34 (*state)[n.Parent.Properties.Id] = parent
35 },
36 }
37}
1package cstyle
2
3// package aui/goldie
4// https://pkg.go.dev/automated.sh/goldie
5// https://pkg.go.dev/automated.sh/aui
6// https://pkg.go.dev/automated.sh/oat
7
8import (
9 "fmt"
10 "gui/color"
11 "gui/element"
12 "gui/font"
13 "gui/parser"
14 "gui/utils"
15 "os"
16 "slices"
17 "sort"
18 "strings"
19)
20
21type Plugin struct {
22 Styles map[string]string
23 Level int
24 Handler func(*element.Node, *map[string]element.State)
25}
26
27type CSS struct {
28 Width float32
29 Height float32
30 StyleSheets []map[string]map[string]string
31 Plugins []Plugin
32 Document *element.Node
33}
34
35func (c *CSS) StyleSheet(path string) {
36 // Parse the CSS file
37 dat, err := os.ReadFile(path)
38 utils.Check(err)
39 styles := parser.ParseCSS(string(dat))
40
41 c.StyleSheets = append(c.StyleSheets, styles)
42}
43
44func (c *CSS) StyleTag(css string) {
45 styles := parser.ParseCSS(css)
46 c.StyleSheets = append(c.StyleSheets, styles)
47}
48
49var inheritedProps = []string{
50 "color",
51 "cursor",
52 "font",
53 "font-family",
54 "font-size",
55 "font-style",
56 "font-weight",
57 "letter-spacing",
58 "line-height",
59 "text-align",
60 "text-indent",
61 "text-justify",
62 "text-shadow",
63 "text-transform",
64 "visibility",
65 "word-spacing",
66 "display",
67}
68
69// need to get rid of the .props for the most part all styles should be computed dynamically
70// can keep like focusable and stuff that describes the element
71
72// currently the append child does not work due to the props and other stuff not existing so it fails
73// moving to a real time style compute would fix that
74
75// :hover is parsed correctly but because the hash func doesn't invalidate it becuase the val
76// is updated in the props. change to append :hover to style to create the effect
77// or merge the class with the styles? idk have to think more
78
79func (c *CSS) GetStyles(n element.Node) map[string]string {
80 styles := map[string]string{}
81
82 if n.Parent != nil {
83 ps := c.GetStyles(*n.Parent)
84 for _, v := range inheritedProps {
85 if ps[v] != "" {
86 styles[v] = ps[v]
87 }
88 }
89 }
90 for k, v := range n.Style {
91 styles[k] = v
92 }
93 hovered := false
94 if slices.Contains(n.ClassList.Classes, ":hover") {
95 hovered = true
96 }
97
98 for _, styleSheet := range c.StyleSheets {
99 for selector := range styleSheet {
100 // fmt.Println(selector, n.Properties.Id)
101 key := selector
102 if strings.Contains(selector, ":hover") && hovered {
103 selector = strings.Replace(selector, ":hover", "", -1)
104 }
105 if element.TestSelector(selector, &n) {
106 for k, v := range styleSheet[key] {
107 styles[k] = v
108 }
109 }
110
111 }
112 }
113 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
114 styles = utils.Merge(styles, inline)
115 // add hover and focus css events
116
117 return styles
118}
119
120func (c *CSS) AddPlugin(plugin Plugin) {
121 c.Plugins = append(c.Plugins, plugin)
122}
123
124func CheckNode(n *element.Node, state *map[string]element.State) {
125 s := *state
126 self := s[n.Properties.Id]
127
128 fmt.Println(n.TagName, n.Properties.Id)
129 fmt.Printf("ID: %v\n", n.Id)
130 fmt.Printf("Parent: %v\n", n.Parent.TagName)
131 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
132 fmt.Printf("Text: %v\n", self.Text.Text)
133 fmt.Printf("X: %v, Y: %v\n", self.X, self.Y)
134 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
135 fmt.Printf("Styles: %v\n", n.Style)
136 fmt.Printf("Background: %v\n", self.Background)
137 fmt.Printf("Border: %v\n\n\n", self.Border)
138}
139
140func (c *CSS) ComputeNodeStyle(n *element.Node, state *map[string]element.State) *element.Node {
141 // Head is not renderable
142 if utils.IsParent(*n, "head") {
143 return n
144 }
145 plugins := c.Plugins
146 n.Style = c.GetStyles(*n)
147 s := *state
148 self := s[n.Properties.Id]
149 parent := s[n.Parent.Properties.Id]
150
151 self.Background = color.Parse(n.Style, "background")
152 self.Border, _ = CompleteBorder(n.Style)
153
154 fs, _ := utils.ConvertToPixels(n.Style["font-size"], parent.EM, parent.Width)
155 self.EM = fs
156
157 if n.Style["display"] == "none" {
158 self.X = 0
159 self.Y = 0
160 self.Width = 0
161 self.Height = 0
162 return n
163 }
164
165 wh := utils.GetWH(*n)
166 width := wh.Width
167 height := wh.Height
168
169 x, y := parent.X, parent.Y
170
171 var top, left, right, bottom bool = false, false, false, false
172
173 m := utils.GetMP(*n, "margin")
174 p := utils.GetMP(*n, "padding")
175
176 self.Margin = m
177 self.Padding = p
178
179 if n.Style["position"] == "absolute" {
180 bas := utils.GetPositionOffsetNode(n)
181 base := s[bas.Properties.Id]
182 if n.Style["top"] != "" {
183 v, _ := utils.ConvertToPixels(n.Style["top"], self.EM, parent.Width)
184 y = v + base.Y
185 top = true
186 }
187 if n.Style["left"] != "" {
188 v, _ := utils.ConvertToPixels(n.Style["left"], self.EM, parent.Width)
189 x = v + base.X
190 left = true
191 }
192 if n.Style["right"] != "" {
193 v, _ := utils.ConvertToPixels(n.Style["right"], self.EM, parent.Width)
194 x = (base.Width - width) - v
195 right = true
196 }
197 if n.Style["bottom"] != "" {
198 v, _ := utils.ConvertToPixels(n.Style["bottom"], self.EM, parent.Width)
199 y = (base.Height - height) - v
200 bottom = true
201 }
202 } else {
203 for i, v := range n.Parent.Children {
204 if v.Properties.Id == n.Properties.Id {
205 if i-1 > 0 {
206 sib := n.Parent.Children[i-1]
207 sibling := s[sib.Properties.Id]
208 if n.Style["display"] == "inline" {
209 if sib.Style["display"] == "inline" {
210 y = sibling.Y
211 } else {
212 y = sibling.Y + sibling.Height
213 }
214 } else {
215 y = sibling.Y + sibling.Height
216 }
217 }
218 break
219 } else if n.Style["display"] != "inline" {
220 vState := s[v.Properties.Id]
221 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height
222 }
223 }
224 }
225
226 // Display modes need to be calculated here
227
228 relPos := !top && !left && !right && !bottom
229
230 if left || relPos {
231 x += m.Left
232 }
233 if top || relPos {
234 y += m.Top
235 }
236 if right {
237 x -= m.Right
238 }
239 if bottom {
240 y -= m.Bottom
241 }
242
243 if len(n.Children) == 0 {
244 // Confirm text exists
245 if len(n.InnerText) > 0 {
246 innerWidth := width
247 innerHeight := height
248 (*state)[n.Properties.Id] = self
249 self = genTextNode(n, &innerWidth, &innerHeight, p, state)
250 width = innerWidth + p.Left + p.Right
251 height = innerHeight
252 }
253 }
254
255 self.X = x
256 self.Y = y
257 self.Width = width
258 self.Height = height
259
260 (*state)[n.Properties.Id] = self
261 (*state)[n.Parent.Properties.Id] = parent
262
263 // CheckNode(n, state)
264
265 // Call children here
266
267 var childYOffset float32
268 for i, v := range n.Children {
269 v.Parent = n
270 n.Children[i] = *c.ComputeNodeStyle(&v, state)
271 if n.Style["height"] == "" {
272 cState := s[n.Children[i].Properties.Id]
273 if n.Children[i].Style["position"] != "absolute" && cState.Y > childYOffset {
274 childYOffset = cState.Y
275 self.Height += cState.Height
276 self.Height += cState.Margin.Top
277 self.Height += cState.Margin.Bottom
278 self.Height += cState.Padding.Top
279 self.Height += cState.Padding.Bottom
280 }
281
282 }
283 }
284
285 (*state)[n.Properties.Id] = self
286
287 // Sorting the array by the Level field
288 sort.Slice(plugins, func(i, j int) bool {
289 return plugins[i].Level < plugins[j].Level
290 })
291
292 for _, v := range plugins {
293 matches := true
294 for name, value := range v.Styles {
295 if n.Style[name] != value && !(value == "*") {
296 matches = false
297 }
298 }
299 if matches {
300 v.Handler(n, state)
301 }
302 }
303
304 return n
305}
306
307func parseBorderShorthand(borderShorthand string) (element.Border, error) {
308 // Split the shorthand into components
309 borderComponents := strings.Fields(borderShorthand)
310
311 // Ensure there are at least 1 component (width or style or color)
312 if len(borderComponents) >= 1 {
313 width := "0px" // Default width
314 style := "solid"
315 borderColor := "#000000" // Default color
316
317 // Extract style and color if available
318 if len(borderComponents) >= 1 {
319 width = borderComponents[0]
320 }
321
322 // Extract style and color if available
323 if len(borderComponents) >= 2 {
324 style = borderComponents[1]
325 }
326 if len(borderComponents) >= 3 {
327 borderColor = borderComponents[2]
328 }
329
330 parsedColor, _ := color.Color(borderColor)
331
332 return element.Border{
333 Width: width,
334 Style: style,
335 Color: parsedColor,
336 Radius: "", // Default radius
337 }, nil
338 }
339
340 return element.Border{}, fmt.Errorf("invalid border shorthand format")
341}
342
343func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
344 border, err := parseBorderShorthand(cssProperties["border"])
345 border.Radius = cssProperties["border-radius"]
346
347 return border, err
348}
349
350func genTextNode(n *element.Node, width, height *float32, p element.MarginPadding, state *map[string]element.State) element.State {
351 s := *state
352 self := s[n.Properties.Id]
353 parent := s[n.Parent.Properties.Id]
354
355 bold, italic := false, false
356
357 if n.Style["font-weight"] == "bold" {
358 bold = true
359 }
360
361 if n.Style["font-style"] == "italic" {
362 italic = true
363 }
364
365 if self.Text.Font == nil {
366 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
367 self.Text.Font = f
368 }
369
370 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, *width)
371 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, *width)
372 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], self.EM, *width)
373 if lineHeight == 0 {
374 lineHeight = self.EM + 3
375 }
376
377 self.Text.LineHeight = int(lineHeight)
378 self.Text.WordSpacing = int(wordSpacing)
379 self.Text.LetterSpacing = int(letterSpacing)
380 wb := " "
381
382 if n.Style["word-wrap"] == "break-word" {
383 wb = ""
384 }
385
386 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
387 wb = ""
388 }
389
390 var dt float32
391
392 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
393 dt = 2
394 } else {
395 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, *width)
396 }
397
398 col := color.Parse(n.Style, "font")
399
400 self.Text.Color = col
401 self.Text.Align = n.Style["text-align"]
402 self.Text.WordBreak = wb
403 self.Text.WordSpacing = int(wordSpacing)
404 self.Text.LetterSpacing = int(letterSpacing)
405 self.Text.WhiteSpace = n.Style["white-space"]
406 self.Text.DecorationThickness = int(dt)
407 self.Text.Overlined = n.Style["text-decoration"] == "overline"
408 self.Text.Underlined = n.Style["text-decoration"] == "underline"
409 self.Text.LineThrough = n.Style["text-decoration"] == "linethrough"
410 self.Text.EM = int(self.EM)
411 self.Text.Width = int(parent.Width)
412 self.Text.Text = n.InnerText
413
414 if n.Style["word-spacing"] == "" {
415 self.Text.WordSpacing = font.MeasureSpace(&self.Text)
416 }
417 if parent.Width != 0 && n.Style["display"] != "inline" && n.Style["width"] == "" {
418 *width = (parent.Width - p.Right) - p.Left
419 } else if n.Style["width"] == "" {
420 *width = utils.Max(*width, float32(font.MeasureLongest(&self)))
421 } else if n.Style["width"] != "" {
422 *width, _ = utils.ConvertToPixels(n.Style["width"], self.EM, parent.Width)
423 }
424
425 self.Text.Width = int(*width)
426 h := font.Render(&self)
427 if n.Style["height"] == "" {
428 *height = h
429 }
430
431 return self
432
433}