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