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