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