package grim import ( "sort" "strconv" "strings" "unicode" ) type EventData struct { Position []int Click bool Context bool ScrollX int ScrollY int Key int KeyState bool Modifiers Modifiers } type Modifiers struct { CtrlKey bool ShiftKey bool MetaKey bool AltKey bool } type Monitor struct { Adapter *Adapter EventMap map[string]Event CSS *CSS Focus Focus Drag Drag } type Drag struct { Position []int Type string Id string } type Focus struct { Selected int LastClickWasFocused bool Nodes []string SoftFocused string } func (m *Monitor) RunEvents(n *Node) bool { var scrolled bool for _, v := range n.Children { r := m.RunEvents(v) if r { scrolled = r } } evt := m.EventMap[n.Properties.Id] evt.Target = n if scrolled { evt.ScrollX = 0 evt.ScrollY = 0 m.EventMap[n.Properties.Id] = evt } eventListeners := []string{} if evt.MouseDown { if n.OnMouseDown != nil { n.OnMouseDown(evt) } eventListeners = append(eventListeners, "mousedown") } if evt.MouseUp { if n.OnMouseUp != nil { n.OnMouseUp(evt) } eventListeners = append(eventListeners, "mouseup") } if evt.Click { if n.OnClick != nil { n.OnClick(evt) } eventListeners = append(eventListeners, "click") } if evt.ContextMenu { if n.OnContextMenu != nil { n.OnContextMenu(evt) } eventListeners = append(eventListeners, "contextmenu") } if evt.MouseEnter { if n.OnMouseEnter != nil { n.OnMouseEnter(evt) } eventListeners = append(eventListeners, "mouseenter") } if evt.MouseOver { if n.OnMouseOver != nil { n.OnMouseOver(evt) } eventListeners = append(eventListeners, "mouseover") } if evt.MouseLeave { if n.OnMouseLeave != nil { n.OnMouseLeave(evt) } eventListeners = append(eventListeners, "mouseleave") } if evt.MouseMove { if n.OnMouseMove != nil { n.OnMouseMove(evt) } eventListeners = append(eventListeners, "mousemove") } if evt.Hover != n.hovered { if evt.Hover { n.hovered = true } else { n.hovered = false } ConditionalStyleHandler(n, map[string]string{}) } if len(m.Focus.Nodes) > 0 && m.Focus.Selected > -1 { if m.Focus.Nodes[m.Focus.Selected] == n.Properties.Id { if n.focused == false { n.Focus() } } else { if n.focused == true { n.Blur() } } } else { if n.focused == true { n.Blur() } } left, top := n.GetScroll() if evt.ScrollX != 0 { if n.ComputedStyle["overflow"] == "auto" || n.ComputedStyle["overflow"] == "scroll" || n.ComputedStyle["overflow-x"] == "auto" || n.ComputedStyle["overflow-x"] == "scroll" { s := m.CSS.State self := s[n.Properties.Id] containerWidth := self.Width left += evt.ScrollX if (int((float32(int(left))/((containerWidth/float32(n.scrollWidth))*containerWidth))*containerWidth) + int(containerWidth)) >= n.scrollWidth { left = (((n.scrollWidth) - int(containerWidth)) * int(containerWidth)) / n.scrollWidth } if left <= 0 { left = 0 } if n.OnScroll != nil { n.OnScroll(evt) } evt.ScrollX = 0 m.EventMap[n.Properties.Id] = evt scrolled = true } } if evt.ScrollY != 0 { if n.ComputedStyle["overflow"] == "auto" || n.ComputedStyle["overflow"] == "scroll" || n.ComputedStyle["overflow-y"] == "auto" || n.ComputedStyle["overflow-y"] == "scroll" { s := m.CSS.State self := s[n.Properties.Id] containerHeight := self.Height top -= evt.ScrollY // This is the scroll scaling equation if it is less than the scroll height then let it add the next scroll amount if (int((float32(int(top))/((containerHeight/float32(n.scrollHeight))*containerHeight))*containerHeight) + int(containerHeight)) >= n.scrollHeight { top = (((n.scrollHeight) - int(containerHeight)) * int(containerHeight)) / n.scrollHeight } if top <= 0 { top = 0 } if n.OnScroll != nil { n.OnScroll(evt) } evt.ScrollY = 0 m.EventMap[n.Properties.Id] = evt scrolled = true } } if scrolled { n.ScrollTo(left, top) } for _, v := range eventListeners { if len(n.Properties.EventListeners[v]) > 0 { for _, handler := range n.Properties.EventListeners[v] { handler(evt) } } } return scrolled } type fn struct { Id string TabIndex int } func (m *Monitor) GetEvents(data *EventData) { headElements := []string{ "head", "title", // Defines the title of the document "base", // Specifies the base URL for all relative URLs in the document "link", // Links to external resources like stylesheets "meta", // Provides metadata about the document (e.g., character set, viewport) "style", // Embeds internal CSS styles "script", // Embeds or references JavaScript code "noscript", // Provides alternate content for users without JavaScript "template", // Used to define a client-side template } s := m.CSS.State m.Focus.LastClickWasFocused = false // update focesable nodes nodes := []fn{} for k, self := range s { if self.TabIndex > -1 { nodes = append(nodes, fn{Id: k, TabIndex: self.TabIndex}) } } sort.Slice(nodes, func(i, j int) bool { return nodes[i].TabIndex < nodes[j].TabIndex // Ascending order by TabIndex }) m.Focus.Nodes = []string{} for _, v := range nodes { good := true for _, tag := range headElements { if len(v.Id) >= len(tag) { if v.Id[0:len(tag)] == tag { good = false } } } if good { m.Focus.Nodes = append(m.Focus.Nodes, v.Id) } } if m.Drag.Position == nil { m.Drag = Drag{Position: []int{-1, -1}} } drag := false if m.Drag.Position != nil { if m.Drag.Position[0] > -1 && m.Drag.Position[1] > -1 { // !ISSUE: it should only fire on only draggable drag = true } } if data.Position == nil { return } // if !data.Click { // m.Drag.Id = "" // } var softFocus string for k, self := range s { var isMouseOver, isFocused bool if m.Focus.Selected > -1 { isFocused = m.Focus.Nodes[m.Focus.Selected] == k } else if m.Focus.SoftFocused != "" { isFocused = m.Focus.SoftFocused == k } else { isFocused = false } evt, ok := m.EventMap[k] if !ok { evt = Event{} } boxLeft := self.X - self.Border.Left.Width boxRight := self.X + self.Width + self.Border.Left.Width + self.Border.Right.Width boxTop := self.Y - self.Border.Top.Width boxBottom := self.Y + self.Height + self.Border.Top.Width + self.Border.Bottom.Width insideX := (boxLeft < float32(data.Position[0]) && boxRight > float32(data.Position[0])) insideY := (boxTop < float32(data.Position[1]) && boxBottom > float32(data.Position[1])) inside := (insideX && insideY) arrowScrollX := 0 arrowScrollY := 0 if m.Focus.SoftFocused == k || inside { if data.Key == 265 { // up arrowScrollY += 20 } else if data.Key == 264 { // Down arrowScrollY -= 20 } if data.Key == 262 { // up arrowScrollX += 20 } else if data.Key == 263 { // Down arrowScrollX -= 20 } } if isFocused { // This allows for arrow scrolling when off the element and typing // Get the keycode of the pressed key if data.Key != 0 { if self.ContentEditable { // Sync the innertext and value but idk ProcessText(self, int(data.Key)) evt.Value = self.Value } } if data.Key == 258 && data.KeyState && !m.Focus.LastClickWasFocused { // Tab mfsLen := len(m.Focus.Nodes) if mfsLen > 0 { store := m.Focus.Selected m.Focus.Selected += 1 if m.Focus.Selected >= mfsLen { m.Focus.Selected = 0 } if store != m.Focus.Selected { m.Focus.LastClickWasFocused = true } } } } if inside || isFocused { // Mouse is over element isMouseOver = true evt.Hover = true if data.Click && !evt.MouseDown { evt.MouseDown = true evt.MouseUp = false if m.Drag.Position[0] == -1 && m.Drag.Position[1] == -1 { if strings.Contains(k, "grim-thumb") { t := "" if strings.Contains(k, "grim-thumbx") { t = "x" } else if strings.Contains(k, "grim-thumby") { t = "y" } key := k parts := strings.Split(k, ":") for i, v := range parts { if strings.Contains(v, "grim-track") { key = strings.Join(parts[0:i], ":") break } } m.Drag = Drag{Position: data.Position, Type: t, Id: key, } } } } if !data.Click && !evt.MouseUp { evt.MouseUp = true evt.MouseDown = false evt.Click = false m.Drag = Drag{Position: []int{-1, -1}, Id: ""} m.Focus.SoftFocused = "" } if data.Click && !evt.Click && !drag { evt.Click = true if !inside && !(self.TabIndex > -1) { if m.Focus.SoftFocused == k { m.Focus.SoftFocused = "" softFocus = "" } } if self.TabIndex > -1 { if m.Focus.Selected > -1 { if len(m.Focus.Nodes) > 0 && m.Focus.Selected > -1 { if m.Focus.Nodes[m.Focus.Selected] != k { for i, v := range m.Focus.Nodes { if v == k { m.Focus.Selected = i m.Focus.LastClickWasFocused = true break } } } else { m.Focus.LastClickWasFocused = true } } else { m.Focus.LastClickWasFocused = true } } else { selectedIndex := -1 for i, v := range m.Focus.Nodes { if v == k { selectedIndex = i } } if selectedIndex == -1 { nodes = append(nodes, fn{Id: k, TabIndex: self.TabIndex}) sort.Slice(nodes, func(i, j int) bool { return nodes[i].TabIndex < nodes[j].TabIndex // Ascending order by TabIndex }) m.Focus.Nodes = []string{} for _, v := range nodes { m.Focus.Nodes = append(m.Focus.Nodes, v.Id) } for i, v := range m.Focus.Nodes { if v == k { selectedIndex = i } } } m.Focus.Selected = selectedIndex m.Focus.LastClickWasFocused = true } } else if m.Focus.Selected > -1 { if len(m.Focus.Nodes) > 0 && m.Focus.Selected > -1 { if m.Focus.Nodes[m.Focus.Selected] != k && !m.Focus.LastClickWasFocused { m.Focus.Selected = -1 } } } if inside && m.Focus.Selected == -1 { if softFocus == "" { softFocus = k } else { if s[softFocus].Z < s[k].Z { softFocus = k } else if s[softFocus].Z == s[k].Z { if extractNumber(k) < extractNumber(softFocus) { softFocus = k } } } } } // Regardless set soft focus to trigger events to the selected element: when non is set default body??? if data.Context { evt.ContextMenu = true } if (data.ScrollY != 0 && (inside)) || (data.ScrollX != 0 && (inside)) || arrowScrollX != 0 || arrowScrollY != 0 || drag { if drag && m.Drag.Id != "" { e := m.EventMap[m.Drag.Id] if m.Drag.Type == "y" { e.ScrollY = (evt.Y - data.Position[1]) } else if m.Drag.Type == "x" { e.ScrollX = -(evt.X - data.Position[0]) } m.EventMap[m.Drag.Id] = e evt.X = data.Position[0] evt.Y = data.Position[1] } else { if data.Modifiers.ShiftKey { evt.ScrollX = -data.ScrollY } else { evt.ScrollX = data.ScrollX + arrowScrollX evt.ScrollY = data.ScrollY + arrowScrollY } if strings.Contains(k, "grim-thumb") { data.ScrollX = 0 data.ScrollY = 0 } } } if !evt.MouseEnter && inside { evt.MouseEnter = true evt.MouseOver = true evt.MouseLeave = false // Let the adapter know the cursor has changed m.Adapter.DispatchEvent(Event{ Name: "cursor", Data: self.Cursor, }) } if inside { evt.MouseMove = true evt.X = data.Position[0] evt.Y = data.Position[1] } else { evt.MouseMove = true } } else { isMouseOver = false evt.Hover = false } if !isMouseOver && !evt.MouseLeave { evt.MouseEnter = false evt.MouseOver = false evt.MouseLeave = true // n.Properties.Hover = false } if evt.X != int(data.Position[0]) && evt.Y != int(data.Position[1]) { evt.X = int(data.Position[0]) evt.Y = int(data.Position[1]) } m.EventMap[k] = evt } if softFocus != "" { m.Focus.SoftFocused = softFocus } } // ProcessText processes key events for text entry. func processText(n *Node, key int) { // !ISSUE: Who owns the text value or innerText? // Handle key events for text entry switch key { case 8: // Backspace: remove the last character if len(n.innerText) > 0 { n.innerText = n.innerText[:len(n.InnerText)-1] } if len(n.value) > 0 { n.value = n.value[:len(n.value)-1] } case 65: // !TODO: ctrl a // // Select All: set the entire text as selected // if key == 17 || key == 345 { // n.Properties.Selected = []float32{0, float32(len(n.Value))} // } else { // // Otherwise, append 'A' to the text // n.Value += "A" // } case 67: // Copy: copy the selected text (in this case, print it) // if key == 17 || key == 345 { // } else { // // Otherwise, append 'C' to the text // n.Value += "C" // } case 86: // Paste: paste the copied text (in this case, set it to "Pasted") // if key == 17 || key == 345 { // n.Value = "Pasted" // } else { // // Otherwise, append 'V' to the text // n.Value += "V" // } default: // Record other key presses n.value += string(rune(key)) n.innerText += string(rune(key)) } } func extractNumber(input string) int { var numStr string for _, char := range input { if unicode.IsDigit(char) { numStr += string(char) } } if numStr == "" { return 0 } n, _ := strconv.Atoi(numStr) return n } func calculateEvents(root *Node, data *EventData) []*Node { if root == nil { return make(map[int]string) } stack := []*Node{root} visited := make(map[*Node]bool) focusableElements := []*Node{} // This allows us to loop through the tree without recursion for len(stack) > 0 { currentNode := stack[len(stack)-1] stack = stack[:len(stack)-1] if !visited[currentNode] { nodeHasEvent := false eventListeners := []string{} visited[currentNode] = true // Grab the state data self := state[currentNode.Properties.Id] // Here is where the logic can happen inside := isInside(currentNode.Properties.Id, self) // In ./main.go createNode we set all elements without a tab index to -1 // so this lets us know if the element is focusable if currentNode.tabIndex != -1 { focusableElements = append(focusableElements, currentNode) } // There can only be one focused node at a time so if the current element // is focused then we add text to it if the key value isn't 0 (no data) if currentNode.focused { // Get the keycode of the pressed key if data.Key != 0 { // Check if content editable (all inputs are, the value prop is a reflection of innerText) if currentNode.ContentEditable { // Sync the innertext and value but idk processText(currentNode, int(data.Key)) nodeHasEvent = true } } } // Handle the events for the mouse if inside { if !n.hovered { // if the element wasn't prevously hovered then the mouse just moved over if n.OnMouseEnter != nil { n.OnMouseEnter(evt) nodeHasEvent = true } eventListeners = append(eventListeners, "mouseenter") if n.OnMouseOver != nil { n.OnMouseOver(evt) nodeHasEvent = true } eventListeners = append(eventListeners, "mouseover") } // regardless we will fire mousemove if n.OnMouseMove != nil { n.OnMouseMove(evt) nodeHasEvent = true } eventListeners = append(eventListeners, "mousemove") n.hovered = true } else { // if the element was hovered but the mouse is not inside if n.hovered { if n.OnMouseLeave != nil { n.OnMouseLeave(evt) nodeHasEvent = true } eventListeners = append(eventListeners, "mouseleave") } n.hovered = false } if data.Click { // !ISSUE: IDK IF THIS IS RIGHT if inside { // Don't double fire if !n.focused { n.Focus() } } else { if n.focused { n.Blur() } } } // Handle all of the AddEventListeners for _, v := range eventListeners { if len(n.Properties.EventListeners[v]) > 0 { nodeHasEvent = true for _, handler := range n.Properties.EventListeners[v] { handler(evt) } } } // Push children onto the stack in reverse order to maintain a left-to-right processing (if desired) for i := len(currentNode.Children) - 1; i >= 0; i-- { if currentNode.Children[i] != nil { stack = append(stack, currentNode.Children[i]) } } } } if data.Key == 258 && data.KeyState { // Tab // check if a element is focused, then change the focus to the next tabindex // run the .Focus and .Blur } return properties } func isInside(key string, self *State) bool { // Check to see if the mouse is inside the element or the element is focused boxLeft := self.X - self.Border.Left.Width boxRight := self.X + self.Width + self.Border.Left.Width + self.Border.Right.Width boxTop := self.Y - self.Border.Top.Width boxBottom := self.Y + self.Height + self.Border.Top.Width + self.Border.Bottom.Width insideX := (boxLeft < float32(data.Position[0]) && boxRight > float32(data.Position[0])) insideY := (boxTop < float32(data.Position[1]) && boxBottom > float32(data.Position[1])) inside := (insideX && insideY) return inside }