1// Package component provides UI components for the browser.
2package component
3
4import (
5 "context"
6 "errors"
7
8 "github.com/bnema/dumber/internal/domain/entity"
9 "github.com/bnema/dumber/internal/logging"
10 "github.com/bnema/dumber/internal/ui/layout"
11)
12
13// ErrStackNotFound is returned when a StackedView cannot be found for a pane.
14var ErrStackNotFound = errors.New("stacked view not found for pane")
15
16// StackedPaneManager handles stacked pane operations.
17// With the new architecture, every pane is wrapped in a StackedView from the start,
18// making stacking operations trivial - we just add panes to the existing StackedView.
19type StackedPaneManager struct {
20 factory layout.WidgetFactory
21}
22
23// NewStackedPaneManager creates a new stacked pane manager.
24func NewStackedPaneManager(factory layout.WidgetFactory) *StackedPaneManager {
25 return &StackedPaneManager{
26 factory: factory,
27 }
28}
29
30// AddPaneToStack adds a new pane to the stack containing the active pane.
31// The StackedView is looked up from the TreeRenderer.
32func (spm *StackedPaneManager) AddPaneToStack(
33 ctx context.Context,
34 wsView *WorkspaceView,
35 activePaneID entity.PaneID,
36 newPaneView *PaneView,
37 title string,
38) error {
39 log := logging.FromContext(ctx)
40
41 // Get the StackedView containing the active pane
42 tr := wsView.TreeRenderer()
43 if tr == nil {
44 return errors.New("tree renderer not available")
45 }
46
47 stackedView := tr.GetStackedViewForPane(string(activePaneID))
48 if stackedView == nil {
49 return ErrStackNotFound
50 }
51
52 // Get the new pane's widget
53 widget := newPaneView.Widget()
54 if widget == nil {
55 return errors.New("new pane has no widget")
56 }
57
58 // Ensure widget is unparented
59 if widget.GetParent() != nil {
60 widget.Unparent()
61 }
62
63 // Add to the stack after the current active pane (not at end)
64 if title == "" {
65 title = "Untitled"
66 }
67
68 // Get current active index to insert after it
69 currentActiveIndex := stackedView.ActiveIndex()
70
71 log.Debug().
72 Str("active_pane", string(activePaneID)).
73 Str("new_pane", string(newPaneView.PaneID())).
74 Str("title", title).
75 Int("current_active_index", currentActiveIndex).
76 Int("stack_size_before", stackedView.Count()).
77 Msg("StackedPaneManager: inserting pane after active position")
78
79 stackedView.InsertPaneAfter(ctx, currentActiveIndex, string(newPaneView.PaneID()), title, "", widget)
80
81 // Register the new pane in the TreeRenderer's tracking
82 tr.RegisterPaneInStack(string(newPaneView.PaneID()), stackedView)
83
84 log.Info().
85 Str("active_pane", string(activePaneID)).
86 Str("new_pane", string(newPaneView.PaneID())).
87 Int("stack_size", stackedView.Count()).
88 Msg("added pane to stack")
89
90 return nil
91}
92
93// NavigateStack moves to the next or previous pane in a stack.
94// Returns the pane ID that became active.
95func (spm *StackedPaneManager) NavigateStack(
96 ctx context.Context,
97 wsView *WorkspaceView,
98 currentPaneID entity.PaneID,
99 direction string,
100) (entity.PaneID, error) {
101 log := logging.FromContext(ctx)
102
103 tr := wsView.TreeRenderer()
104 if tr == nil {
105 return "", errors.New("tree renderer not available")
106 }
107
108 stackedView := tr.GetStackedViewForPane(string(currentPaneID))
109 if stackedView == nil {
110 return "", ErrStackNotFound
111 }
112
113 // Only navigate if there are multiple panes
114 if stackedView.Count() <= 1 {
115 log.Debug().Msg("only one pane in stack, nothing to navigate")
116 return currentPaneID, nil
117 }
118
119 var err error
120 switch direction {
121 case "up":
122 err = stackedView.NavigatePrevious(ctx)
123 case "down":
124 err = stackedView.NavigateNext(ctx)
125 default:
126 return "", errors.New("invalid direction: use 'up' or 'down'")
127 }
128 if err != nil {
129 return "", err
130 }
131
132 // Get the newly active pane's widget to find its ID
133 // We need to find which pane is now at the active index
134 activeIndex := stackedView.ActiveIndex()
135
136 // Find the pane ID at this index by looking through registered panes
137 // This is a bit indirect but works with the current architecture
138 log.Debug().
139 Str("current_pane", string(currentPaneID)).
140 Str("direction", direction).
141 Int("new_index", activeIndex).
142 Msg("navigated stack")
143
144 // Return empty - caller should update based on domain model
145 return "", nil
146}
147
148// SetStackActiveCallback sets up the callback for when a stack pane is clicked.
149func (spm *StackedPaneManager) SetStackActiveCallback(
150 wsView *WorkspaceView,
151 paneID entity.PaneID,
152 callback func(index int),
153) {
154 tr := wsView.TreeRenderer()
155 if tr == nil {
156 return
157 }
158
159 stackedView := tr.GetStackedViewForPane(string(paneID))
160 if stackedView == nil {
161 return
162 }
163
164 stackedView.SetOnActivate(callback)
165}
166
167// GetStackedView returns the StackedView for a pane.
168func (spm *StackedPaneManager) GetStackedView(wsView *WorkspaceView, paneID entity.PaneID) *layout.StackedView {
169 tr := wsView.TreeRenderer()
170 if tr == nil {
171 return nil
172 }
173 return tr.GetStackedViewForPane(string(paneID))
174}
175
176// IsStacked returns true if the pane is in a stack with multiple panes.
177func (spm *StackedPaneManager) IsStacked(wsView *WorkspaceView, paneID entity.PaneID) bool {
178 sv := spm.GetStackedView(wsView, paneID)
179 return sv != nil && sv.Count() > 1
180}