CStyle
CStyle is a single pass style computer.
# StyleSheet?(go)
# StyleTag?(go)
# GetStyles?(go)
# AddPlugin?(go)
See /cstyle/plugins/
# ComputeNodeStyle?(go)
if !utils.ChildrenHaveText(n) The utils.ChildrenHaveText function is called here instead of checking if the node directly has text is because in the case below
1<em><b>Text</b></em>
The em
element does not have text but it has a element with text insid, but the element will still need to be rendered as text.
# parseBorderShorthand?(go)
# CompleteBorder?(go)
# genTextNode?(go)
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}