floating_pane.go RawHistoryFolder
1package component
2
3import (
4 "context"
5 "strings"
6 "sync"
7
8 "github.com/bnema/dumber/internal/ui/layout"
9)
10
11const defaultFloatingPaneURL = "about:blank"
12
13// FloatingPaneOptions controls floating pane behavior.
14type FloatingPaneOptions struct {
15 WidthPct float64
16 HeightPct float64
17 FallbackWidth int
18 FallbackHeight int
19 OnNavigate func(ctx context.Context, url string) error
20}
21
22// FloatingPane tracks persistent floating workspace pane state.
23type FloatingPane struct {
24 parent layout.OverlayWidget
25 widthPct float64
26 heightPct float64
27 fallbackWidth int
28 fallbackHeight int
29 onNavigate func(ctx context.Context, url string) error
30
31 mu sync.RWMutex
32 visible bool
33 omniboxVisible bool
34 sessionStarted bool
35 currentURL string
36 stateVersion uint64
37 calculatedWidth int
38 calculatedHeight int
39}
40
41// NewFloatingPane creates a floating pane state container.
42func NewFloatingPane(parent layout.OverlayWidget, opts FloatingPaneOptions) *FloatingPane {
43 if opts.WidthPct <= 0 || opts.WidthPct > 1 {
44 opts.WidthPct = 0.82
45 }
46 if opts.HeightPct <= 0 || opts.HeightPct > 1 {
47 opts.HeightPct = 0.72
48 }
49 if opts.FallbackWidth <= 0 {
50 opts.FallbackWidth = 1200
51 }
52 if opts.FallbackHeight <= 0 {
53 opts.FallbackHeight = 800
54 }
55
56 fp := &FloatingPane{
57 parent: parent,
58 widthPct: opts.WidthPct,
59 heightPct: opts.HeightPct,
60 fallbackWidth: opts.FallbackWidth,
61 fallbackHeight: opts.FallbackHeight,
62 onNavigate: opts.OnNavigate,
63 }
64 fp.Resize()
65 return fp
66}
67
68// ShowToggle toggles pane visibility; first open initializes a blank session.
69func (fp *FloatingPane) ShowToggle(ctx context.Context) error {
70 fp.mu.Lock()
71 previousVisible := fp.visible
72 previousOmniboxVisible := fp.omniboxVisible
73 if fp.visible {
74 fp.visible = false
75 fp.omniboxVisible = false
76 fp.stateVersion++
77 fp.mu.Unlock()
78 return nil
79 }
80
81 navigateToBlank := !fp.sessionStarted
82 showOmnibox := navigateToBlank || fp.currentURL == defaultFloatingPaneURL
83 fp.visible = true
84 fp.omniboxVisible = showOmnibox
85 fp.stateVersion++
86 rollbackVersion := fp.stateVersion
87 // Release lock before Navigate: callbacks may block and must not run while
88 // holding fp.mu. On error, rollback below restores visibility state.
89 fp.mu.Unlock()
90
91 if navigateToBlank {
92 if err := fp.Navigate(ctx, defaultFloatingPaneURL); err != nil {
93 fp.mu.Lock()
94 if fp.stateVersion == rollbackVersion {
95 fp.visible = previousVisible
96 fp.omniboxVisible = previousOmniboxVisible
97 fp.stateVersion++
98 }
99 fp.mu.Unlock()
100 return err
101 }
102 }
103
104 return nil
105}
106
107// Show makes the pane visible without navigation.
108// Use for profile sessions that already have a loaded URL.
109func (fp *FloatingPane) Show() {
110 fp.mu.Lock()
111 defer fp.mu.Unlock()
112 fp.visible = true
113 fp.omniboxVisible = false
114 fp.stateVersion++
115}
116
117// SessionStarted reports whether the floating pane has navigated at least once.
118func (fp *FloatingPane) SessionStarted() bool {
119 fp.mu.RLock()
120 defer fp.mu.RUnlock()
121 return fp.sessionStarted
122}
123
124// ShowURL shows the pane and navigates to a target URL.
125func (fp *FloatingPane) ShowURL(ctx context.Context, url string) error {
126 url = strings.TrimSpace(url)
127 if url == "" {
128 fp.mu.Lock()
129 fp.visible = true
130 fp.omniboxVisible = true
131 fp.stateVersion++
132 fp.mu.Unlock()
133 return nil
134 }
135
136 fp.mu.Lock()
137 previousVisible := fp.visible
138 previousOmniboxVisible := fp.omniboxVisible
139 fp.visible = true
140 fp.omniboxVisible = false
141 shouldNavigate := !fp.sessionStarted || fp.currentURL != url
142 fp.stateVersion++
143 rollbackVersion := fp.stateVersion
144 fp.mu.Unlock()
145
146 if shouldNavigate {
147 if err := fp.Navigate(ctx, url); err != nil {
148 fp.mu.Lock()
149 if fp.stateVersion == rollbackVersion {
150 fp.visible = previousVisible
151 fp.omniboxVisible = previousOmniboxVisible
152 fp.stateVersion++
153 }
154 fp.mu.Unlock()
155 return err
156 }
157 }
158
159 return nil
160}
161
162// Hide hides the pane while preserving web session state.
163func (fp *FloatingPane) Hide(_ context.Context) {
164 fp.mu.Lock()
165 defer fp.mu.Unlock()
166 fp.visible = false
167 fp.omniboxVisible = false
168 fp.stateVersion++
169}
170
171// Navigate loads a URL into the floating pane's persistent session.
172func (fp *FloatingPane) Navigate(ctx context.Context, url string) error {
173 url = strings.TrimSpace(url)
174 if url == "" {
175 return nil
176 }
177
178 fp.mu.RLock()
179 onNavigate := fp.onNavigate
180 fp.mu.RUnlock()
181
182 if onNavigate != nil {
183 if err := onNavigate(ctx, url); err != nil {
184 return err
185 }
186 }
187
188 fp.mu.Lock()
189 fp.currentURL = url
190 fp.sessionStarted = true
191 fp.stateVersion++
192 fp.mu.Unlock()
193
194 return nil
195}
196
197// Resize recalculates pane dimensions from parent overlay allocation.
198func (fp *FloatingPane) Resize() {
199 fp.mu.Lock()
200 defer fp.mu.Unlock()
201 fp.calculatedWidth, fp.calculatedHeight = CalculateOverlayDimensions(
202 fp.parent,
203 fp.widthPct,
204 fp.heightPct,
205 fp.fallbackWidth,
206 fp.fallbackHeight,
207 )
208}
209
210// SetParentOverlay updates the workspace overlay used for sizing calculations.
211func (fp *FloatingPane) SetParentOverlay(parent layout.OverlayWidget) {
212 fp.mu.Lock()
213 fp.parent = parent
214 fp.mu.Unlock()
215 fp.Resize()
216}
217
218// Dimensions returns the last calculated pane dimensions.
219func (fp *FloatingPane) Dimensions() (width, height int) {
220 fp.mu.RLock()
221 defer fp.mu.RUnlock()
222 return fp.calculatedWidth, fp.calculatedHeight
223}
224
225// CurrentURL returns the current URL loaded in the floating session.
226func (fp *FloatingPane) CurrentURL() string {
227 fp.mu.RLock()
228 defer fp.mu.RUnlock()
229 return fp.currentURL
230}
231
232// IsVisible reports whether the floating pane is currently visible.
233func (fp *FloatingPane) IsVisible() bool {
234 fp.mu.RLock()
235 defer fp.mu.RUnlock()
236 return fp.visible
237}
238
239// IsOmniboxVisible reports whether the floating pane omnibox is visible.
240func (fp *FloatingPane) IsOmniboxVisible() bool {
241 fp.mu.RLock()
242 defer fp.mu.RUnlock()
243 return fp.omniboxVisible
244}
245
246// SetOmniboxVisible updates omnibox visibility state for the floating pane.
247func (fp *FloatingPane) SetOmniboxVisible(visible bool) {
248 fp.mu.Lock()
249 fp.omniboxVisible = visible
250 fp.stateVersion++
251 fp.mu.Unlock()
252}
253
254// RecordLoadedURL updates floating pane URL state from external navigation events.
255func (fp *FloatingPane) RecordLoadedURL(url string) {
256 url = strings.TrimSpace(url)
257 if url == "" {
258 return
259 }
260
261 fp.mu.Lock()
262 fp.currentURL = url
263 fp.sessionStarted = true
264 fp.stateVersion++
265 fp.mu.Unlock()
266}