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