1package ui
2
3import (
4 "context"
5 "fmt"
6 "strings"
7 "unsafe"
8
9 "github.com/bnema/dumber/internal/application/port"
10 "github.com/bnema/dumber/internal/domain/entity"
11 "github.com/bnema/dumber/internal/ui/component"
12 "github.com/bnema/dumber/internal/ui/layout"
13 "github.com/bnema/puregotk/v4/gtk"
14)
15
16type floatingWorkspaceSession struct {
17 paneID entity.PaneID
18 pane *component.FloatingPane
19 paneView *component.PaneView
20 webView port.WebView
21 overlay layout.OverlayWidget
22 widget layout.Widget
23 focusWidget layout.Widget
24 omnibox *component.Omnibox
25 omniboxWidget layout.Widget
26 resizeWatcherActive bool
27 // resizeTickCallback keeps the purego callback live until its GTK source is
28 // removed. It is cleared exactly once with resizeTickID.
29 resizeTickCallback *gtk.TickCallback
30 resizeTickID uint
31 appliedWidth int
32 appliedHeight int
33}
34
35type floatingSessionKey struct {
36 tabID entity.TabID
37 sessionID string
38}
39
40func normalizeFloatingSessionID(sessionID string) string {
41 sessionID = strings.TrimSpace(sessionID)
42 if sessionID == "" {
43 return floatingSessionIDDefault
44 }
45 return sessionID
46}
47
48func floatingPaneIDForSession(tabID entity.TabID, sessionID string) entity.PaneID {
49 return entity.PaneID(floatingPaneIDPrefix + string(tabID) + ":" + normalizeFloatingSessionID(sessionID))
50}
51
52func floatingSessionMapKey(tabID entity.TabID, sessionID string) floatingSessionKey {
53 return floatingSessionKey{tabID: tabID, sessionID: normalizeFloatingSessionID(sessionID)}
54}
55
56func decorateFloatingOverlay(overlay layout.OverlayWidget) {
57 if overlay == nil {
58 return
59 }
60
61 overlay.SetHexpand(false)
62 overlay.SetVexpand(false)
63 overlay.AddCssClass("floating-pane-container")
64 overlay.AddCssClass("pane-border")
65 overlay.AddCssClass("pane-active")
66}
67
68// showFloatingWidget makes a floating pane widget interactive and visible.
69// Visibility is controlled by CSS class to animate opacity while keeping the
70// widget mapped so WebKit can redraw without a hard pop.
71func showFloatingWidget(w layout.Widget) {
72 if w == nil {
73 return
74 }
75 w.AddCssClass(floatingPaneVisibleClass)
76 w.SetCanTarget(true)
77 w.SetCanFocus(true)
78}
79
80// hideFloatingWidget makes a floating pane widget invisible and non-interactive.
81// Removes the visible CSS class instead of SetVisible(false) so GTK keeps the
82// widget mapped and WebKit retains its rendered surface in GPU memory.
83func hideFloatingWidget(w layout.Widget) {
84 if w == nil {
85 return
86 }
87 w.RemoveCssClass(floatingPaneVisibleClass)
88 w.SetCanTarget(false)
89 w.SetCanFocus(false)
90}
91
92// setFloatingWidgetShown applies show or hide based on the visible flag.
93func setFloatingWidgetShown(w layout.Widget, visible bool) {
94 if visible {
95 showFloatingWidget(w)
96 } else {
97 hideFloatingWidget(w)
98 }
99}
100
101func configureFloatingOverlayMeasurement(workspaceOverlay layout.OverlayWidget, floatingOverlay layout.Widget) {
102 if workspaceOverlay == nil || floatingOverlay == nil {
103 return
104 }
105
106 workspaceOverlay.SetMeasureOverlay(floatingOverlay, false)
107 workspaceOverlay.SetClipOverlay(floatingOverlay, false)
108}
109
110func floatingAllocationRect(overlayWidth, overlayHeight, desiredWidth, desiredHeight int) (x, y, width, height int, ok bool) {
111 if overlayWidth <= 0 || overlayHeight <= 0 || desiredWidth <= 0 || desiredHeight <= 0 {
112 return 0, 0, 0, 0, false
113 }
114
115 width = desiredWidth
116 height = desiredHeight
117 if width > overlayWidth {
118 width = overlayWidth
119 }
120 if height > overlayHeight {
121 height = overlayHeight
122 }
123
124 x = (overlayWidth - width) / 2
125 y = (overlayHeight - height) / 2
126 if x < 0 {
127 x = 0
128 }
129 if y < 0 {
130 y = 0
131 }
132
133 return x, y, width, height, true
134}
135
136type gtkAllocationLayout [4]int32
137
138const gtkAllocationLayoutSize = 16
139
140var (
141 _ [gtkAllocationLayoutSize - int(unsafe.Sizeof(gtkAllocationLayout{}))]struct{}
142 _ [int(unsafe.Sizeof(gtkAllocationLayout{})) - gtkAllocationLayoutSize]struct{}
143)
144
145// writeOverlayAllocation writes GTK-owned GtkAllocation fields in place.
146// Assumes GTK4 C layout for GdkRectangle/GtkAllocation: four contiguous
147// 32-bit signed integers {x, y, width, height} in native endianness.
148// If GTK struct layout changes, revisit offsets and writes below.
149//
150//nolint:gosec // Intentional unsafe pointer math against GTK-owned allocation memory.
151func writeOverlayAllocation(allocationPtr *uintptr, x, y, width, height int) bool {
152 if allocationPtr == nil {
153 return false
154 }
155
156 const (
157 gtkAllocationOffsetX = uintptr(0)
158 gtkAllocationOffsetY = uintptr(4)
159 gtkAllocationOffsetWidth = uintptr(8)
160 gtkAllocationOffsetHeight = uintptr(12)
161 )
162
163 // GtkAllocation/GdkRectangle is four 32-bit signed integers in C.
164 base := unsafe.Pointer(allocationPtr)
165 *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetX)) = int32(x)
166 *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetY)) = int32(y)
167 *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetWidth)) = int32(width)
168 *(*int32)(unsafe.Pointer(uintptr(base) + gtkAllocationOffsetHeight)) = int32(height)
169 return true
170}
171
172func (a *App) floatingAllocationForWidget(
173 tabID entity.TabID,
174 widgetPtr uintptr,
175 overlayWidth, overlayHeight int,
176) (x, y, width, height int, ok bool) {
177 for _, session := range a.floatingSessions.forTab(tabID) {
178 if session.widget == nil {
179 continue
180 }
181
182 gtkWidget := session.widget.GtkWidget()
183 if gtkWidget == nil || gtkWidget.GoPointer() != widgetPtr {
184 continue
185 }
186
187 desiredWidth := session.appliedWidth
188 desiredHeight := session.appliedHeight
189 if (desiredWidth <= 0 || desiredHeight <= 0) && session.pane != nil {
190 session.pane.Resize()
191 desiredWidth, desiredHeight = session.pane.Dimensions()
192 }
193
194 return floatingAllocationRect(overlayWidth, overlayHeight, desiredWidth, desiredHeight)
195 }
196
197 return 0, 0, 0, 0, false
198}
199
200func (a *App) installFloatingOverlayPositioning(tabID entity.TabID, workspaceOverlay layout.OverlayWidget) {
201 if workspaceOverlay == nil {
202 return
203 }
204
205 workspaceWidget := workspaceOverlay.GtkWidget()
206 if workspaceWidget == nil {
207 return
208 }
209
210 gtkOverlay := gtk.OverlayNewFromInternalPtr(workspaceWidget.GoPointer())
211 if gtkOverlay == nil {
212 return
213 }
214
215 cb := func(_ gtk.Overlay, widgetPtr uintptr, allocationPtr *uintptr) bool {
216 widget := gtk.WidgetNewFromInternalPtr(widgetPtr)
217 if widget != nil {
218 widgetPtr = widget.GoPointer()
219 }
220 overlayWidth := workspaceOverlay.GetAllocatedWidth()
221 overlayHeight := workspaceOverlay.GetAllocatedHeight()
222 x, y, width, height, ok := a.floatingAllocationForWidget(tabID, widgetPtr, overlayWidth, overlayHeight)
223 if !ok {
224 return false
225 }
226 return writeOverlayAllocation(allocationPtr, x, y, width, height)
227 }
228
229 gtkOverlay.ConnectGetChildPosition(&cb)
230}
231
232// currentFloatingWidthPct returns the configured floating pane width percentage.
233func (a *App) currentFloatingWidthPct() float64 {
234 return a.runtimeConfigSnapshot().UI.Workspace.FloatingPane.WidthPct
235}
236
237// currentFloatingHeightPct returns the configured floating pane height percentage.
238func (a *App) currentFloatingHeightPct() float64 {
239 return a.runtimeConfigSnapshot().UI.Workspace.FloatingPane.HeightPct
240}
241
242func (a *App) ensureFloatingSession(
243 ctx context.Context,
244 tabID entity.TabID,
245 sessionID string,
246 wsView *component.WorkspaceView,
247) (*floatingWorkspaceSession, error) {
248 key := floatingSessionMapKey(tabID, sessionID)
249 if session, ok := a.floatingSessions[key]; ok && session != nil {
250 if wsView == nil {
251 return session, nil
252 }
253 session.pane.SetParentOverlay(wsView.WorkspaceOverlayWidget())
254 if session.widget != nil {
255 wsView.AddWorkspaceOverlayWidget(session.widget)
256 configureFloatingOverlayMeasurement(wsView.WorkspaceOverlayWidget(), session.widget)
257 }
258 if session.paneView != nil {
259 wsView.RegisterPaneView(session.paneID, session.paneView)
260 if session.focusWidget == nil {
261 session.focusWidget = session.paneView.WebViewWidget()
262 }
263 }
264 return session, nil
265 }
266
267 if wsView == nil {
268 return nil, fmt.Errorf("workspace view not found for tab %s", tabID)
269 }
270 if a.contentCoord == nil {
271 return nil, fmt.Errorf("content coordinator not initialized")
272 }
273 if a.widgetFactory == nil {
274 return nil, fmt.Errorf("widget factory not initialized")
275 }
276
277 paneID := floatingPaneIDForSession(tabID, sessionID)
278 wv, err := a.contentCoord.EnsureWebView(ctx, paneID)
279 if err != nil {
280 return nil, fmt.Errorf("ensure floating webview: %w", err)
281 }
282
283 webViewWidget := a.contentCoord.WrapWidget(ctx, wv)
284 if webViewWidget == nil {
285 return nil, fmt.Errorf("wrap floating webview widget")
286 }
287 webViewWidget.SetHexpand(true)
288 webViewWidget.SetVexpand(true)
289
290 // Wrap the WebView in a PaneView so it gets progress bar, loading
291 // skeleton, and link status overlays — the same widget hierarchy as
292 // regular workspace panes. This allows the content coordinator's
293 // existing callbacks (onLoadStarted, onProgressChanged, etc.) to find
294 // and update the floating pane automatically.
295 pv := component.NewPaneView(ctx, a.widgetFactory, paneID, webViewWidget)
296 pv.SetActive(true)
297 // Hide loading skeleton — floating panes have their own themed container.
298 pv.HideLoadingSkeleton()
299 wsView.RegisterPaneView(paneID, pv)
300
301 // Use the PaneView's overlay directly as the floating overlay.
302 // This avoids nesting two overlays which causes GTK4 allocation
303 // issues (inner overlay expanding to fill the parent's allocation).
304 pvOverlay := pv.Overlay()
305 decorateFloatingOverlay(pvOverlay)
306 pvOverlay.SetHalign(gtk.AlignCenterValue)
307 pvOverlay.SetValign(gtk.AlignCenterValue)
308 hideFloatingWidget(pvOverlay)
309 wsView.AddWorkspaceOverlayWidget(pvOverlay)
310 configureFloatingOverlayMeasurement(wsView.WorkspaceOverlayWidget(), pvOverlay)
311
312 floatingPane := component.NewFloatingPane(wsView.WorkspaceOverlayWidget(), component.FloatingPaneOptions{
313 WidthPct: a.currentFloatingWidthPct(),
314 HeightPct: a.currentFloatingHeightPct(),
315 FallbackWidth: floatingPaneFallbackWidth,
316 FallbackHeight: floatingPaneFallbackHeight,
317 OnNavigate: func(navCtx context.Context, url string) error {
318 return wv.LoadURI(navCtx, url)
319 },
320 })
321
322 session := &floatingWorkspaceSession{
323 paneID: paneID,
324 pane: floatingPane,
325 paneView: pv,
326 webView: wv,
327 overlay: pvOverlay,
328 widget: pvOverlay,
329 focusWidget: webViewWidget,
330 }
331 a.floatingSessions[key] = session
332 return session, nil
333}
334
335func (a *App) releaseFloatingSessionsForTab(ctx context.Context, tabID entity.TabID) {
336 // Snapshot: releaseFloatingSession deletes from the registry.
337 sessions := a.floatingSessions.forTabSnapshot(tabID)
338 if len(sessions) == 0 {
339 return
340 }
341
342 for key, session := range sessions {
343 a.releaseFloatingSession(ctx, key, session)
344 }
345 a.syncFloatingFocus()
346}
347
348func (a *App) activeFloatingSessionEntry() (floatingSessionKey, *floatingWorkspaceSession, bool) {
349 activeTab := a.activeTabForBrowserWindow(a.lastFocusedBrowserWindow())
350 if activeTab == nil {
351 return floatingSessionKey{}, nil, false
352 }
353 return a.floatingSessions.visibleForTab(activeTab.ID)
354}
355
356func (a *App) activeFloatingSession() (*floatingWorkspaceSession, entity.TabID) {
357 key, session, ok := a.activeFloatingSessionEntry()
358 if !ok {
359 return nil, key.tabID
360 }
361 return session, key.tabID
362}
363
364func (a *App) releaseFloatingSession(ctx context.Context, key floatingSessionKey, session *floatingWorkspaceSession) {
365 if session == nil {
366 return
367 }
368
369 a.stopFloatingResizeWatcher(session)
370 if session.pane != nil {
371 session.pane.Hide(ctx)
372 }
373 a.hideFloatingOmnibox(ctx, session)
374 if wsView := a.workspaceViews[key.tabID]; wsView != nil {
375 if session.widget != nil {
376 wsView.RemoveWorkspaceOverlayWidget(session.widget)
377 }
378 if session.paneID != "" {
379 wsView.UnregisterPaneView(session.paneID)
380 }
381 }
382 if a.contentCoord != nil && session.paneID != "" {
383 a.contentCoord.ReleaseWebView(ctx, session.paneID)
384 }
385 delete(a.floatingSessions, key)
386
387 session.appliedWidth = 0
388 session.appliedHeight = 0
389 session.focusWidget = nil
390 session.paneView = nil
391 session.widget = nil
392 session.overlay = nil
393 session.webView = nil
394 session.pane = nil
395 // Session is fully released/zeroed; caller must drop this reference and not reuse it.
396}
397
398func (a *App) updateFloatingSessionURI(paneID entity.PaneID, url string) {
399 if session := a.floatingSessionByPaneID(paneID); session != nil && session.pane != nil {
400 session.pane.RecordLoadedURL(url)
401 }
402}
403
404func (a *App) syncFloatingFocus() {
405 for _, wsView := range a.workspaceViews {
406 if wsView != nil {
407 wsView.SetHoverFocusLocked(false)
408 }
409 }
410
411 if a.contentCoord == nil {
412 return
413 }
414
415 activeTab := a.activeTabForBrowserWindow(a.lastFocusedBrowserWindow())
416 if activeTab == nil {
417 a.contentCoord.ClearActivePaneOverride()
418 return
419 }
420
421 if session, _ := a.activeFloatingSession(); session != nil {
422 if wsView := a.workspaceViews[activeTab.ID]; wsView != nil {
423 wsView.SetHoverFocusLocked(true)
424 }
425 a.startFloatingResizeWatcher(session)
426 a.contentCoord.SetActivePaneOverride(session.paneID)
427 return
428 }
429
430 a.contentCoord.ClearActivePaneOverride()
431}
432
433func (a *App) showFloatingOmnibox(ctx context.Context, session *floatingWorkspaceSession) {
434 if session == nil || session.overlay == nil || a.widgetFactory == nil {
435 return
436 }
437
438 if session.omnibox == nil {
439 cfg := a.omniboxCfg
440 cfg.OnNavigate = func(navCtx context.Context, url string) error {
441 if session.pane == nil {
442 return fmt.Errorf("floating pane is not available")
443 }
444 if navCtx == nil {
445 navCtx = ctx
446 }
447 return session.pane.Navigate(navCtx, url)
448 }
449 cfg.OnToast = func(toastCtx context.Context, message string, level component.ToastLevel) {
450 a.showToastOnLastFocusedBrowserWindow(toastCtx, message, level)
451 }
452
453 omnibox := component.NewOmnibox(ctx, cfg)
454 if omnibox == nil {
455 return
456 }
457
458 omnibox.SetParentOverlay(session.overlay)
459 omniboxWidget := omnibox.WidgetAsLayout(a.widgetFactory)
460 if omniboxWidget == nil {
461 return
462 }
463
464 session.overlay.AddOverlay(omniboxWidget)
465 session.overlay.SetClipOverlay(omniboxWidget, false)
466 session.overlay.SetMeasureOverlay(omniboxWidget, false)
467
468 session.omnibox = omnibox
469 session.omniboxWidget = omniboxWidget
470 }
471
472 session.pane.SetOmniboxVisible(true)
473 session.omnibox.Show(ctx, "")
474}
475
476func (a *App) hideFloatingOmnibox(ctx context.Context, session *floatingWorkspaceSession) {
477 if session == nil || session.pane == nil {
478 return
479 }
480
481 session.pane.SetOmniboxVisible(false)
482 if session.omnibox == nil {
483 return
484 }
485
486 session.omnibox.Hide(ctx)
487 if session.overlay != nil && session.omniboxWidget != nil {
488 if parent := session.omniboxWidget.GetParent(); parent == session.overlay {
489 session.overlay.RemoveOverlay(session.omniboxWidget)
490 } else if parent != nil {
491 session.omniboxWidget.Unparent()
492 }
493 }
494 session.omnibox = nil
495 session.omniboxWidget = nil
496}
497
498func floatingFocusWidget(session *floatingWorkspaceSession) layout.Widget {
499 if session == nil {
500 return nil
501 }
502 if session.focusWidget != nil {
503 return session.focusWidget
504 }
505 if session.paneView != nil {
506 return session.paneView.WebViewWidget()
507 }
508 return nil
509}
510
511func focusFloatingSessionContent(session *floatingWorkspaceSession) {
512 if session == nil || session.pane == nil {
513 return
514 }
515 if !session.pane.IsVisible() || session.pane.IsOmniboxVisible() {
516 return
517 }
518 w := floatingFocusWidget(session)
519 if w == nil {
520 return
521 }
522 w.GrabFocus()
523}
524
525func (a *App) resizeFloatingWidget(session *floatingWorkspaceSession) {
526 if session == nil || session.pane == nil {
527 return
528 }
529 session.pane.Resize()
530 if session.widget == nil {
531 return
532 }
533 width, height := session.pane.Dimensions()
534 if session.appliedWidth == width && session.appliedHeight == height {
535 return
536 }
537 session.widget.SetSizeRequest(width, height)
538 session.appliedWidth = width
539 session.appliedHeight = height
540 if a.contentCoord != nil {
541 syncCtx := context.Background()
542 if a.deps != nil && a.deps.Ctx != nil {
543 syncCtx = a.deps.Ctx
544 }
545 a.contentCoord.SyncWebViewViewport(syncCtx, session.paneID, "floating-resize")
546 }
547}
548
549func (a *App) handleFloatingViewportTick(session *floatingWorkspaceSession) bool {
550 if session == nil || session.pane == nil {
551 return false
552 }
553 if !session.pane.IsVisible() {
554 session.resizeWatcherActive = false
555 session.resizeTickID = 0
556 return false
557 }
558
559 a.resizeFloatingWidget(session)
560 return true
561}
562
563func (a *App) floatingSessionByPaneID(paneID entity.PaneID) *floatingWorkspaceSession {
564 _, session, _ := a.floatingSessions.byPaneID(paneID)
565 return session
566}
567
568// popupOwnerWindowIDForPane scopes named contexts to the top-level owner. A
569// floating pane is mapped through its session's tab, never by parsing PaneID.
570func (a *App) popupOwnerWindowIDForPane(paneID entity.PaneID) (string, bool) {
571 if key, _, ok := a.floatingSessions.byPaneID(paneID); ok {
572 if bw := a.browserWindowForTab(key.tabID); bw != nil {
573 return bw.id, true
574 }
575 return "", false
576 }
577 if bw := a.browserWindowForAnyPane(paneID); bw != nil {
578 return bw.id, true
579 }
580 return "", false
581}
582
583func (a *App) startFloatingResizeWatcher(session *floatingWorkspaceSession) {
584 if session == nil || session.overlay == nil || session.resizeWatcherActive {
585 return
586 }
587
588 overlayWidget := session.overlay.GtkWidget()
589 if overlayWidget == nil {
590 return
591 }
592
593 session.resizeWatcherActive = true
594 paneID := session.paneID
595 callback := new(gtk.TickCallback)
596 *callback = func(_ uintptr, _ uintptr, _ uintptr) bool {
597 liveSession := a.floatingSessionByPaneID(paneID)
598 if liveSession == nil {
599 session.releaseResizeTickCallback()
600 return false
601 }
602
603 keepRunning := a.handleFloatingViewportTick(liveSession)
604 if !keepRunning {
605 liveSession.releaseResizeTickCallback()
606 }
607 return keepRunning
608 }
609 session.resizeTickCallback = callback
610 session.resizeTickID = overlayWidget.AddTickCallback(callback, 0, nil)
611}
612
613func (a *App) stopFloatingResizeWatcher(session *floatingWorkspaceSession) {
614 if session == nil {
615 return
616 }
617
618 tickID := session.resizeTickID
619 session.releaseResizeTickCallback()
620 if tickID != 0 && session.overlay != nil {
621 if overlayWidget := session.overlay.GtkWidget(); overlayWidget != nil {
622 overlayWidget.RemoveTickCallback(tickID)
623 }
624 }
625}
626
627// releaseResizeTickCallback releases the raw purego callback slot. It is safe
628// to call after GTK has already removed a callback that returned false.
629func (session *floatingWorkspaceSession) releaseResizeTickCallback() {
630 if session == nil {
631 return
632 }
633 callback := session.resizeTickCallback
634 session.resizeTickID = 0
635 session.resizeTickCallback = nil
636 session.resizeWatcherActive = false
637 if callback != nil {
638 // AddTickCallback creates a purego function-pointer slot. Clearing the Go
639 // pointer alone cannot return that finite runtime slot to the ledger.
640 _ = unrefTickCallback(callback)
641 }
642}
643
644func (a *App) hideFloatingSession(ctx context.Context, session *floatingWorkspaceSession) {
645 if session == nil || session.pane == nil {
646 return
647 }
648
649 a.stopFloatingResizeWatcher(session)
650 session.pane.Hide(ctx)
651 a.hideFloatingOmnibox(ctx, session)
652 hideFloatingWidget(session.widget)
653 // Force a fresh size request on next show so WebKit gets a new
654 // allocation cycle and repaints content after rapid toggles.
655 session.appliedWidth = 0
656 session.appliedHeight = 0
657}
658
659func (a *App) hideVisibleFloatingSessions(ctx context.Context, tabID entity.TabID, except *floatingWorkspaceSession) {
660 for _, session := range a.floatingSessions.forTab(tabID) {
661 if session == except || session.pane == nil {
662 continue
663 }
664 if !session.pane.IsVisible() {
665 continue
666 }
667 a.hideFloatingSession(ctx, session)
668 }
669}
670
671func (a *App) closeActiveFloatingPane(ctx context.Context) bool {
672 session, _ := a.activeFloatingSession()
673 if session == nil {
674 return false
675 }
676 a.hideFloatingSession(ctx, session)
677 a.syncFloatingFocus()
678 return true
679}
680
681func (a *App) handleGlobalEscape(ctx context.Context) bool {
682 return a.closeActiveFloatingPane(ctx)
683}
684
685func (a *App) closeAndReleaseActiveFloatingPane(ctx context.Context) bool {
686 key, session, ok := a.activeFloatingSessionEntry()
687 if !ok {
688 return false
689 }
690 a.releaseFloatingSession(ctx, key, session)
691 a.syncFloatingFocus()
692 return true
693}
694
695// ToggleFloatingPane toggles the active workspace floating pane visibility.
696func (a *App) ToggleFloatingPane(ctx context.Context) error {
697 activeTab := a.activeTabForBrowserWindow(a.lastFocusedBrowserWindow())
698 if activeTab == nil {
699 return nil
700 }
701 if a.closeActiveFloatingPane(ctx) {
702 return nil
703 }
704 return a.openFloatingPaneSession(ctx, floatingSessionIDDefault)
705}
706
707// OpenFloatingPaneURL opens the active workspace floating pane directly to a URL.
708func (a *App) OpenFloatingPaneURL(ctx context.Context, url string) error {
709 return a.openFloatingPaneSession(ctx, floatingSessionIDDefault, url)
710}
711
712// OpenFloatingPaneProfileURL opens a named floating pane profile session directly to a URL.
713func (a *App) OpenFloatingPaneProfileURL(ctx context.Context, sessionID, url string) error {
714 return a.openFloatingPaneSession(ctx, sessionID, url)
715}
716
717func (a *App) openFloatingPaneSession(ctx context.Context, sessionID string, url ...string) error {
718 activeTab := a.activeTabForBrowserWindow(a.lastFocusedBrowserWindow())
719 if activeTab == nil {
720 return nil
721 }
722
723 wsView := a.workspaceViews[activeTab.ID]
724 session, err := a.ensureFloatingSession(ctx, activeTab.ID, sessionID, wsView)
725 if err != nil {
726 return err
727 }
728 hasURL := len(url) > 0 && strings.TrimSpace(url[0]) != ""
729 isProfileSession := normalizeFloatingSessionID(sessionID) != floatingSessionIDDefault
730
731 if hasURL && isProfileSession && session.pane.IsVisible() {
732 a.hideFloatingSession(ctx, session)
733 a.syncFloatingFocus()
734 return nil
735 }
736
737 a.hideVisibleFloatingSessions(ctx, activeTab.ID, session)
738
739 if hasURL {
740 if isProfileSession && session.pane.SessionStarted() {
741 session.pane.Show()
742 } else {
743 if err := session.pane.ShowURL(ctx, url[0]); err != nil {
744 return err
745 }
746 }
747 } else {
748 if err := session.pane.ShowToggle(ctx); err != nil {
749 return err
750 }
751 }
752
753 a.resizeFloatingWidget(session)
754 setFloatingWidgetShown(session.widget, session.pane.IsVisible())
755 if session.pane.IsOmniboxVisible() {
756 a.showFloatingOmnibox(ctx, session)
757 } else {
758 a.hideFloatingOmnibox(ctx, session)
759 focusFloatingSessionContent(session)
760 }
761 a.startFloatingResizeWatcher(session)
762 a.syncFloatingFocus()
763
764 return nil
765}