detached 9134c7b

feat(omnibox): add dynamic sizing, zoom indicator, and UI scale support

commit
9134c7bf020d30d46ac84264386ca33d093b2bc3
author
brice · · 2025-12-15 19:49 UTC
committer
brice · · 2025-12-15 22:11 UTC
parent
453b589
signature
unsigned
1 file · +76 -3
M internal/ui/component/omnibox.go +76 -3
@@ -2,6 +2,7 @@ package component
22
33 import (
44 "context"
5+ "fmt"
56 "strings"
67 "sync"
78 "time"
@@ -17,7 +18,8 @@ import (
1718 )
1819
1920 const (
20- omniboxWidth = 600
21+ omniboxWidthPct = 0.5 // 50% of parent window width
22+ omniboxHeightPct = 0.6 // 60% of parent window height
2123 omniboxMaxResults = 10
2224 debounceDelayMs = 250 // Increased for stability
2325 )
@@ -54,6 +56,7 @@ type Omnibox struct {
5456 headerBox *gtk.Box
5557 historyBtn *gtk.Button
5658 favoritesBtn *gtk.Button
59+ zoomLabel *gtk.Label
5760 entry *gtk.SearchEntry
5861 scrolledWin *gtk.ScrolledWindow
5962 listBox *gtk.ListBox
@@ -86,6 +89,9 @@ type Omnibox struct {
8689 debounceTimer *time.Timer
8790 debounceMu sync.Mutex
8891 lastQuery string // Prevent duplicate searches
92+
93+ // Scaling
94+ uiScale float64
8995 }
9096
9197 // OmniboxConfig holds configuration for creating an Omnibox.
@@ -96,12 +102,18 @@ type OmniboxConfig struct {
96102 Shortcuts map[string]config.SearchShortcut
97103 DefaultSearch string
98104 InitialBehavior string
105+ UIScale float64 // UI scale for favicon sizing
99106 }
100107
101108 // NewOmnibox creates a new native GTK4 omnibox widget.
102109 func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxConfig) *Omnibox {
103110 log := logging.FromContext(ctx)
104111
112+ uiScale := cfg.UIScale
113+ if uiScale <= 0 {
114+ uiScale = 1.0
115+ }
116+
105117 o := &Omnibox{
106118 parentWindow: parent,
107119 viewMode: ViewModeHistory,
@@ -113,6 +125,7 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC
113125 defaultSearch: cfg.DefaultSearch,
114126 initialBehavior: cfg.InitialBehavior,
115127 ctx: ctx,
128+ uiScale: uiScale,
116129 }
117130
118131 if err := o.createWidgets(); err != nil {
@@ -127,6 +140,30 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC
127140 return o
128141 }
129142
143+// updateSize sets the omnibox size based on parent window dimensions.
144+func (o *Omnibox) updateSize() {
145+ if o.parentWindow == nil || o.window == nil {
146+ return
147+ }
148+
149+ // Use GetAllocatedWidth/Height for actual rendered size
150+ parentWidth := o.parentWindow.GetAllocatedWidth()
151+ parentHeight := o.parentWindow.GetAllocatedHeight()
152+
153+ if parentWidth <= 0 || parentHeight <= 0 {
154+ log := logging.FromContext(o.ctx)
155+ log.Error().
156+ Int("parent_width", parentWidth).
157+ Int("parent_height", parentHeight).
158+ Msg("omnibox: invalid parent window dimensions")
159+ return
160+ }
161+
162+ width := int(float64(parentWidth) * omniboxWidthPct)
163+ height := int(float64(parentHeight) * omniboxHeightPct)
164+ o.window.SetDefaultSize(width, height)
165+}
166+
130167 // createWidgets builds the GTK widget hierarchy.
131168 func (o *Omnibox) createWidgets() error {
132169 // Create the popup window
@@ -137,12 +174,14 @@ func (o *Omnibox) createWidgets() error {
137174
138175 o.window.SetModal(true)
139176 o.window.SetDecorated(false)
140- o.window.SetDefaultSize(omniboxWidth, 400)
141177 o.window.AddCssClass("omnibox-window")
142178
143179 // Set transient for parent window (positioning)
144180 if o.parentWindow != nil {
145181 o.window.SetTransientFor(&o.parentWindow.Window)
182+ } else {
183+ log := logging.FromContext(o.ctx)
184+ log.Error().Msg("omnibox: parent window is nil, cannot set transient")
146185 }
147186
148187 // Main vertical box
@@ -186,8 +225,20 @@ func (o *Omnibox) createWidgets() error {
186225 }
187226 o.favoritesBtn.ConnectClicked(&favoritesClickCb)
188227
228+ // Zoom indicator label (shown when zoom != 100%)
229+ o.zoomLabel = gtk.NewLabel(nil)
230+ if o.zoomLabel != nil {
231+ o.zoomLabel.AddCssClass("omnibox-zoom-indicator")
232+ o.zoomLabel.SetVisible(false)
233+ o.zoomLabel.SetHalign(gtk.AlignEndValue)
234+ o.zoomLabel.SetHexpand(true)
235+ }
236+
189237 o.headerBox.Append(&o.historyBtn.Widget)
190238 o.headerBox.Append(&o.favoritesBtn.Widget)
239+ if o.zoomLabel != nil {
240+ o.headerBox.Append(&o.zoomLabel.Widget)
241+ }
191242
192243 // Search entry field
193244 o.entry = gtk.NewSearchEntry()
@@ -546,7 +597,7 @@ func (o *Omnibox) createFaviconImage(url, fallbackIcon string) *gtk.Image {
546597 return nil
547598 }
548599 favicon.SetFromIconName(&fallbackIcon)
549- favicon.SetPixelSize(16)
600+ favicon.SetPixelSize(int(16 * o.uiScale))
550601 favicon.AddCssClass("omnibox-favicon")
551602
552603 // Async load favicon from cache
@@ -892,6 +943,9 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
892943 // Load initial data
893944 o.performSearch()
894945
946+ // Update size based on current parent window dimensions
947+ o.updateSize()
948+
895949 // Present the window
896950 o.window.Present()
897951
@@ -962,6 +1016,25 @@ func (o *Omnibox) SetOnClose(fn func()) {
9621016 o.onClose = fn
9631017 }
9641018
1019+// UpdateZoomIndicator updates the zoom percentage display.
1020+// Shows the indicator when zoom != 100%, hides it when at 100%.
1021+func (o *Omnibox) UpdateZoomIndicator(factor float64) {
1022+ var cb glib.SourceFunc = func(data uintptr) bool {
1023+ if o.zoomLabel == nil {
1024+ return false
1025+ }
1026+ if factor == 1.0 {
1027+ o.zoomLabel.SetVisible(false)
1028+ } else {
1029+ percentage := int(factor * 100)
1030+ o.zoomLabel.SetText(fmt.Sprintf("%d%%", percentage))
1031+ o.zoomLabel.SetVisible(true)
1032+ }
1033+ return false
1034+ }
1035+ glib.IdleAdd(&cb, 0)
1036+}
1037+
9651038 // idleAddUpdateSuggestions schedules updateSuggestions on the GTK main thread.
9661039 func (o *Omnibox) idleAddUpdateSuggestions(suggestions []Suggestion) {
9671040 var cb glib.SourceFunc = func(data uintptr) bool {
@@ -2,6 +2,7 @@ package component
2 2
3 import (3 import (
4 "context"4 "context"
5+ "fmt"
5 "strings"6 "strings"
6 "sync"7 "sync"
7 "time"8 "time"
@@ -17,7 +18,8 @@ import (
17 )18 )
18 19
19 const (20 const (
20- omniboxWidth = 60021+ omniboxWidthPct = 0.5 // 50% of parent window width
22+ omniboxHeightPct = 0.6 // 60% of parent window height
21 omniboxMaxResults = 1023 omniboxMaxResults = 10
22 debounceDelayMs = 250 // Increased for stability24 debounceDelayMs = 250 // Increased for stability
23 )25 )
@@ -54,6 +56,7 @@ type Omnibox struct {
54 headerBox *gtk.Box56 headerBox *gtk.Box
55 historyBtn *gtk.Button57 historyBtn *gtk.Button
56 favoritesBtn *gtk.Button58 favoritesBtn *gtk.Button
59+ zoomLabel *gtk.Label
57 entry *gtk.SearchEntry60 entry *gtk.SearchEntry
58 scrolledWin *gtk.ScrolledWindow61 scrolledWin *gtk.ScrolledWindow
59 listBox *gtk.ListBox62 listBox *gtk.ListBox
@@ -86,6 +89,9 @@ type Omnibox struct {
86 debounceTimer *time.Timer89 debounceTimer *time.Timer
87 debounceMu sync.Mutex90 debounceMu sync.Mutex
88 lastQuery string // Prevent duplicate searches91 lastQuery string // Prevent duplicate searches
92+
93+ // Scaling
94+ uiScale float64
89 }95 }
90 96
91 // OmniboxConfig holds configuration for creating an Omnibox.97 // OmniboxConfig holds configuration for creating an Omnibox.
@@ -96,12 +102,18 @@ type OmniboxConfig struct {
96 Shortcuts map[string]config.SearchShortcut102 Shortcuts map[string]config.SearchShortcut
97 DefaultSearch string103 DefaultSearch string
98 InitialBehavior string104 InitialBehavior string
105+ UIScale float64 // UI scale for favicon sizing
99 }106 }
100 107
101 // NewOmnibox creates a new native GTK4 omnibox widget.108 // NewOmnibox creates a new native GTK4 omnibox widget.
102 func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxConfig) *Omnibox {109 func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxConfig) *Omnibox {
103 log := logging.FromContext(ctx)110 log := logging.FromContext(ctx)
104 111
112+ uiScale := cfg.UIScale
113+ if uiScale <= 0 {
114+ uiScale = 1.0
115+ }
116+
105 o := &Omnibox{117 o := &Omnibox{
106 parentWindow: parent,118 parentWindow: parent,
107 viewMode: ViewModeHistory,119 viewMode: ViewModeHistory,
@@ -113,6 +125,7 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC
113 defaultSearch: cfg.DefaultSearch,125 defaultSearch: cfg.DefaultSearch,
114 initialBehavior: cfg.InitialBehavior,126 initialBehavior: cfg.InitialBehavior,
115 ctx: ctx,127 ctx: ctx,
128+ uiScale: uiScale,
116 }129 }
117 130
118 if err := o.createWidgets(); err != nil {131 if err := o.createWidgets(); err != nil {
@@ -127,6 +140,30 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC
127 return o140 return o
128 }141 }
129 142
143+// updateSize sets the omnibox size based on parent window dimensions.
144+func (o *Omnibox) updateSize() {
145+ if o.parentWindow == nil || o.window == nil {
146+ return
147+ }
148+
149+ // Use GetAllocatedWidth/Height for actual rendered size
150+ parentWidth := o.parentWindow.GetAllocatedWidth()
151+ parentHeight := o.parentWindow.GetAllocatedHeight()
152+
153+ if parentWidth <= 0 || parentHeight <= 0 {
154+ log := logging.FromContext(o.ctx)
155+ log.Error().
156+ Int("parent_width", parentWidth).
157+ Int("parent_height", parentHeight).
158+ Msg("omnibox: invalid parent window dimensions")
159+ return
160+ }
161+
162+ width := int(float64(parentWidth) * omniboxWidthPct)
163+ height := int(float64(parentHeight) * omniboxHeightPct)
164+ o.window.SetDefaultSize(width, height)
165+}
166+
130 // createWidgets builds the GTK widget hierarchy.167 // createWidgets builds the GTK widget hierarchy.
131 func (o *Omnibox) createWidgets() error {168 func (o *Omnibox) createWidgets() error {
132 // Create the popup window169 // Create the popup window
@@ -137,12 +174,14 @@ func (o *Omnibox) createWidgets() error {
137 174
138 o.window.SetModal(true)175 o.window.SetModal(true)
139 o.window.SetDecorated(false)176 o.window.SetDecorated(false)
140- o.window.SetDefaultSize(omniboxWidth, 400)
141 o.window.AddCssClass("omnibox-window")177 o.window.AddCssClass("omnibox-window")
142 178
143 // Set transient for parent window (positioning)179 // Set transient for parent window (positioning)
144 if o.parentWindow != nil {180 if o.parentWindow != nil {
145 o.window.SetTransientFor(&o.parentWindow.Window)181 o.window.SetTransientFor(&o.parentWindow.Window)
182+ } else {
183+ log := logging.FromContext(o.ctx)
184+ log.Error().Msg("omnibox: parent window is nil, cannot set transient")
146 }185 }
147 186
148 // Main vertical box187 // Main vertical box
@@ -186,8 +225,20 @@ func (o *Omnibox) createWidgets() error {
186 }225 }
187 o.favoritesBtn.ConnectClicked(&favoritesClickCb)226 o.favoritesBtn.ConnectClicked(&favoritesClickCb)
188 227
228+ // Zoom indicator label (shown when zoom != 100%)
229+ o.zoomLabel = gtk.NewLabel(nil)
230+ if o.zoomLabel != nil {
231+ o.zoomLabel.AddCssClass("omnibox-zoom-indicator")
232+ o.zoomLabel.SetVisible(false)
233+ o.zoomLabel.SetHalign(gtk.AlignEndValue)
234+ o.zoomLabel.SetHexpand(true)
235+ }
236+
189 o.headerBox.Append(&o.historyBtn.Widget)237 o.headerBox.Append(&o.historyBtn.Widget)
190 o.headerBox.Append(&o.favoritesBtn.Widget)238 o.headerBox.Append(&o.favoritesBtn.Widget)
239+ if o.zoomLabel != nil {
240+ o.headerBox.Append(&o.zoomLabel.Widget)
241+ }
191 242
192 // Search entry field243 // Search entry field
193 o.entry = gtk.NewSearchEntry()244 o.entry = gtk.NewSearchEntry()
@@ -546,7 +597,7 @@ func (o *Omnibox) createFaviconImage(url, fallbackIcon string) *gtk.Image {
546 return nil597 return nil
547 }598 }
548 favicon.SetFromIconName(&fallbackIcon)599 favicon.SetFromIconName(&fallbackIcon)
549- favicon.SetPixelSize(16)600+ favicon.SetPixelSize(int(16 * o.uiScale))
550 favicon.AddCssClass("omnibox-favicon")601 favicon.AddCssClass("omnibox-favicon")
551 602
552 // Async load favicon from cache603 // Async load favicon from cache
@@ -892,6 +943,9 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
892 // Load initial data943 // Load initial data
893 o.performSearch()944 o.performSearch()
894 945
946+ // Update size based on current parent window dimensions
947+ o.updateSize()
948+
895 // Present the window949 // Present the window
896 o.window.Present()950 o.window.Present()
897 951
@@ -962,6 +1016,25 @@ func (o *Omnibox) SetOnClose(fn func()) {
962 o.onClose = fn1016 o.onClose = fn
963 }1017 }
964 1018
1019+// UpdateZoomIndicator updates the zoom percentage display.
1020+// Shows the indicator when zoom != 100%, hides it when at 100%.
1021+func (o *Omnibox) UpdateZoomIndicator(factor float64) {
1022+ var cb glib.SourceFunc = func(data uintptr) bool {
1023+ if o.zoomLabel == nil {
1024+ return false
1025+ }
1026+ if factor == 1.0 {
1027+ o.zoomLabel.SetVisible(false)
1028+ } else {
1029+ percentage := int(factor * 100)
1030+ o.zoomLabel.SetText(fmt.Sprintf("%d%%", percentage))
1031+ o.zoomLabel.SetVisible(true)
1032+ }
1033+ return false
1034+ }
1035+ glib.IdleAdd(&cb, 0)
1036+}
1037+
965 // idleAddUpdateSuggestions schedules updateSuggestions on the GTK main thread.1038 // idleAddUpdateSuggestions schedules updateSuggestions on the GTK main thread.
966 func (o *Omnibox) idleAddUpdateSuggestions(suggestions []Suggestion) {1039 func (o *Omnibox) idleAddUpdateSuggestions(suggestions []Suggestion) {
967 var cb glib.SourceFunc = func(data uintptr) bool {1040 var cb glib.SourceFunc = func(data uintptr) bool {

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