GUI
GUI is the source package
# Open?(go)
# New?(go)
# View?(go)
# CreateNode?(go)
# extractStylesheets?(go)
# extractStyleTags?(go)
# localizePath?(go)
# encapsulateText?(go)
# matchFactory?(go)
# removeWhitespace?(go)
# removeHTMLComments?(go)
1package gui
2
3import (
4 "bufio"
5 "bytes"
6 "crypto/sha256"
7 _ "embed"
8 "encoding/json"
9 "gui/cstyle"
10 "gui/cstyle/plugins/flex"
11 "gui/cstyle/plugins/inline"
12 "gui/cstyle/plugins/textAlign"
13 "gui/cstyle/transformers/text"
14 "gui/window"
15
16 "gui/element"
17 "gui/events"
18 "gui/utils"
19 "net/url"
20 "os"
21 "path/filepath"
22 "regexp"
23 "strconv"
24 "strings"
25
26 rl "github.com/gen2brain/raylib-go/raylib"
27 "golang.org/x/net/html"
28)
29
30// _ "net/http/pprof"
31
32//go:embed master.css
33var mastercss string
34
35type Window struct {
36 CSS cstyle.CSS
37 Document element.Node
38 Adapter func()
39}
40
41func Open(path string) Window {
42 window := New()
43
44 styleSheets, styleTags, htmlNodes := parseHTMLFromFile(path)
45
46 for _, v := range styleSheets {
47 window.CSS.StyleSheet(v)
48 }
49
50 for _, v := range styleTags {
51 window.CSS.StyleTag(v)
52 }
53
54 CreateNode(htmlNodes, &window.Document)
55
56 return window
57}
58
59func New() Window {
60 css := cstyle.CSS{
61 Width: 800,
62 Height: 450,
63 }
64
65 css.StyleTag(mastercss)
66 // This is still apart of computestyle
67 // css.AddPlugin(position.Init())
68 css.AddPlugin(inline.Init())
69 // css.AddPlugin(block.Init())
70 css.AddPlugin(textAlign.Init())
71 // css.AddPlugin(inlineText.Init())
72 css.AddPlugin(flex.Init())
73
74 css.AddTransformer(text.Init())
75
76 el := element.Node{}
77 document := el.CreateElement("ROOT")
78 document.Style["width"] = "800px"
79 document.Style["height"] = "450px"
80 document.Properties.Id = "ROOT"
81 return Window{
82 CSS: css,
83 Document: document,
84 }
85}
86
87func (w *Window) Render(doc element.Node, state *map[string]element.State) []element.State {
88 s := *state
89
90 flatDoc := flatten(doc)
91
92 store := []element.State{}
93
94 keys := []string{}
95
96 for _, v := range flatDoc {
97 store = append(store, s[v.Properties.Id])
98 keys = append(keys, v.Properties.Id)
99 }
100
101 // Create a set of keys to keep
102 keysSet := make(map[string]struct{}, len(keys))
103 for _, key := range keys {
104 keysSet[key] = struct{}{}
105 }
106
107 // Iterate over the map and delete keys not in the set
108 for k := range s {
109 if _, found := keysSet[k]; !found {
110 delete(s, k)
111 }
112 }
113
114 return store
115}
116
117func flatten(n element.Node) []element.Node {
118 var nodes []element.Node
119 nodes = append(nodes, n)
120
121 children := n.Children
122 if len(children) > 0 {
123 for _, ch := range children {
124 chNodes := flatten(ch)
125 nodes = append(nodes, chNodes...)
126 }
127 }
128 return nodes
129}
130
131func View(data *Window, width, height int32) {
132 debug := false
133 data.Document.Style["width"] = strconv.Itoa(int(width)) + "px"
134 data.Document.Style["height"] = strconv.Itoa(int(height)) + "px"
135
136 wm := window.NewWindowManager()
137 // wm.FPS = true
138
139 wm.OpenWindow(width, height)
140 defer wm.CloseWindow()
141
142 evts := map[string]element.EventList{}
143
144 eventStore := &evts
145
146 state := map[string]element.State{}
147
148 shouldStop := false
149
150 var hash []byte
151 var rd []element.State
152
153 // Main game loop
154 for !wm.WindowShouldClose() && !shouldStop {
155 // fmt.Println("######################")
156 rl.BeginDrawing()
157 if !shouldStop && debug {
158 shouldStop = true
159 }
160 // Check if the window size has changed
161 newWidth := int32(rl.GetScreenWidth())
162 newHeight := int32(rl.GetScreenHeight())
163
164 resize := false
165
166 if newWidth != width || newHeight != height {
167 resize = true
168 rl.ClearBackground(rl.RayWhite)
169 // Window has been resized, handle the event
170 width = newWidth
171 height = newHeight
172
173 data.CSS.Width = float32(width)
174 data.CSS.Height = float32(height)
175
176 data.Document.Style["width"] = strconv.Itoa(int(width)) + "px"
177 data.Document.Style["height"] = strconv.Itoa(int(height)) + "px"
178 }
179
180 newHash, _ := hashStruct(&data.Document.Children[0])
181 eventStore = events.GetEvents(&data.Document.Children[0], &state, eventStore)
182 if !bytes.Equal(hash, newHash) || resize {
183 hash = newHash
184 newDoc := CopyNode(data.CSS, data.Document.Children[0], &data.Document)
185
186 newDoc = data.CSS.Transform(newDoc)
187
188 data.CSS.ComputeNodeStyle(&newDoc, &state)
189 rd = data.Render(newDoc, &state)
190 wm.LoadTextures(rd)
191 AddHTML(&data.Document)
192 }
193 wm.Draw(rd)
194
195 events.RunEvents(eventStore)
196
197 rl.EndDrawing()
198 }
199}
200
201func CopyNode(c cstyle.CSS, node element.Node, parent *element.Node) element.Node {
202 n := element.Node{}
203 n.TagName = node.TagName
204 n.InnerText = node.InnerText
205 n.Style = node.Style
206 n.Id = node.Id
207 n.ClassList = node.ClassList
208 n.Href = node.Href
209 n.Src = node.Src
210 n.Title = node.Title
211 n.Attribute = node.Attribute
212 n.Value = node.Value
213 n.ScrollY = node.ScrollY
214 n.InnerHTML = node.InnerHTML
215 n.OuterHTML = node.OuterHTML
216 n.Properties.Id = node.Properties.Id
217 n.Properties.Focusable = node.Properties.Focusable
218 n.Properties.Focused = node.Properties.Focused
219 n.Properties.Editable = node.Properties.Editable
220 n.Properties.Hover = node.Properties.Hover
221 n.Properties.Selected = node.Properties.Selected
222
223 n.Parent = parent
224
225 n.Style = c.GetStyles(n)
226
227 for _, v := range node.Children {
228 n.Children = append(n.Children, CopyNode(c, v, &n))
229 }
230 return n
231}
232
233func CreateNode(node *html.Node, parent *element.Node) {
234 if node.Type == html.ElementNode {
235 newNode := parent.CreateElement(node.Data)
236 for _, attr := range node.Attr {
237 if attr.Key == "class" {
238 classes := strings.Split(attr.Val, " ")
239 for _, class := range classes {
240 newNode.ClassList.Add(class)
241 }
242 } else if attr.Key == "id" {
243 newNode.Id = attr.Val
244 } else if attr.Key == "contenteditable" && (attr.Val == "" || attr.Val == "true") {
245 newNode.Properties.Editable = true
246 } else if attr.Key == "href" {
247 newNode.Href = attr.Val
248 } else if attr.Key == "src" {
249 newNode.Src = attr.Val
250 } else if attr.Key == "title" {
251 newNode.Title = attr.Val
252 } else {
253 newNode.SetAttribute(attr.Key, attr.Val)
254 }
255 }
256 newNode.InnerText = strings.TrimSpace(utils.GetInnerText(node))
257 // Recursively traverse child nodes
258 for child := node.FirstChild; child != nil; child = child.NextSibling {
259 if child.Type == html.ElementNode {
260 CreateNode(child, &newNode)
261 }
262 }
263 parent.AppendChild(newNode)
264
265 } else {
266 for child := node.FirstChild; child != nil; child = child.NextSibling {
267 if child.Type == html.ElementNode {
268 CreateNode(child, parent)
269 }
270 }
271 }
272}
273
274func AddHTML(n *element.Node) {
275 // Head is not renderable
276 n.InnerHTML = utils.InnerHTML(*n)
277 tag, closing := utils.NodeToHTML(*n)
278 n.OuterHTML = tag + n.InnerHTML + closing
279 for i := range n.Children {
280 AddHTML(&n.Children[i])
281 }
282}
283
284func parseHTMLFromFile(path string) ([]string, []string, *html.Node) {
285 file, _ := os.Open(path)
286 defer file.Close()
287
288 scanner := bufio.NewScanner(file)
289 var htmlContent string
290
291 for scanner.Scan() {
292 htmlContent += scanner.Text() + "\n"
293 }
294
295 htmlContent = removeHTMLComments(htmlContent)
296
297 doc, _ := html.Parse(strings.NewReader(encapsulateText(removeWhitespaceBetweenTags(htmlContent))))
298
299 // Extract stylesheet link tags and style tags
300 stylesheets := extractStylesheets(doc, filepath.Dir(path))
301 styleTags := extractStyleTags(doc)
302
303 return stylesheets, styleTags, doc
304}
305
306func extractStylesheets(n *html.Node, baseDir string) []string {
307 var stylesheets []string
308
309 var dfs func(*html.Node)
310 dfs = func(node *html.Node) {
311 if node.Type == html.ElementNode && node.Data == "link" {
312 var href string
313 isStylesheet := false
314
315 for _, attr := range node.Attr {
316 if attr.Key == "rel" && attr.Val == "stylesheet" {
317 isStylesheet = true
318 } else if attr.Key == "href" {
319 href = attr.Val
320 }
321 }
322
323 if isStylesheet {
324 resolvedHref := localizePath(baseDir, href)
325 stylesheets = append(stylesheets, resolvedHref)
326 }
327 }
328
329 for c := node.FirstChild; c != nil; c = c.NextSibling {
330 dfs(c)
331 }
332 }
333
334 dfs(n)
335 return stylesheets
336}
337
338func extractStyleTags(n *html.Node) []string {
339 var styleTags []string
340
341 var dfs func(*html.Node)
342 dfs = func(node *html.Node) {
343 if node.Type == html.ElementNode && node.Data == "style" {
344 var styleContent strings.Builder
345 for c := node.FirstChild; c != nil; c = c.NextSibling {
346 if c.Type == html.TextNode {
347 styleContent.WriteString(c.Data)
348 }
349 }
350 styleTags = append(styleTags, styleContent.String())
351 }
352
353 for c := node.FirstChild; c != nil; c = c.NextSibling {
354 dfs(c)
355 }
356 }
357
358 dfs(n)
359 return styleTags
360}
361
362func localizePath(rootPath, filePath string) string {
363 // Check if the file path has a scheme, indicating it's a URL
364 u, err := url.Parse(filePath)
365 if err == nil && u.Scheme != "" {
366 return filePath
367 }
368
369 // Join the root path and the file path to create an absolute path
370 absPath := filepath.Join(rootPath, filePath)
371
372 // If the absolute path is the same as the original path, return it
373 if absPath == filePath {
374 return filePath
375 }
376
377 return "./" + absPath
378}
379
380func encapsulateText(htmlString string) string {
381 openOpen := regexp.MustCompile(`(<\w+[^>]*>)([^<]+)(<\w+[^>]*>)`)
382 closeOpen := regexp.MustCompile(`(</\w+[^>]*>)([^<]+)(<\w+[^>]*>)`)
383 closeClose := regexp.MustCompile(`(<\/\w+[^>]*>)([^<]+)(<\/\w+[^>]*>)`)
384 a := matchFactory(openOpen)
385 t := openOpen.ReplaceAllStringFunc(htmlString, a)
386 // fmt.Println(t)
387 b := matchFactory(closeOpen)
388 u := closeOpen.ReplaceAllStringFunc(t, b)
389 // fmt.Println(u)
390 c := matchFactory(closeClose)
391 v := closeClose.ReplaceAllStringFunc(u, c)
392 // fmt.Println(v)
393 return v
394}
395
396func matchFactory(re *regexp.Regexp) func(string) string {
397 return func(match string) string {
398 submatches := re.FindStringSubmatch(match)
399 if len(submatches) != 4 {
400 return match
401 }
402
403 // Process submatches
404 if len(removeWhitespace(submatches[2])) > 0 {
405 return submatches[1] + "<text>" + submatches[2] + "</text>" + submatches[3]
406 } else {
407 return match
408 }
409 }
410}
411func removeWhitespace(htmlString string) string {
412 // Remove extra white space
413 reSpaces := regexp.MustCompile(`\s+`)
414 htmlString = reSpaces.ReplaceAllString(htmlString, " ")
415
416 // Trim leading and trailing white space
417 htmlString = strings.TrimSpace(htmlString)
418
419 return htmlString
420}
421
422func removeHTMLComments(htmlString string) string {
423 re := regexp.MustCompile(`<!--[\s\S]*?-->`)
424 return re.ReplaceAllString(htmlString, "")
425}
426
427// important to allow the notspans to be injected, the spaces after removing the comments cause the regexp to fail
428func removeWhitespaceBetweenTags(html string) string {
429 // Create a regular expression to match spaces between angle brackets
430 re := regexp.MustCompile(`>\s+<`)
431 // Replace all matches of spaces between angle brackets with "><"
432 return re.ReplaceAllString(html, "><")
433}
434
435// Function to hash a struct using SHA-256
436func hashStruct(s interface{}) ([]byte, error) {
437 // Convert struct to JSON
438 jsonData, err := json.Marshal(s)
439 if err != nil {
440 return nil, err
441 }
442
443 // Hash the JSON data using SHA-256
444 hasher := sha256.New()
445 hasher.Write(jsonData)
446 hash := hasher.Sum(nil)
447
448 return hash, nil
449}