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