main_window.go RawHistoryFolder
1// Package window provides GTK window implementations.
2package window
3
4import (
5 "context"
6
7 "github.com/bnema/dumber/internal/infrastructure/config"
8 "github.com/bnema/dumber/internal/logging"
9 "github.com/bnema/dumber/internal/ui/component"
10 "github.com/jwijenbergh/puregotk/v4/gtk"
11 "github.com/rs/zerolog"
12)
13
14const (
15 defaultWidth = 1280
16 defaultHeight = 800
17 windowTitle = "Dumber"
18)
19
20// MainWindow represents the main browser window.
21type MainWindow struct {
22 window *gtk.ApplicationWindow
23 rootBox *gtk.Box // Vertical: tab bar + content
24 tabBar *component.TabBar
25 contentOverlay *gtk.Overlay // Overlay for content + omnibox
26 contentArea *gtk.Box // Container for workspace content
27 currentContent *gtk.Widget // Track current content for removal on tab switch
28
29 cfg *config.Config
30 logger zerolog.Logger
31}
32
33// New creates a new main browser window.
34func New(ctx context.Context, app *gtk.Application, cfg *config.Config) (*MainWindow, error) {
35 log := logging.FromContext(ctx)
36
37 mw := &MainWindow{
38 cfg: cfg,
39 logger: log.With().Str("component", "main-window").Logger(),
40 }
41
42 // Create the application window
43 mw.window = gtk.NewApplicationWindow(app)
44 if mw.window == nil {
45 return nil, ErrWindowCreationFailed
46 }
47
48 // Configure window
49 title := windowTitle
50 mw.window.SetTitle(&title)
51 mw.window.SetDefaultSize(defaultWidth, defaultHeight)
52
53 // Create root container (vertical box)
54 mw.rootBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
55 if mw.rootBox == nil {
56 mw.window.Unref()
57 return nil, ErrWidgetCreationFailed("rootBox")
58 }
59 mw.rootBox.SetHexpand(true)
60 mw.rootBox.SetVexpand(true)
61 mw.rootBox.SetVisible(true)
62
63 // Create tab bar
64 mw.tabBar = component.NewTabBar()
65 if mw.tabBar == nil {
66 mw.rootBox.Unref()
67 mw.window.Unref()
68 return nil, ErrWidgetCreationFailed("tabBar")
69 }
70
71 // Create content overlay (for omnibox and other overlays)
72 mw.contentOverlay = gtk.NewOverlay()
73 if mw.contentOverlay == nil {
74 mw.tabBar.Destroy()
75 mw.rootBox.Unref()
76 mw.window.Unref()
77 return nil, ErrWidgetCreationFailed("contentOverlay")
78 }
79 mw.contentOverlay.SetHexpand(true)
80 mw.contentOverlay.SetVexpand(true)
81 mw.contentOverlay.SetVisible(true)
82
83 // Create content area
84 mw.contentArea = gtk.NewBox(gtk.OrientationVerticalValue, 0)
85 if mw.contentArea == nil {
86 mw.contentOverlay.Unref()
87 mw.tabBar.Destroy()
88 mw.rootBox.Unref()
89 mw.window.Unref()
90 return nil, ErrWidgetCreationFailed("contentArea")
91 }
92 mw.contentArea.SetHexpand(true)
93 mw.contentArea.SetVexpand(true)
94 mw.contentArea.SetVisible(true)
95 mw.contentArea.AddCssClass("content-area")
96
97 // Set content area as the main child of the overlay
98 mw.contentOverlay.SetChild(&mw.contentArea.Widget)
99
100 // Assemble layout based on tab bar position
101 mw.assembleLayout()
102
103 // Set root box as window content
104 mw.window.SetChild(&mw.rootBox.Widget)
105
106 return mw, nil
107}
108
109// assembleLayout arranges widgets based on configuration.
110func (mw *MainWindow) assembleLayout() {
111 tabBarPos := "top" // default
112 if mw.cfg != nil {
113 tabBarPos = mw.cfg.Workspace.TabBarPosition
114 }
115
116 if tabBarPos == "bottom" {
117 // Content first, then tab bar
118 mw.rootBox.Append(&mw.contentOverlay.Widget)
119 mw.rootBox.Append(mw.tabBar.Widget())
120 } else {
121 // Tab bar first, then content (default: top)
122 mw.rootBox.Append(mw.tabBar.Widget())
123 mw.rootBox.Append(&mw.contentOverlay.Widget)
124 }
125
126 mw.logger.Debug().
127 Str("tab_bar_position", tabBarPos).
128 Msg("window layout assembled")
129}
130
131// Show makes the window visible.
132func (mw *MainWindow) Show() {
133 mw.window.Present()
134}
135
136// Close closes the window.
137func (mw *MainWindow) Close() {
138 mw.window.Close()
139}
140
141// TabBar returns the tab bar component.
142func (mw *MainWindow) TabBar() *component.TabBar {
143 return mw.tabBar
144}
145
146// ContentArea returns the content area box.
147func (mw *MainWindow) ContentArea() *gtk.Box {
148 return mw.contentArea
149}
150
151// SetContent sets the content of the content area.
152// This removes any existing content first to properly swap workspace views on tab switch.
153func (mw *MainWindow) SetContent(widget *gtk.Widget) {
154 // Remove existing content first
155 if mw.currentContent != nil {
156 mw.contentArea.Remove(mw.currentContent)
157 mw.currentContent = nil
158 }
159
160 // Add new content
161 if widget != nil {
162 widget.SetVisible(true)
163 mw.contentArea.Append(widget)
164 mw.currentContent = widget
165 }
166}
167
168// Window returns the underlying GTK window.
169func (mw *MainWindow) Window() *gtk.ApplicationWindow {
170 return mw.window
171}
172
173// SetTitle updates the window title with proper formatting.
174// The title is capped at 255 characters for display.
175func (mw *MainWindow) SetTitle(title string) {
176 if mw.window == nil {
177 return
178 }
179 // Truncate title to 255 characters max (by runes, to avoid breaking UTF-8)
180 const maxTitleLen = 255
181 runes := []rune(title)
182 if len(runes) > maxTitleLen {
183 title = string(runes[:maxTitleLen-3]) + "..."
184 }
185 mw.window.SetTitle(&title)
186}
187
188// AddOverlay adds a widget to the content overlay.
189// The widget will be displayed on top of the workspace content.
190func (mw *MainWindow) AddOverlay(widget *gtk.Widget) {
191 if mw.contentOverlay != nil && widget != nil {
192 mw.contentOverlay.AddOverlay(widget)
193 }
194}
195
196// Destroy cleans up window resources.
197func (mw *MainWindow) Destroy() {
198 if mw.tabBar != nil {
199 mw.tabBar.Destroy()
200 mw.tabBar = nil
201 }
202 if mw.contentArea != nil {
203 mw.contentArea.Unref()
204 mw.contentArea = nil
205 }
206 if mw.rootBox != nil {
207 mw.rootBox.Unref()
208 mw.rootBox = nil
209 }
210 if mw.window != nil {
211 mw.window.Destroy()
212 mw.window = nil
213 }
214}
215
216// WindowError represents a window-related error.
217type WindowError struct {
218 Message string
219}
220
221func (e WindowError) Error() string {
222 return e.Message
223}
224
225// Error constants.
226var (
227 ErrWindowCreationFailed = WindowError{Message: "failed to create application window"}
228)
229
230// ErrWidgetCreationFailed creates an error for widget creation failure.
231func ErrWidgetCreationFailed(name string) error {
232 return WindowError{Message: "failed to create widget: " + name}
233}