1package browser
2
3import (
4 "fmt"
5 "sync/atomic"
6
7 "github.com/bnema/dumber/internal/logging"
8 "github.com/bnema/dumber/pkg/webkit"
9 "github.com/diamondburned/gotk4/pkg/gtk/v4"
10)
11
12// createTabBar creates the horizontal tab bar container.
13// Returns the tab bar widget.
14func (tm *TabManager) createTabBar() gtk.Widgetter {
15 logging.Debug("[tabs] Creating tab bar container")
16
17 tabBar := gtk.NewBox(gtk.OrientationHorizontal, 0)
18 if tabBar == nil {
19 logging.Error("[tabs] Failed to create tab bar box")
20 return nil
21 }
22
23 tabBar.AddCSSClass("tab-bar")
24 tabBar.SetHExpand(true)
25 tabBar.SetVExpand(false)
26
27 tm.tabBar = tabBar
28 logging.Debug("[tabs] Tab bar container created")
29 return tabBar
30}
31
32// createTabButton creates a simple GTK button widget for a tab with just a text label.
33func (tm *TabManager) createTabButton(tab *Tab) gtk.Widgetter {
34 logging.Debug(fmt.Sprintf("[tabs] Creating button for tab %s", tab.id))
35
36 // Create button
37 button := gtk.NewButton()
38 if button == nil {
39 logging.Error("[tabs] Failed to create tab button")
40 return nil
41 }
42
43 // CRITICAL: Disable focus-on-click to prevent GTK focus system from interfering
44 // Tab buttons should be clickable without grabbing focus from the WebView
45 button.SetFocusOnClick(false)
46 button.SetCanFocus(false)
47
48 // Apply CSS classes
49 button.AddCSSClass("tab-button")
50
51 // Get display title (custom name or default name)
52 displayTitle := tab.title
53 if tab.customTitle != "" {
54 displayTitle = tab.customTitle
55 }
56
57 // Create title label
58 titleLabel := gtk.NewLabel(displayTitle)
59 if titleLabel != nil {
60 titleLabel.AddCSSClass("tab-title")
61 titleLabel.SetEllipsize(2) // Pango ellipsize mode: middle
62 titleLabel.SetMaxWidthChars(20)
63 webkit.WidgetSetVisible(titleLabel, true)
64 }
65
66 // Set button child
67 button.SetChild(titleLabel)
68
69 // Store button reference for later updates
70 tab.titleButton = button
71
72 // Note: Click handler will be attached in createTabInternal after tab is added to slice
73
74 // Set active class if this is the active tab
75 if tab.isActive {
76 webkit.WidgetAddCSSClass(button, "tab-button-active")
77 }
78
79 webkit.WidgetSetVisible(button, true)
80 logging.Debug(fmt.Sprintf("[tabs] Button created for tab %s", tab.id))
81 return button
82}
83
84// attachTabClickHandler attaches a click handler to switch to a tab when clicked.
85// Uses the same pattern as StackedPaneManager: maps unique button ID -> tab,
86// then looks up current index at click time to handle tab reordering correctly.
87func (tm *TabManager) attachTabClickHandler(button gtk.Widgetter, tab *Tab) {
88 if button == nil || tab == nil {
89 logging.Error("[tabs] Cannot attach click handler: nil button or tab")
90 return
91 }
92
93 gtkButton, ok := button.(*gtk.Button)
94 if !ok {
95 logging.Error(fmt.Sprintf("[tabs] Cannot attach click handler: widget is not *gtk.Button for tab %s", tab.id))
96 return
97 }
98
99 // Generate unique button ID and store mapping (pattern from stacked panes)
100 buttonID := atomic.AddUint64(&tm.nextButtonID, 1)
101 tm.buttonToTab[buttonID] = tab
102
103 gtkButton.ConnectClicked(func() {
104 tm.handleTabButtonClick(buttonID)
105 })
106
107 logging.Debug(fmt.Sprintf("[tabs] Click handler attached to tab %s (buttonID=%d)", tab.id, buttonID))
108}
109
110// handleTabButtonClick handles clicks on tab buttons to switch tabs.
111// Looks up the current index dynamically to handle tab reordering (pattern from stacked panes).
112func (tm *TabManager) handleTabButtonClick(buttonID uint64) {
113 tm.mu.Lock()
114 defer tm.mu.Unlock()
115
116 // Look up tab from button ID
117 tab, exists := tm.buttonToTab[buttonID]
118 if !exists || tab == nil {
119 logging.Warn(fmt.Sprintf("[tabs] Tab button click for unknown ID: %d", buttonID))
120 return
121 }
122
123 // Find current index of this tab
124 targetIndex := -1
125 for i, t := range tm.tabs {
126 if t == tab {
127 targetIndex = i
128 break
129 }
130 }
131
132 if targetIndex < 0 {
133 logging.Error(fmt.Sprintf("[tabs] Tab %s not found in tabs slice", tab.id))
134 return
135 }
136
137 // Only switch if not already active
138 if targetIndex == tm.activeIndex {
139 logging.Debug(fmt.Sprintf("[tabs] Tab %s already active (index %d)", tab.id, targetIndex))
140 return
141 }
142
143 logging.Info(fmt.Sprintf("[tabs] Tab button clicked: switching from tab %d to %d (%s)", tm.activeIndex, targetIndex, tab.id))
144
145 // Switch to this tab
146 if err := tm.switchToTabInternal(targetIndex); err != nil {
147 logging.Error(fmt.Sprintf("[tabs] Failed to switch to tab %d: %v", targetIndex, err))
148 }
149}
150
151// updateTabButton updates the tab button's title label.
152func (tm *TabManager) updateTabButton(tab *Tab) {
153 if tab.titleButton == nil {
154 logging.Warn(fmt.Sprintf("[tabs] Cannot update button for tab %s: button is nil", tab.id))
155 return
156 }
157
158 webkit.RunOnMainThread(func() {
159 button, ok := tab.titleButton.(*gtk.Button)
160 if !ok {
161 logging.Error(fmt.Sprintf("[tabs] Tab button is not a *gtk.Button: %s", tab.id))
162 return
163 }
164
165 // Get button child (the label)
166 child := button.Child()
167 if child == nil {
168 logging.Warn(fmt.Sprintf("[tabs] Tab button has no child: %s", tab.id))
169 return
170 }
171
172 label, ok := child.(*gtk.Label)
173 if !ok {
174 logging.Error(fmt.Sprintf("[tabs] Tab button child is not a *gtk.Label: %s", tab.id))
175 return
176 }
177
178 // Update label text
179 displayTitle := tab.title
180 if tab.customTitle != "" {
181 displayTitle = tab.customTitle
182 }
183 label.SetLabel(displayTitle)
184 logging.Debug(fmt.Sprintf("[tabs] Updated tab button label: %s -> %s", tab.id, displayTitle))
185 })
186}
187
188// addTabToBar adds a tab button to the tab bar.
189func (tm *TabManager) addTabToBar(tab *Tab) {
190 if tm.tabBar == nil {
191 logging.Error("[tabs] Cannot add tab to bar: tab bar is nil")
192 return
193 }
194
195 if tab.titleButton == nil {
196 logging.Error(fmt.Sprintf("[tabs] Cannot add tab %s to bar: button is nil", tab.id))
197 return
198 }
199
200 webkit.RunOnMainThread(func() {
201 if tabBarBox, ok := tm.tabBar.(*gtk.Box); ok {
202 tabBarBox.Append(tab.titleButton)
203 logging.Debug(fmt.Sprintf("[tabs] Added tab %s to tab bar", tab.id))
204 } else {
205 logging.Error("[tabs] Tab bar is not a *gtk.Box")
206 }
207 })
208}
209
210// removeTabFromBar removes a tab button from the tab bar.
211func (tm *TabManager) removeTabFromBar(tab *Tab) {
212 if tm.tabBar == nil || tab.titleButton == nil {
213 return
214 }
215
216 webkit.RunOnMainThread(func() {
217 if tabBarBox, ok := tm.tabBar.(*gtk.Box); ok {
218 tabBarBox.Remove(tab.titleButton)
219 logging.Debug(fmt.Sprintf("[tabs] Removed tab %s from tab bar", tab.id))
220 }
221 })
222}
223
224// setTabActiveStyle applies or removes the active CSS class on a tab button.
225func (tm *TabManager) setTabActiveStyle(tab *Tab, active bool) {
226 if tab.titleButton == nil {
227 logging.Warn(fmt.Sprintf("[tabs] Cannot set active style: button is nil for tab %s", tab.id))
228 return
229 }
230
231 webkit.RunOnMainThread(func() {
232 if active {
233 webkit.WidgetAddCSSClass(tab.titleButton, "tab-button-active")
234 logging.Debug(fmt.Sprintf("[tabs] Added active class to tab %s", tab.id))
235 } else {
236 webkit.WidgetRemoveCSSClass(tab.titleButton, "tab-button-active")
237 logging.Debug(fmt.Sprintf("[tabs] Removed active class from tab %s", tab.id))
238 }
239 })
240}