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