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