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.FPSCounterOn = 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 // could use a return value that indicates whether or not a event has ran to ramp/deramp fps based on activity
196
197 events.RunEvents(eventStore)
198 // ran := events.RunEvents(eventStore)
199
200 // if !ran {
201 // if wm.FPS < 60 {
202 // wm.SetFSP(wm.FPS + 1)
203 // }
204 // } else {
205 // if wm.FPS > 10 {
206 // wm.SetFSP(wm.FPS - 1)
207 // }
208 // }
209
210 rl.EndDrawing()
211 }
212}
213
214func CopyNode(c cstyle.CSS, node element.Node, parent *element.Node) element.Node {
215 n := element.Node{}
216 n.TagName = node.TagName
217 n.InnerText = node.InnerText
218 n.Style = node.Style
219 n.Id = node.Id
220 n.ClassList = node.ClassList
221 n.Href = node.Href
222 n.Src = node.Src
223 n.Title = node.Title
224 n.Attribute = node.Attribute
225 n.Value = node.Value
226 n.ScrollY = node.ScrollY
227 n.InnerHTML = node.InnerHTML
228 n.OuterHTML = node.OuterHTML
229 n.Properties.Id = node.Properties.Id
230 n.Properties.Focusable = node.Properties.Focusable
231 n.Properties.Focused = node.Properties.Focused
232 n.Properties.Editable = node.Properties.Editable
233 n.Properties.Hover = node.Properties.Hover
234 n.Properties.Selected = node.Properties.Selected
235
236 n.Parent = parent
237
238 n.Style = c.GetStyles(n)
239
240 for _, v := range node.Children {
241 n.Children = append(n.Children, CopyNode(c, v, &n))
242 }
243 return n
244}
245
246func CreateNode(node *html.Node, parent *element.Node) {
247 if node.Type == html.ElementNode {
248 newNode := parent.CreateElement(node.Data)
249 for _, attr := range node.Attr {
250 if attr.Key == "class" {
251 classes := strings.Split(attr.Val, " ")
252 for _, class := range classes {
253 newNode.ClassList.Add(class)
254 }
255 } else if attr.Key == "id" {
256 newNode.Id = attr.Val
257 } else if attr.Key == "contenteditable" && (attr.Val == "" || attr.Val == "true") {
258 newNode.Properties.Editable = true
259 } else if attr.Key == "href" {
260 newNode.Href = attr.Val
261 } else if attr.Key == "src" {
262 newNode.Src = attr.Val
263 } else if attr.Key == "title" {
264 newNode.Title = attr.Val
265 } else {
266 newNode.SetAttribute(attr.Key, attr.Val)
267 }
268 }
269 newNode.InnerText = strings.TrimSpace(utils.GetInnerText(node))
270 // Recursively traverse child nodes
271 for child := node.FirstChild; child != nil; child = child.NextSibling {
272 if child.Type == html.ElementNode {
273 CreateNode(child, &newNode)
274 }
275 }
276 parent.AppendChild(newNode)
277
278 } else {
279 for child := node.FirstChild; child != nil; child = child.NextSibling {
280 if child.Type == html.ElementNode {
281 CreateNode(child, parent)
282 }
283 }
284 }
285}
286
287func AddHTML(n *element.Node) {
288 // Head is not renderable
289 n.InnerHTML = utils.InnerHTML(*n)
290 tag, closing := utils.NodeToHTML(*n)
291 n.OuterHTML = tag + n.InnerHTML + closing
292 for i := range n.Children {
293 AddHTML(&n.Children[i])
294 }
295}
296
297func parseHTMLFromFile(path string) ([]string, []string, *html.Node) {
298 file, _ := os.Open(path)
299 defer file.Close()
300
301 scanner := bufio.NewScanner(file)
302 var htmlContent string
303
304 for scanner.Scan() {
305 htmlContent += scanner.Text() + "\n"
306 }
307
308 htmlContent = removeHTMLComments(htmlContent)
309
310 doc, _ := html.Parse(strings.NewReader(encapsulateText(removeWhitespaceBetweenTags(htmlContent))))
311
312 // Extract stylesheet link tags and style tags
313 stylesheets := extractStylesheets(doc, filepath.Dir(path))
314 styleTags := extractStyleTags(doc)
315
316 return stylesheets, styleTags, doc
317}
318
319func extractStylesheets(n *html.Node, baseDir string) []string {
320 var stylesheets []string
321
322 var dfs func(*html.Node)
323 dfs = func(node *html.Node) {
324 if node.Type == html.ElementNode && node.Data == "link" {
325 var href string
326 isStylesheet := false
327
328 for _, attr := range node.Attr {
329 if attr.Key == "rel" && attr.Val == "stylesheet" {
330 isStylesheet = true
331 } else if attr.Key == "href" {
332 href = attr.Val
333 }
334 }
335
336 if isStylesheet {
337 resolvedHref := localizePath(baseDir, href)
338 stylesheets = append(stylesheets, resolvedHref)
339 }
340 }
341
342 for c := node.FirstChild; c != nil; c = c.NextSibling {
343 dfs(c)
344 }
345 }
346
347 dfs(n)
348 return stylesheets
349}
350
351func extractStyleTags(n *html.Node) []string {
352 var styleTags []string
353
354 var dfs func(*html.Node)
355 dfs = func(node *html.Node) {
356 if node.Type == html.ElementNode && node.Data == "style" {
357 var styleContent strings.Builder
358 for c := node.FirstChild; c != nil; c = c.NextSibling {
359 if c.Type == html.TextNode {
360 styleContent.WriteString(c.Data)
361 }
362 }
363 styleTags = append(styleTags, styleContent.String())
364 }
365
366 for c := node.FirstChild; c != nil; c = c.NextSibling {
367 dfs(c)
368 }
369 }
370
371 dfs(n)
372 return styleTags
373}
374
375func localizePath(rootPath, filePath string) string {
376 // Check if the file path has a scheme, indicating it's a URL
377 u, err := url.Parse(filePath)
378 if err == nil && u.Scheme != "" {
379 return filePath
380 }
381
382 // Join the root path and the file path to create an absolute path
383 absPath := filepath.Join(rootPath, filePath)
384
385 // If the absolute path is the same as the original path, return it
386 if absPath == filePath {
387 return filePath
388 }
389
390 return "./" + absPath
391}
392
393func encapsulateText(htmlString string) string {
394 openOpen := regexp.MustCompile(`(<\w+[^>]*>)([^<]+)(<\w+[^>]*>)`)
395 closeOpen := regexp.MustCompile(`(</\w+[^>]*>)([^<]+)(<\w+[^>]*>)`)
396 closeClose := regexp.MustCompile(`(<\/\w+[^>]*>)([^<]+)(<\/\w+[^>]*>)`)
397 a := matchFactory(openOpen)
398 t := openOpen.ReplaceAllStringFunc(htmlString, a)
399 // fmt.Println(t)
400 b := matchFactory(closeOpen)
401 u := closeOpen.ReplaceAllStringFunc(t, b)
402 // fmt.Println(u)
403 c := matchFactory(closeClose)
404 v := closeClose.ReplaceAllStringFunc(u, c)
405 // fmt.Println(v)
406 return v
407}
408
409func matchFactory(re *regexp.Regexp) func(string) string {
410 return func(match string) string {
411 submatches := re.FindStringSubmatch(match)
412 if len(submatches) != 4 {
413 return match
414 }
415
416 // Process submatches
417 if len(removeWhitespace(submatches[2])) > 0 {
418 return submatches[1] + "<text>" + submatches[2] + "</text>" + submatches[3]
419 } else {
420 return match
421 }
422 }
423}
424func removeWhitespace(htmlString string) string {
425 // Remove extra white space
426 reSpaces := regexp.MustCompile(`\s+`)
427 htmlString = reSpaces.ReplaceAllString(htmlString, " ")
428
429 // Trim leading and trailing white space
430 htmlString = strings.TrimSpace(htmlString)
431
432 return htmlString
433}
434
435func removeHTMLComments(htmlString string) string {
436 re := regexp.MustCompile(`<!--[\s\S]*?-->`)
437 return re.ReplaceAllString(htmlString, "")
438}
439
440// important to allow the notspans to be injected, the spaces after removing the comments cause the regexp to fail
441func removeWhitespaceBetweenTags(html string) string {
442 // Create a regular expression to match spaces between angle brackets
443 re := regexp.MustCompile(`>\s+<`)
444 // Replace all matches of spaces between angle brackets with "><"
445 return re.ReplaceAllString(html, "><")
446}
447
448// Function to hash a struct using SHA-256
449func hashStruct(s interface{}) ([]byte, error) {
450 // Convert struct to JSON
451 jsonData, err := json.Marshal(s)
452 if err != nil {
453 return nil, err
454 }
455
456 // Hash the JSON data using SHA-256
457 hasher := sha256.New()
458 hasher.Write(jsonData)
459 hash := hasher.Sum(nil)
460
461 return hash, nil
462}