hover.go RawHistoryFolder
1package input
2
3import (
4 "context"
5 "sync"
6 "time"
7
8 "github.com/bnema/dumber/internal/domain/entity"
9 "github.com/bnema/dumber/internal/logging"
10 "github.com/bnema/puregotk/v4/gtk"
11)
12
13const (
14 // HoverFocusDelay is the delay before switching focus on hover.
15 HoverFocusDelay = 150 * time.Millisecond
16)
17
18// HoverCallback is called when a pane should receive focus from hover.
19type HoverCallback func(paneID entity.PaneID)
20
21// MotionCallback is called when intentional mouse movement is detected.
22type MotionCallback func()
23
24// HoverHandler handles mouse hover events for focus-follows-mouse behavior.
25// It uses a debounce timer to avoid rapid focus switches.
26type HoverHandler struct {
27 motionCtrl *gtk.EventControllerMotion
28 paneID entity.PaneID
29 onEnter HoverCallback
30 onMotion MotionCallback
31
32 // Callback retention: must stay reachable by Go GC.
33 enterCb func(gtk.EventControllerMotion, float64, float64)
34 leaveCb func(gtk.EventControllerMotion)
35 motionCb func(gtk.EventControllerMotion, float64, float64)
36
37 timer *time.Timer
38 timerMu sync.Mutex
39 canceled bool
40
41 ctx context.Context
42}
43
44// NewHoverHandler creates a new hover handler for a specific pane.
45func NewHoverHandler(ctx context.Context, paneID entity.PaneID) *HoverHandler {
46 log := logging.FromContext(ctx)
47 log.Debug().Str("pane_id", string(paneID)).Msg("creating hover handler")
48
49 return &HoverHandler{
50 ctx: ctx,
51 paneID: paneID,
52 }
53}
54
55// SetOnEnter sets the callback for when the pane should receive focus.
56func (h *HoverHandler) SetOnEnter(fn HoverCallback) {
57 h.onEnter = fn
58}
59
60// SetOnMotion sets the callback for intentional mouse movement.
61// This fires on actual mouse motion within the pane, not on synthetic
62// enter/leave events from GTK widget rearrangement.
63func (h *HoverHandler) SetOnMotion(fn MotionCallback) {
64 h.onMotion = fn
65}
66
67// AttachTo attaches the hover handler to a GTK widget.
68func (h *HoverHandler) AttachTo(widget *gtk.Widget) {
69 log := logging.FromContext(h.ctx)
70
71 if widget == nil {
72 log.Error().Msg("cannot attach hover handler to nil widget")
73 return
74 }
75
76 h.motionCtrl = gtk.NewEventControllerMotion()
77 if h.motionCtrl == nil {
78 log.Error().Msg("failed to create motion controller")
79 return
80 }
81
82 // Connect enter handler with debounce (retain callbacks to prevent GC)
83 h.enterCb = func(_ gtk.EventControllerMotion, _ float64, _ float64) {
84 h.handleEnter()
85 }
86 h.motionCtrl.ConnectEnter(&h.enterCb)
87
88 // Connect leave handler to cancel pending focus
89 h.leaveCb = func(_ gtk.EventControllerMotion) {
90 h.handleLeave()
91 }
92 h.motionCtrl.ConnectLeave(&h.leaveCb)
93
94 // Connect motion handler to detect intentional mouse movement.
95 // Unlike enter/leave, motion only fires when the cursor physically moves
96 // inside the widget — not on synthetic events from widget rearrangement.
97 h.motionCb = func(_ gtk.EventControllerMotion, _ float64, _ float64) {
98 if h.onMotion != nil {
99 h.onMotion()
100 }
101 }
102 h.motionCtrl.ConnectMotion(&h.motionCb)
103
104 // Add controller to widget
105 widget.AddController(&h.motionCtrl.EventController)
106
107 log.Debug().Str("pane_id", string(h.paneID)).Msg("hover handler attached to widget")
108}
109
110// handleEnter processes the mouse enter event with debouncing.
111func (h *HoverHandler) handleEnter() {
112 h.timerMu.Lock()
113 defer h.timerMu.Unlock()
114
115 // Cancel any existing timer
116 if h.timer != nil {
117 h.timer.Stop()
118 }
119
120 h.canceled = false
121
122 // Start new debounce timer
123 h.timer = time.AfterFunc(HoverFocusDelay, func() {
124 h.timerMu.Lock()
125 canceled := h.canceled
126 h.timerMu.Unlock()
127
128 if canceled {
129 return
130 }
131
132 if h.onEnter != nil {
133 h.onEnter(h.paneID)
134 }
135 })
136}
137
138// handleLeave cancels any pending focus switch.
139func (h *HoverHandler) handleLeave() {
140 h.Cancel()
141}
142
143// Cancel cancels any pending focus switch.
144// This can be called externally to cancel hover when keyboard navigation occurs.
145func (h *HoverHandler) Cancel() {
146 h.timerMu.Lock()
147 defer h.timerMu.Unlock()
148
149 h.canceled = true
150 if h.timer != nil {
151 h.timer.Stop()
152 h.timer = nil
153 }
154}
155
156// Detach removes the hover handler.
157func (h *HoverHandler) Detach() {
158 h.timerMu.Lock()
159 if h.timer != nil {
160 h.timer.Stop()
161 h.timer = nil
162 }
163 h.timerMu.Unlock()
164
165 h.motionCtrl = nil
166 h.enterCb = nil
167 h.leaveCb = nil
168 h.motionCb = nil
169}