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