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/jwijenbergh/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// HoverHandler handles mouse hover events for focus-follows-mouse behavior.
22// It uses a debounce timer to avoid rapid focus switches.
23type HoverHandler struct {
24 motionCtrl *gtk.EventControllerMotion
25 paneID entity.PaneID
26 onEnter HoverCallback
27
28 // Callback retention: must stay reachable by Go GC.
29 enterCb func(gtk.EventControllerMotion, float64, float64)
30 leaveCb func(gtk.EventControllerMotion)
31
32 timer *time.Timer
33 timerMu sync.Mutex
34 canceled bool
35
36 ctx context.Context
37}
38
39// NewHoverHandler creates a new hover handler for a specific pane.
40func NewHoverHandler(ctx context.Context, paneID entity.PaneID) *HoverHandler {
41 log := logging.FromContext(ctx)
42 log.Debug().Str("pane_id", string(paneID)).Msg("creating hover handler")
43
44 return &HoverHandler{
45 ctx: ctx,
46 paneID: paneID,
47 }
48}
49
50// SetOnEnter sets the callback for when the pane should receive focus.
51func (h *HoverHandler) SetOnEnter(fn HoverCallback) {
52 h.onEnter = fn
53}
54
55// AttachTo attaches the hover handler to a GTK widget.
56func (h *HoverHandler) AttachTo(widget *gtk.Widget) {
57 log := logging.FromContext(h.ctx)
58
59 if widget == nil {
60 log.Error().Msg("cannot attach hover handler to nil widget")
61 return
62 }
63
64 h.motionCtrl = gtk.NewEventControllerMotion()
65 if h.motionCtrl == nil {
66 log.Error().Msg("failed to create motion controller")
67 return
68 }
69
70 // Connect enter handler with debounce (retain callbacks to prevent GC)
71 h.enterCb = func(_ gtk.EventControllerMotion, _ float64, _ float64) {
72 h.handleEnter()
73 }
74 h.motionCtrl.ConnectEnter(&h.enterCb)
75
76 // Connect leave handler to cancel pending focus
77 h.leaveCb = func(_ gtk.EventControllerMotion) {
78 h.handleLeave()
79 }
80 h.motionCtrl.ConnectLeave(&h.leaveCb)
81
82 // Add controller to widget
83 widget.AddController(&h.motionCtrl.EventController)
84
85 log.Debug().Str("pane_id", string(h.paneID)).Msg("hover handler attached to widget")
86}
87
88// handleEnter processes the mouse enter event with debouncing.
89func (h *HoverHandler) handleEnter() {
90 h.timerMu.Lock()
91 defer h.timerMu.Unlock()
92
93 // Cancel any existing timer
94 if h.timer != nil {
95 h.timer.Stop()
96 }
97
98 h.canceled = false
99
100 // Start new debounce timer
101 h.timer = time.AfterFunc(HoverFocusDelay, func() {
102 h.timerMu.Lock()
103 canceled := h.canceled
104 h.timerMu.Unlock()
105
106 if canceled {
107 return
108 }
109
110 if h.onEnter != nil {
111 h.onEnter(h.paneID)
112 }
113 })
114}
115
116// handleLeave cancels any pending focus switch.
117func (h *HoverHandler) handleLeave() {
118 h.timerMu.Lock()
119 defer h.timerMu.Unlock()
120
121 h.canceled = true
122 if h.timer != nil {
123 h.timer.Stop()
124 h.timer = nil
125 }
126}
127
128// Detach removes the hover handler.
129func (h *HoverHandler) Detach() {
130 h.timerMu.Lock()
131 if h.timer != nil {
132 h.timer.Stop()
133 h.timer = nil
134 }
135 h.timerMu.Unlock()
136
137 h.motionCtrl = nil
138 h.enterCb = nil
139 h.leaveCb = nil
140}