1// Package component provides UI components for the browser.
2package component
3
4import (
5 "context"
6 "errors"
7 "fmt"
8 "sync"
9
10 "github.com/bnema/dumber/internal/domain/entity"
11 "github.com/bnema/dumber/internal/logging"
12 "github.com/bnema/dumber/internal/ui/layout"
13 "github.com/rs/zerolog"
14)
15
16// ErrNilWorkspace is returned when attempting to set a nil workspace.
17var ErrNilWorkspace = errors.New("workspace is nil")
18
19// ErrPaneNotFound is returned when a pane ID cannot be found.
20var ErrPaneNotFound = errors.New("pane not found")
21
22// WorkspaceView is the top-level container that renders a workspace's pane tree.
23// It manages the widget tree and handles active pane state.
24type WorkspaceView struct {
25 factory layout.WidgetFactory
26 treeRenderer *layout.TreeRenderer
27 container layout.BoxWidget
28 overlay layout.OverlayWidget // Wraps container for mode borders
29 rootWidget layout.Widget // Current root widget for removal on rebuild
30 logger zerolog.Logger
31
32 // Mode border overlay slot
33 modeBorderWidget layout.Widget
34
35 // Omnibox lifecycle
36 omnibox *Omnibox // Current omnibox (nil if not shown)
37 omniboxWidget layout.Widget // Wrapped omnibox widget for overlay
38 omniboxPaneID entity.PaneID // Which pane has the omnibox
39 omniboxCfg OmniboxConfig // Stored config for creating omniboxes
40
41 // Find bar lifecycle
42 findBar *FindBar // Current find bar (nil if not shown)
43 findBarWidget layout.Widget // Wrapped find bar widget for overlay
44 findBarPaneID entity.PaneID // Which pane has the find bar
45 findBarCfg FindBarConfig // Stored config for creating find bars
46
47 workspace *entity.Workspace
48 paneViews map[entity.PaneID]*PaneView
49
50 onPaneFocused func(paneID entity.PaneID)
51 onSplitRatioDragged func(nodeID string, ratio float64)
52
53 mu sync.RWMutex
54}
55
56// paneViewFactoryAdapter adapts WorkspaceView to the PaneViewFactory interface
57// so TreeRenderer can create PaneViews.
58type paneViewFactoryAdapter struct {
59 wv *WorkspaceView
60 ctx context.Context
61}
62
63func (a *paneViewFactoryAdapter) CreatePaneView(node *entity.PaneNode) layout.Widget {
64 if node == nil || node.Pane == nil {
65 return nil
66 }
67
68 // Create PaneView without WebView widget for now
69 // WebView will be attached later by the application layer
70 pv := NewPaneView(a.wv.factory, node.Pane.ID, nil)
71
72 // Store in map for later lookup
73 // Note: Caller (SetWorkspace) already holds the lock, so we access directly
74 a.wv.paneViews[node.Pane.ID] = pv
75
76 // Set up focus callback
77 pv.SetOnFocusIn(func(paneID entity.PaneID) {
78 a.wv.mu.RLock()
79 callback := a.wv.onPaneFocused
80 a.wv.mu.RUnlock()
81
82 if callback != nil {
83 callback(paneID)
84 }
85 })
86
87 // Set up hover callback for focus-follows-mouse
88 pv.SetOnHover(func(paneID entity.PaneID) {
89 // Skip if this pane is already active
90 if a.wv.GetActivePaneID() == paneID {
91 return
92 }
93
94 // Activate the hovered pane and grab focus
95 if err := a.wv.SetActivePaneID(paneID); err == nil {
96 a.wv.FocusPane(paneID)
97 }
98 })
99
100 // Attach hover handler with debouncing
101 pv.AttachHoverHandler(a.ctx)
102
103 return pv.Widget()
104}
105
106// NewWorkspaceView creates a new workspace view.
107func NewWorkspaceView(ctx context.Context, factory layout.WidgetFactory) *WorkspaceView {
108 log := logging.FromContext(ctx)
109
110 container := factory.NewBox(layout.OrientationVertical, 0)
111 container.SetHexpand(true)
112 container.SetVexpand(true)
113 container.SetVisible(true)
114
115 // Wrap container in overlay for mode borders
116 overlay := factory.NewOverlay()
117 overlay.SetHexpand(true)
118 overlay.SetVexpand(true)
119 overlay.SetChild(container)
120 overlay.SetVisible(true)
121
122 wv := &WorkspaceView{
123 factory: factory,
124 container: container,
125 overlay: overlay,
126 logger: log.With().Str("component", "workspace-view").Logger(),
127 paneViews: make(map[entity.PaneID]*PaneView),
128 }
129
130 // Create tree renderer with our adapter as the pane view factory
131 wv.treeRenderer = layout.NewTreeRenderer(ctx, factory, &paneViewFactoryAdapter{wv: wv, ctx: ctx})
132 wv.treeRenderer.SetOnSplitRatioChanged(func(nodeID string, ratio float64) {
133 wv.mu.RLock()
134 cb := wv.onSplitRatioDragged
135 wv.mu.RUnlock()
136 if cb != nil {
137 cb(nodeID, ratio)
138 }
139 })
140
141 return wv
142}
143
144// SetWorkspace sets the workspace to render and builds the widget tree.
145func (wv *WorkspaceView) SetWorkspace(ctx context.Context, ws *entity.Workspace) error {
146 if ws == nil {
147 return ErrNilWorkspace
148 }
149
150 wv.mu.Lock()
151 defer wv.mu.Unlock()
152
153 // Clear previous state
154 wv.workspace = ws
155 wv.paneViews = make(map[entity.PaneID]*PaneView)
156
157 // Remove old root widget from container before building new tree
158 if wv.rootWidget != nil {
159 wv.container.Remove(wv.rootWidget)
160 wv.rootWidget = nil
161 }
162
163 // Build new tree
164 if ws.Root != nil {
165 widget, err := wv.treeRenderer.Build(ctx, ws.Root)
166 if err != nil {
167 return err
168 }
169
170 if widget != nil {
171 widget.SetVisible(true)
172 wv.container.Append(widget)
173 wv.rootWidget = widget
174 }
175 }
176
177 // Set initial active pane
178 if ws.ActivePaneID != "" {
179 if err := wv.setActivePaneIDInternal(ws.ActivePaneID); err != nil {
180 return fmt.Errorf("set initial active pane: %w", err)
181 }
182 }
183
184 // Update single-pane mode based on pane count
185 wv.updateSinglePaneModeInternal()
186
187 return nil
188}
189
190// SetActivePaneID updates which pane is visually marked as active.
191func (wv *WorkspaceView) SetActivePaneID(paneID entity.PaneID) error {
192 wv.mu.Lock()
193 defer wv.mu.Unlock()
194
195 return wv.setActivePaneIDInternal(paneID)
196}
197
198// setActivePaneIDInternal updates active pane without locking.
199// Updates the domain model as the single source of truth.
200func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error {
201 currentActiveID := wv.getActivePaneIDInternal()
202
203 wv.logger.Debug().
204 Str("from_pane", string(currentActiveID)).
205 Str("to_pane", string(paneID)).
206 Bool("omnibox_visible", wv.omnibox != nil).
207 Str("omnibox_pane", string(wv.omniboxPaneID)).
208 Msg("active pane changing")
209
210 // Destroy omnibox if active pane is changing
211 if currentActiveID != paneID && wv.omnibox != nil {
212 wv.logger.Debug().Msg("destroying omnibox due to pane change")
213 wv.hideOmniboxInternal()
214 }
215 // Destroy find bar if active pane is changing
216 if currentActiveID != paneID && wv.findBar != nil {
217 wv.hideFindBarInternal()
218 }
219
220 // Deactivate current active pane
221 if currentActiveID != "" {
222 if oldPV, ok := wv.paneViews[currentActiveID]; ok {
223 oldPV.SetActive(false)
224 }
225 }
226
227 // Activate new pane
228 newPV, ok := wv.paneViews[paneID]
229 if !ok {
230 return ErrPaneNotFound
231 }
232
233 newPV.SetActive(true)
234
235 // Update domain model - single source of truth
236 if wv.workspace != nil {
237 wv.workspace.ActivePaneID = paneID
238 }
239
240 return nil
241}
242
243// GetActivePaneID returns the ID of the currently active pane.
244// Reads from the domain model as the single source of truth.
245func (wv *WorkspaceView) GetActivePaneID() entity.PaneID {
246 wv.mu.RLock()
247 defer wv.mu.RUnlock()
248
249 return wv.getActivePaneIDInternal()
250}
251
252// getActivePaneIDInternal returns the active pane ID without locking.
253// Must be called with at least a read lock held.
254func (wv *WorkspaceView) getActivePaneIDInternal() entity.PaneID {
255 if wv.workspace == nil {
256 return ""
257 }
258 return wv.workspace.ActivePaneID
259}
260
261// GetPaneView returns the PaneView for a given pane ID.
262// Returns nil if not found.
263func (wv *WorkspaceView) GetPaneView(paneID entity.PaneID) *PaneView {
264 wv.mu.RLock()
265 defer wv.mu.RUnlock()
266
267 return wv.paneViews[paneID]
268}
269
270// GetPaneIDs returns all pane IDs in this workspace view.
271func (wv *WorkspaceView) GetPaneIDs() []entity.PaneID {
272 wv.mu.RLock()
273 defer wv.mu.RUnlock()
274
275 ids := make([]entity.PaneID, 0, len(wv.paneViews))
276 for id := range wv.paneViews {
277 ids = append(ids, id)
278 }
279 return ids
280}
281
282// PaneCount returns the number of panes in the workspace view.
283func (wv *WorkspaceView) PaneCount() int {
284 wv.mu.RLock()
285 defer wv.mu.RUnlock()
286
287 return len(wv.paneViews)
288}
289
290// singlePaneClass is used to hide the active pane border when only one pane exists.
291const singlePaneClass = "single-pane"
292
293// updateSinglePaneModeInternal adds/removes the single-pane CSS class on the container.
294// Must be called with the lock held.
295func (wv *WorkspaceView) updateSinglePaneModeInternal() {
296 if wv.container == nil {
297 return
298 }
299
300 if len(wv.paneViews) <= 1 {
301 wv.container.AddCssClass(singlePaneClass)
302 } else {
303 wv.container.RemoveCssClass(singlePaneClass)
304 }
305}
306
307// SetOnPaneFocused sets the callback for when a pane receives focus.
308func (wv *WorkspaceView) SetOnPaneFocused(fn func(paneID entity.PaneID)) {
309 wv.mu.Lock()
310 defer wv.mu.Unlock()
311
312 wv.onPaneFocused = fn
313}
314
315func (wv *WorkspaceView) SetOnSplitRatioDragged(fn func(nodeID string, ratio float64)) {
316 wv.mu.Lock()
317 defer wv.mu.Unlock()
318
319 wv.onSplitRatioDragged = fn
320}
321
322// Rebuild rebuilds the widget tree from the current workspace.
323// Use this after structural changes like splits or closes.
324func (wv *WorkspaceView) Rebuild(ctx context.Context) error {
325 wv.mu.RLock()
326 ws := wv.workspace
327 wv.mu.RUnlock()
328
329 if ws == nil {
330 return ErrNilWorkspace
331 }
332
333 return wv.SetWorkspace(ctx, ws)
334}
335
336// FocusPane attempts to give focus to a specific pane.
337// Returns true if focus was successfully grabbed.
338func (wv *WorkspaceView) FocusPane(paneID entity.PaneID) bool {
339 wv.mu.RLock()
340 pv, ok := wv.paneViews[paneID]
341 wv.mu.RUnlock()
342
343 if !ok || pv == nil {
344 return false
345 }
346
347 return pv.GrabFocus()
348}
349
350// SetWebViewWidget attaches a WebView widget to a specific pane.
351func (wv *WorkspaceView) SetWebViewWidget(paneID entity.PaneID, widget layout.Widget) error {
352 wv.mu.RLock()
353 pv, ok := wv.paneViews[paneID]
354 wv.mu.RUnlock()
355
356 if !ok {
357 return ErrPaneNotFound
358 }
359
360 pv.SetWebViewWidget(widget)
361 return nil
362}
363
364// Widget returns the overlay widget for embedding in the UI.
365// The overlay wraps the pane container and allows mode borders to be displayed.
366func (wv *WorkspaceView) Widget() layout.Widget {
367 return wv.overlay
368}
369
370// Container returns the underlying BoxWidget for direct access.
371func (wv *WorkspaceView) Container() layout.BoxWidget {
372 return wv.container
373}
374
375// TreeRenderer returns the underlying tree renderer.
376func (wv *WorkspaceView) TreeRenderer() *layout.TreeRenderer {
377 return wv.treeRenderer
378}
379
380// Workspace returns the current workspace.
381func (wv *WorkspaceView) Workspace() *entity.Workspace {
382 wv.mu.RLock()
383 defer wv.mu.RUnlock()
384
385 return wv.workspace
386}
387
388// RegisterPaneView adds a PaneView to the tracking map without rebuilding.
389// Use this for incremental operations like stacked panes.
390func (wv *WorkspaceView) RegisterPaneView(paneID entity.PaneID, pv *PaneView) {
391 wv.mu.Lock()
392 defer wv.mu.Unlock()
393
394 wv.paneViews[paneID] = pv
395 wv.updateSinglePaneModeInternal()
396}
397
398// UnregisterPaneView removes a PaneView from the tracking map and cleans it up.
399// This must be called when closing a pane to properly release GTK resources
400// before destroying the WebView.
401func (wv *WorkspaceView) UnregisterPaneView(paneID entity.PaneID) {
402 wv.mu.Lock()
403 defer wv.mu.Unlock()
404
405 // Call Cleanup on the PaneView to unparent widgets and clear callbacks
406 // This must happen before WebView.Destroy() is called
407 if pv, ok := wv.paneViews[paneID]; ok && pv != nil {
408 pv.Cleanup()
409 }
410
411 delete(wv.paneViews, paneID)
412 wv.updateSinglePaneModeInternal()
413}
414
415// GetRootWidget returns the current root widget of the workspace.
416// This is useful for incremental operations that need to modify the tree.
417func (wv *WorkspaceView) GetRootWidget() layout.Widget {
418 wv.mu.RLock()
419 defer wv.mu.RUnlock()
420
421 return wv.rootWidget
422}
423
424// SetRootWidgetDirect replaces the root widget without rebuilding the entire tree.
425// Use this for incremental operations when converting to/from stacked panes.
426func (wv *WorkspaceView) SetRootWidgetDirect(widget layout.Widget) {
427 wv.mu.Lock()
428 defer wv.mu.Unlock()
429
430 // Remove old root if present
431 if wv.rootWidget != nil {
432 wv.container.Remove(wv.rootWidget)
433 }
434
435 // Add new root
436 if widget != nil {
437 widget.SetVisible(true)
438 wv.container.Append(widget)
439 }
440
441 wv.rootWidget = widget
442}
443
444// ClearRootWidgetRef clears the stored root widget reference without removing it.
445// Use this before SetRootWidgetDirect when the old root has already been removed
446// through other means (e.g., GTK paned operations).
447func (wv *WorkspaceView) ClearRootWidgetRef() {
448 wv.mu.Lock()
449 defer wv.mu.Unlock()
450 wv.rootWidget = nil
451}
452
453// Factory returns the widget factory used by this workspace view.
454func (wv *WorkspaceView) Factory() layout.WidgetFactory {
455 return wv.factory
456}
457
458// SetModeBorderOverlay attaches a mode border overlay widget.
459// The widget will be displayed on top of the pane container when modes are active.
460func (wv *WorkspaceView) SetModeBorderOverlay(widget layout.Widget) {
461 wv.mu.Lock()
462 defer wv.mu.Unlock()
463
464 // Remove old overlay if present
465 if wv.modeBorderWidget != nil && wv.overlay != nil {
466 wv.overlay.RemoveOverlay(wv.modeBorderWidget)
467 }
468
469 wv.modeBorderWidget = widget
470
471 // Add new overlay
472 if widget != nil && wv.overlay != nil {
473 wv.overlay.AddOverlay(widget)
474 // Don't clip or measure the overlay - it should fill the entire area
475 wv.overlay.SetClipOverlay(widget, false)
476 wv.overlay.SetMeasureOverlay(widget, false)
477 }
478}
479
480// GetPaneWidget returns the widget for a pane ID.
481// Implements focus.PaneGeometryProvider.
482func (wv *WorkspaceView) GetPaneWidget(paneID entity.PaneID) layout.Widget {
483 wv.mu.RLock()
484 defer wv.mu.RUnlock()
485
486 pv, ok := wv.paneViews[paneID]
487 if !ok || pv == nil {
488 return nil
489 }
490 return pv.Widget()
491}
492
493// GetStackContainerWidget returns the stack container widget for a stacked pane.
494// Returns nil if the pane is not in a stack.
495// Implements focus.PaneGeometryProvider.
496func (wv *WorkspaceView) GetStackContainerWidget(paneID entity.PaneID) layout.Widget {
497 if wv.treeRenderer == nil {
498 return nil
499 }
500
501 stackedView := wv.treeRenderer.GetStackedViewForPane(string(paneID))
502 if stackedView == nil {
503 return nil
504 }
505
506 return stackedView.Widget()
507}
508
509// ContainerWidget returns the container widget for relative positioning.
510// Implements focus.PaneGeometryProvider.
511func (wv *WorkspaceView) ContainerWidget() layout.Widget {
512 return wv.container
513}
514
515// SetOmniboxConfig stores the omnibox configuration for later use.
516func (wv *WorkspaceView) SetOmniboxConfig(cfg OmniboxConfig) {
517 wv.mu.Lock()
518 defer wv.mu.Unlock()
519 wv.omniboxCfg = cfg
520}
521
522// SetFindBarConfig stores the find bar configuration for later use.
523func (wv *WorkspaceView) SetFindBarConfig(cfg FindBarConfig) {
524 wv.mu.Lock()
525 defer wv.mu.Unlock()
526 wv.findBarCfg = cfg
527}
528
529// ShowOmnibox creates and shows the omnibox in the active pane.
530func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) {
531 wv.mu.Lock()
532 defer wv.mu.Unlock()
533
534 // Get the active pane view
535 activePaneID := wv.getActivePaneIDInternal()
536 pv := wv.paneViews[activePaneID]
537 if pv == nil {
538 wv.logger.Warn().Str("paneID", string(activePaneID)).Msg("cannot show omnibox: active pane not found")
539 return
540 }
541
542 // If omnibox exists but on different pane, destroy and recreate on active pane
543 if wv.omnibox != nil {
544 if wv.omniboxPaneID != activePaneID {
545 wv.logger.Debug().
546 Str("old_pane", string(wv.omniboxPaneID)).
547 Str("new_pane", string(activePaneID)).
548 Msg("omnibox pane changed, recreating")
549 wv.hideOmniboxInternal()
550 } else {
551 wv.logger.Debug().Str("pane", string(activePaneID)).Msg("showing existing omnibox")
552 wv.omnibox.Show(ctx, query)
553 return
554 }
555 }
556
557 // Create omnibox with pane-specific toast callback
558 cfg := wv.omniboxCfg
559 cfg.OnToast = func(toastCtx context.Context, message string, level ToastLevel) {
560 pv.ShowToast(toastCtx, message, level)
561 }
562 omnibox := NewOmnibox(ctx, cfg)
563 if omnibox == nil {
564 wv.logger.Error().Msg("failed to create omnibox")
565 return
566 }
567
568 // Set parent overlay for sizing
569 omnibox.SetParentOverlay(pv.Overlay())
570
571 // Wrap widget for layout system
572 omniboxWidget := omnibox.WidgetAsLayout(wv.factory)
573 if omniboxWidget == nil {
574 wv.logger.Error().Msg("failed to wrap omnibox widget")
575 return
576 }
577
578 // Add to pane overlay
579 pv.AddOverlayWidget(omniboxWidget)
580
581 // Store references
582 wv.omnibox = omnibox
583 wv.omniboxWidget = omniboxWidget
584 wv.omniboxPaneID = activePaneID
585
586 // Show the omnibox
587 omnibox.Show(ctx, query)
588
589 wv.logger.Debug().Str("paneID", string(activePaneID)).Msg("omnibox shown")
590}
591
592// HideOmnibox hides and destroys the current omnibox.
593func (wv *WorkspaceView) HideOmnibox() {
594 wv.mu.Lock()
595 defer wv.mu.Unlock()
596
597 wv.hideOmniboxInternal()
598}
599
600// hideOmniboxInternal destroys the omnibox without locking.
601func (wv *WorkspaceView) hideOmniboxInternal() {
602 if wv.omnibox == nil {
603 return
604 }
605
606 // Hide the omnibox first (use Background context since this is internal cleanup)
607 wv.omnibox.Hide(context.Background())
608
609 // Remove from pane overlay
610 if pv := wv.paneViews[wv.omniboxPaneID]; pv != nil && wv.omniboxWidget != nil {
611 // The omnibox widget may have been reparented during a rebuild/move.
612 // Avoid GTK criticals by only removing from the overlay that owns it.
613 if parent := wv.omniboxWidget.GetParent(); parent == pv.Overlay() {
614 pv.RemoveOverlayWidget(wv.omniboxWidget)
615 } else if parent != nil {
616 wv.omniboxWidget.Unparent()
617 }
618 }
619
620 // Clear references
621 wv.omnibox = nil
622 wv.omniboxWidget = nil
623 wv.omniboxPaneID = ""
624
625 wv.logger.Debug().Msg("omnibox hidden and destroyed")
626}
627
628// ShowFindBar creates and shows the find bar in the active pane.
629func (wv *WorkspaceView) ShowFindBar(ctx context.Context) {
630 wv.mu.Lock()
631 defer wv.mu.Unlock()
632
633 // If find bar already exists, just show it
634 if wv.findBar != nil {
635 wv.findBar.Show()
636 return
637 }
638
639 activePaneID := wv.getActivePaneIDInternal()
640 pv := wv.paneViews[activePaneID]
641 if pv == nil {
642 wv.logger.Warn().Str("paneID", string(activePaneID)).Msg("cannot show find bar: active pane not found")
643 return
644 }
645
646 cfg := wv.findBarCfg
647 cfg.OnClose = func() {
648 wv.HideFindBar()
649 }
650 findBar := NewFindBar(ctx, cfg)
651 if findBar == nil {
652 wv.logger.Error().Msg("failed to create find bar")
653 return
654 }
655
656 findBarWidget := findBar.WidgetAsLayout(wv.factory)
657 if findBarWidget == nil {
658 wv.logger.Error().Msg("failed to wrap find bar widget")
659 return
660 }
661
662 pv.AddOverlayWidget(findBarWidget)
663
664 if cfg.GetFindController != nil {
665 controller := cfg.GetFindController(activePaneID)
666 findBar.SetFindController(controller)
667 }
668
669 wv.findBar = findBar
670 wv.findBarWidget = findBarWidget
671 wv.findBarPaneID = activePaneID
672
673 findBar.Show()
674
675 wv.logger.Debug().Str("paneID", string(activePaneID)).Msg("find bar shown")
676}
677
678// HideFindBar hides and destroys the current find bar.
679func (wv *WorkspaceView) HideFindBar() {
680 wv.mu.Lock()
681 defer wv.mu.Unlock()
682
683 wv.hideFindBarInternal()
684}
685
686// FindNext triggers the next match in the find bar if available.
687func (wv *WorkspaceView) FindNext() {
688 wv.mu.RLock()
689 findBar := wv.findBar
690 wv.mu.RUnlock()
691
692 if findBar != nil {
693 findBar.FindNext()
694 }
695}
696
697// FindPrevious triggers the previous match in the find bar if available.
698func (wv *WorkspaceView) FindPrevious() {
699 wv.mu.RLock()
700 findBar := wv.findBar
701 wv.mu.RUnlock()
702
703 if findBar != nil {
704 findBar.FindPrevious()
705 }
706}
707
708// IsFindBarVisible returns whether the find bar is currently visible.
709func (wv *WorkspaceView) IsFindBarVisible() bool {
710 wv.mu.RLock()
711 defer wv.mu.RUnlock()
712 return wv.findBar != nil && wv.findBar.IsVisible()
713}
714
715// hideFindBarInternal destroys the find bar without locking.
716func (wv *WorkspaceView) hideFindBarInternal() {
717 if wv.findBar == nil {
718 return
719 }
720
721 wv.findBar.Hide()
722
723 if pv := wv.paneViews[wv.findBarPaneID]; pv != nil && wv.findBarWidget != nil {
724 if parent := wv.findBarWidget.GetParent(); parent == pv.Overlay() {
725 pv.RemoveOverlayWidget(wv.findBarWidget)
726 } else if parent != nil {
727 wv.findBarWidget.Unparent()
728 }
729 }
730
731 wv.findBar = nil
732 wv.findBarWidget = nil
733 wv.findBarPaneID = ""
734
735 wv.logger.Debug().Msg("find bar hidden and destroyed")
736}
737
738// GetOmnibox returns the current omnibox if visible.
739func (wv *WorkspaceView) GetOmnibox() *Omnibox {
740 wv.mu.RLock()
741 defer wv.mu.RUnlock()
742 return wv.omnibox
743}
744
745// IsOmniboxVisible returns whether the omnibox is currently visible.
746func (wv *WorkspaceView) IsOmniboxVisible() bool {
747 wv.mu.RLock()
748 defer wv.mu.RUnlock()
749 return wv.omnibox != nil && wv.omnibox.IsVisible()
750}
751
752// GetActivePaneView returns the PaneView for the current active pane.
753func (wv *WorkspaceView) GetActivePaneView() *PaneView {
754 wv.mu.RLock()
755 defer wv.mu.RUnlock()
756 return wv.paneViews[wv.getActivePaneIDInternal()]
757}