link_status.go RawHistoryFolder
1// Package component provides UI components for the browser.
2package component
3
4import (
5 "sync"
6
7 "github.com/bnema/dumber/internal/ui/mainloop"
8 "github.com/bnema/puregotk/v4/glib"
9 "github.com/bnema/puregotk/v4/gtk"
10
11 "github.com/bnema/dumber/internal/ui/layout"
12)
13
14const (
15 // linkStatusShowDelayMs is the delay before showing the overlay to avoid flicker.
16 linkStatusShowDelayMs = 100
17 // linkStatusAutoHideMs is the delay before auto-hiding the overlay (e.g., during fullscreen video).
18 linkStatusAutoHideMs = 10000
19 // linkStatusMaxChars is the maximum characters before truncation.
20 linkStatusMaxChars = 80
21)
22
23// LinkStatusOverlay displays the destination URL when hovering over links.
24// It appears in the bottom-left corner with a fade-in/fade-out transition.
25type LinkStatusOverlay struct {
26 factory layout.WidgetFactory
27 container layout.BoxWidget
28 label layout.LabelWidget
29
30 pendingURI string // URI waiting to be shown after delay
31 showTimer uint // GLib timer for delayed show
32 autoHideTimer uint // GLib timer for auto-hide after timeout
33 visible bool // Whether overlay is currently visible (has .visible class)
34 showTimerCb glib.SourceFunc
35 autoHideCb glib.SourceFunc
36 coalescer *mainloop.Coalescer
37 mu sync.Mutex
38}
39
40// NewLinkStatusOverlay creates a new link status overlay component.
41// The overlay is positioned at bottom-left with fade transitions via CSS.
42func NewLinkStatusOverlay(factory layout.WidgetFactory) *LinkStatusOverlay {
43 // Create container box for styling and positioning
44 container := factory.NewBox(layout.OrientationHorizontal, 0)
45 container.AddCssClass("link-status")
46
47 // Position at bottom-left
48 container.SetHalign(gtk.AlignStartValue)
49 container.SetValign(gtk.AlignEndValue)
50
51 // Don't expand to fill space
52 container.SetHexpand(false)
53 container.SetVexpand(false)
54
55 // Don't intercept pointer events - let clicks pass through
56 container.SetCanTarget(false)
57 container.SetCanFocus(false)
58
59 // Create label for URL text
60 label := factory.NewLabel("")
61 label.SetCanTarget(false)
62 label.SetCanFocus(false)
63 label.SetEllipsize(layout.EllipsizeMiddle) // Truncate in middle for URLs
64 label.SetMaxWidthChars(linkStatusMaxChars)
65 container.Append(label)
66
67 overlay := &LinkStatusOverlay{
68 factory: factory,
69 container: container,
70 label: label,
71 visible: false,
72 }
73 overlay.coalescer = mainloop.NewCoalescer(func(fn func()) {
74 var cb glib.SourceFunc = func(uintptr) bool {
75 fn()
76 return false
77 }
78 glib.IdleAdd(&cb, 0)
79 })
80 overlay.showTimerCb = func(_ uintptr) bool {
81 overlay.mu.Lock()
82 defer overlay.mu.Unlock()
83
84 if overlay.pendingURI != "" {
85 overlay.label.SetText(overlay.pendingURI)
86 if !overlay.visible {
87 overlay.visible = true
88 overlay.container.AddCssClass("visible")
89 }
90 overlay.resetAutoHideTimerLocked()
91 }
92 overlay.showTimer = 0
93 return false
94 }
95 overlay.autoHideCb = func(_ uintptr) bool {
96 overlay.mu.Lock()
97 defer overlay.mu.Unlock()
98
99 overlay.autoHideTimer = 0
100 overlay.hide()
101 return false
102 }
103
104 return overlay
105}
106
107// Show displays the link status overlay with the given URI.
108// If uri is empty, hides the overlay instead.
109// Uses a small delay to avoid flicker during rapid mouse movement.
110func (l *LinkStatusOverlay) Show(uri string) {
111 l.mu.Lock()
112 defer l.mu.Unlock()
113
114 // Cancel any pending show timer
115 if l.showTimer != 0 {
116 glib.SourceRemove(l.showTimer)
117 l.showTimer = 0
118 }
119
120 if uri == "" {
121 l.hide()
122 return
123 }
124
125 l.pendingURI = uri
126
127 // Coalesce rapid hover events before scheduling delay timer.
128 l.coalescer.Post("link-status-show-delay", func() {
129 l.mu.Lock()
130 defer l.mu.Unlock()
131
132 if l.pendingURI == "" {
133 return
134 }
135 if l.showTimer != 0 {
136 glib.SourceRemove(l.showTimer)
137 }
138 l.showTimer = glib.TimeoutAdd(linkStatusShowDelayMs, &l.showTimerCb, 0)
139 })
140}
141
142// Hide manually hides the link status overlay.
143func (l *LinkStatusOverlay) Hide() {
144 l.mu.Lock()
145 defer l.mu.Unlock()
146 l.hide()
147}
148
149// hide performs the actual hide operation (must be called with lock held).
150func (l *LinkStatusOverlay) hide() {
151 // Cancel pending show timer
152 if l.showTimer != 0 {
153 glib.SourceRemove(l.showTimer)
154 l.showTimer = 0
155 }
156 // Cancel auto-hide timer
157 if l.autoHideTimer != 0 {
158 glib.SourceRemove(l.autoHideTimer)
159 l.autoHideTimer = 0
160 }
161 l.pendingURI = ""
162
163 if l.visible {
164 l.visible = false
165 l.container.RemoveCssClass("visible")
166 }
167}
168
169// resetAutoHideTimerLocked starts or resets the auto-hide timer (must be called with lock held).
170func (l *LinkStatusOverlay) resetAutoHideTimerLocked() {
171 // Cancel existing auto-hide timer
172 if l.autoHideTimer != 0 {
173 glib.SourceRemove(l.autoHideTimer)
174 l.autoHideTimer = 0
175 }
176
177 // Coalesce timer resets while pointer moves quickly across links.
178 l.coalescer.Post("link-status-auto-hide", func() {
179 l.mu.Lock()
180 defer l.mu.Unlock()
181
182 if l.pendingURI == "" {
183 return
184 }
185 if l.autoHideTimer != 0 {
186 glib.SourceRemove(l.autoHideTimer)
187 }
188 l.autoHideTimer = glib.TimeoutAdd(linkStatusAutoHideMs, &l.autoHideCb, 0)
189 })
190}
191
192// Widget returns the underlying widget for embedding in overlays.
193func (l *LinkStatusOverlay) Widget() layout.Widget {
194 return l.container
195}
196
197// IsVisible returns whether the overlay is currently visible.
198func (l *LinkStatusOverlay) IsVisible() bool {
199 l.mu.Lock()
200 defer l.mu.Unlock()
201 return l.visible
202}
203
204// Cleanup cancels any pending timers and clears state.
205// Must be called before removing the overlay from the UI.
206func (l *LinkStatusOverlay) Cleanup() {
207 l.mu.Lock()
208 defer l.mu.Unlock()
209
210 // Cancel pending show timer to prevent use-after-free
211 if l.showTimer != 0 {
212 glib.SourceRemove(l.showTimer)
213 l.showTimer = 0
214 }
215 // Cancel auto-hide timer
216 if l.autoHideTimer != 0 {
217 glib.SourceRemove(l.autoHideTimer)
218 l.autoHideTimer = 0
219 }
220 if l.coalescer != nil {
221 l.coalescer.Destroy()
222 }
223 l.pendingURI = ""
224 l.visible = false
225}