detached adc2dc4

feat(window): add overlay infrastructure for positioned UI elements

commit
adc2dc4430a9b1e9b700a9921680d3d5b78f5d87
author
bnema · · 2025-12-16 21:41 UTC
parent
12a6535
signature
unsigned
1 file · +31 -6
M internal/ui/window/main_window.go +31 -6
@@ -19,10 +19,11 @@ const (
1919
2020 // MainWindow represents the main browser window.
2121 type MainWindow struct {
22- window *gtk.ApplicationWindow
23- rootBox *gtk.Box // Vertical: tab bar + content
24- tabBar *component.TabBar
25- contentArea *gtk.Box // Container for workspace content
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
2627
2728 cfg *config.Config
2829 logger zerolog.Logger
@@ -66,9 +67,22 @@ func New(ctx context.Context, app *gtk.Application, cfg *config.Config) (*MainWi
6667 return nil, ErrWidgetCreationFailed("tabBar")
6768 }
6869
70+ // Create content overlay (for omnibox and other overlays)
71+ mw.contentOverlay = gtk.NewOverlay()
72+ if mw.contentOverlay == nil {
73+ mw.tabBar.Destroy()
74+ mw.rootBox.Unref()
75+ mw.window.Unref()
76+ return nil, ErrWidgetCreationFailed("contentOverlay")
77+ }
78+ mw.contentOverlay.SetHexpand(true)
79+ mw.contentOverlay.SetVexpand(true)
80+ mw.contentOverlay.SetVisible(true)
81+
6982 // Create content area
7083 mw.contentArea = gtk.NewBox(gtk.OrientationVerticalValue, 0)
7184 if mw.contentArea == nil {
85+ mw.contentOverlay.Unref()
7286 mw.tabBar.Destroy()
7387 mw.rootBox.Unref()
7488 mw.window.Unref()
@@ -79,6 +93,9 @@ func New(ctx context.Context, app *gtk.Application, cfg *config.Config) (*MainWi
7993 mw.contentArea.SetVisible(true)
8094 mw.contentArea.AddCssClass("content-area")
8195
96+ // Set content area as the main child of the overlay
97+ mw.contentOverlay.SetChild(&mw.contentArea.Widget)
98+
8299 // Assemble layout based on tab bar position
83100 mw.assembleLayout()
84101
@@ -97,12 +114,12 @@ func (mw *MainWindow) assembleLayout() {
97114
98115 if tabBarPos == "bottom" {
99116 // Content first, then tab bar
100- mw.rootBox.Append(&mw.contentArea.Widget)
117+ mw.rootBox.Append(&mw.contentOverlay.Widget)
101118 mw.rootBox.Append(mw.tabBar.Widget())
102119 } else {
103120 // Tab bar first, then content (default: top)
104121 mw.rootBox.Append(mw.tabBar.Widget())
105- mw.rootBox.Append(&mw.contentArea.Widget)
122+ mw.rootBox.Append(&mw.contentOverlay.Widget)
106123 }
107124
108125 mw.logger.Debug().
@@ -148,6 +165,14 @@ func (mw *MainWindow) Window() *gtk.ApplicationWindow {
148165 return mw.window
149166 }
150167
168+// AddOverlay adds a widget to the content overlay.
169+// The widget will be displayed on top of the workspace content.
170+func (mw *MainWindow) AddOverlay(widget *gtk.Widget) {
171+ if mw.contentOverlay != nil && widget != nil {
172+ mw.contentOverlay.AddOverlay(widget)
173+ }
174+}
175+
151176 // Destroy cleans up window resources.
152177 func (mw *MainWindow) Destroy() {
153178 if mw.tabBar != nil {
@@ -19,10 +19,11 @@ const (
19 19
20 // MainWindow represents the main browser window.20 // MainWindow represents the main browser window.
21 type MainWindow struct {21 type MainWindow struct {
22- window *gtk.ApplicationWindow22+ window *gtk.ApplicationWindow
23- rootBox *gtk.Box // Vertical: tab bar + content23+ rootBox *gtk.Box // Vertical: tab bar + content
24- tabBar *component.TabBar24+ tabBar *component.TabBar
25- contentArea *gtk.Box // Container for workspace content25+ contentOverlay *gtk.Overlay // Overlay for content + omnibox
26+ contentArea *gtk.Box // Container for workspace content
26 27
27 cfg *config.Config28 cfg *config.Config
28 logger zerolog.Logger29 logger zerolog.Logger
@@ -66,9 +67,22 @@ func New(ctx context.Context, app *gtk.Application, cfg *config.Config) (*MainWi
66 return nil, ErrWidgetCreationFailed("tabBar")67 return nil, ErrWidgetCreationFailed("tabBar")
67 }68 }
68 69
70+ // Create content overlay (for omnibox and other overlays)
71+ mw.contentOverlay = gtk.NewOverlay()
72+ if mw.contentOverlay == nil {
73+ mw.tabBar.Destroy()
74+ mw.rootBox.Unref()
75+ mw.window.Unref()
76+ return nil, ErrWidgetCreationFailed("contentOverlay")
77+ }
78+ mw.contentOverlay.SetHexpand(true)
79+ mw.contentOverlay.SetVexpand(true)
80+ mw.contentOverlay.SetVisible(true)
81+
69 // Create content area82 // Create content area
70 mw.contentArea = gtk.NewBox(gtk.OrientationVerticalValue, 0)83 mw.contentArea = gtk.NewBox(gtk.OrientationVerticalValue, 0)
71 if mw.contentArea == nil {84 if mw.contentArea == nil {
85+ mw.contentOverlay.Unref()
72 mw.tabBar.Destroy()86 mw.tabBar.Destroy()
73 mw.rootBox.Unref()87 mw.rootBox.Unref()
74 mw.window.Unref()88 mw.window.Unref()
@@ -79,6 +93,9 @@ func New(ctx context.Context, app *gtk.Application, cfg *config.Config) (*MainWi
79 mw.contentArea.SetVisible(true)93 mw.contentArea.SetVisible(true)
80 mw.contentArea.AddCssClass("content-area")94 mw.contentArea.AddCssClass("content-area")
81 95
96+ // Set content area as the main child of the overlay
97+ mw.contentOverlay.SetChild(&mw.contentArea.Widget)
98+
82 // Assemble layout based on tab bar position99 // Assemble layout based on tab bar position
83 mw.assembleLayout()100 mw.assembleLayout()
84 101
@@ -97,12 +114,12 @@ func (mw *MainWindow) assembleLayout() {
97 114
98 if tabBarPos == "bottom" {115 if tabBarPos == "bottom" {
99 // Content first, then tab bar116 // Content first, then tab bar
100- mw.rootBox.Append(&mw.contentArea.Widget)117+ mw.rootBox.Append(&mw.contentOverlay.Widget)
101 mw.rootBox.Append(mw.tabBar.Widget())118 mw.rootBox.Append(mw.tabBar.Widget())
102 } else {119 } else {
103 // Tab bar first, then content (default: top)120 // Tab bar first, then content (default: top)
104 mw.rootBox.Append(mw.tabBar.Widget())121 mw.rootBox.Append(mw.tabBar.Widget())
105- mw.rootBox.Append(&mw.contentArea.Widget)122+ mw.rootBox.Append(&mw.contentOverlay.Widget)
106 }123 }
107 124
108 mw.logger.Debug().125 mw.logger.Debug().
@@ -148,6 +165,14 @@ func (mw *MainWindow) Window() *gtk.ApplicationWindow {
148 return mw.window165 return mw.window
149 }166 }
150 167
168+// AddOverlay adds a widget to the content overlay.
169+// The widget will be displayed on top of the workspace content.
170+func (mw *MainWindow) AddOverlay(widget *gtk.Widget) {
171+ if mw.contentOverlay != nil && widget != nil {
172+ mw.contentOverlay.AddOverlay(widget)
173+ }
174+}
175+
151 // Destroy cleans up window resources.176 // Destroy cleans up window resources.
152 func (mw *MainWindow) Destroy() {177 func (mw *MainWindow) Destroy() {
153 if mw.tabBar != nil {178 if mw.tabBar != nil {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close