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