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