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