detached 5b585ba

feat: adapt omnibox result count to parent pane height

When the parent pane is too short to fit 10 results, the omnibox
now dynamically switches to showing 5 results. This prevents the
bottom of the omnibox from being clipped off-screen in small panes.

The effective row count is computed from available space (parent
height minus top margin and chrome) rather than a fixed threshold,
so it adapts correctly across different UI scales.
commit
5b585ba6b1593a26b5dd1bc3e1b91216032160ce
author
brice · · 2026-01-29 06:08 UTC
committer
brice · · 2026-01-29 08:42 UTC
parent
62a3808
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +60 -13
M internal/ui/component/modal_sizing.go +25 -4
@@ -24,8 +24,9 @@ type RowHeightDefaults struct {
2424
2525 // ListDisplayDefaults holds display limits for list-based modals.
2626 type ListDisplayDefaults struct {
27- MaxVisibleRows int // Maximum rows visible before scrolling
28- MaxResults int // Maximum results to fetch/display
27+ MaxVisibleRows int // Maximum rows visible before scrolling
28+ MaxResults int // Maximum results to fetch/display
29+ SmallMaxVisibleRows int // Reduced row count when rows don't fit (0 = no adaptation)
2930 }
3031
3132 // Package-level defaults for row heights.
@@ -46,8 +47,9 @@ var OmniboxSizeDefaults = ModalSizeConfig{
4647
4748 // OmniboxListDefaults provides display limits for omnibox modal.
4849 var OmniboxListDefaults = ListDisplayDefaults{
49- MaxVisibleRows: 10,
50- MaxResults: 10,
50+ MaxVisibleRows: 10,
51+ MaxResults: 10,
52+ SmallMaxVisibleRows: 5,
5153 }
5254
5355 // SessionManagerSizeDefaults provides default sizing for session manager modal.
@@ -80,6 +82,25 @@ var TabPickerListDefaults = ListDisplayDefaults{
8082 MaxResults: 20,
8183 }
8284
85+// EffectiveMaxRows returns the maximum number of visible rows that fit
86+// in the available space. It computes available height by subtracting the
87+// top margin (TopMarginPct of parentHeight) and an estimated chrome height
88+// (header + search entry ≈ 120px). If MaxVisibleRows worth of rows don't
89+// fit, it falls back to SmallMaxVisibleRows.
90+func EffectiveMaxRows(parentHeight, rowHeight int, sizeCfg ModalSizeConfig, defaults ListDisplayDefaults) int {
91+ if parentHeight <= 0 || rowHeight <= 0 || defaults.SmallMaxVisibleRows <= 0 {
92+ return defaults.MaxVisibleRows
93+ }
94+ topMargin := int(float64(parentHeight) * sizeCfg.TopMarginPct)
95+ const chromeHeight = 150 // header tabs + search entry + bottom padding
96+ available := parentHeight - topMargin - chromeHeight
97+ needed := defaults.MaxVisibleRows * rowHeight
98+ if available < needed {
99+ return defaults.SmallMaxVisibleRows
100+ }
101+ return defaults.MaxVisibleRows
102+}
103+
83104 // CalculateModalDimensions computes width and top margin based on parent overlay.
84105 // Returns calculated width and top margin in pixels.
85106 func CalculateModalDimensions(parent layout.OverlayWidget, cfg ModalSizeConfig) (width, marginTop int) {
@@ -24,8 +24,9 @@ type RowHeightDefaults struct {
24 24
25 // ListDisplayDefaults holds display limits for list-based modals.25 // ListDisplayDefaults holds display limits for list-based modals.
26 type ListDisplayDefaults struct {26 type ListDisplayDefaults struct {
27- MaxVisibleRows int // Maximum rows visible before scrolling27+ MaxVisibleRows int // Maximum rows visible before scrolling
28- MaxResults int // Maximum results to fetch/display28+ MaxResults int // Maximum results to fetch/display
29+ SmallMaxVisibleRows int // Reduced row count when rows don't fit (0 = no adaptation)
29 }30 }
30 31
31 // Package-level defaults for row heights.32 // Package-level defaults for row heights.
@@ -46,8 +47,9 @@ var OmniboxSizeDefaults = ModalSizeConfig{
46 47
47 // OmniboxListDefaults provides display limits for omnibox modal.48 // OmniboxListDefaults provides display limits for omnibox modal.
48 var OmniboxListDefaults = ListDisplayDefaults{49 var OmniboxListDefaults = ListDisplayDefaults{
49- MaxVisibleRows: 10,50+ MaxVisibleRows: 10,
50- MaxResults: 10,51+ MaxResults: 10,
52+ SmallMaxVisibleRows: 5,
51 }53 }
52 54
53 // SessionManagerSizeDefaults provides default sizing for session manager modal.55 // SessionManagerSizeDefaults provides default sizing for session manager modal.
@@ -80,6 +82,25 @@ var TabPickerListDefaults = ListDisplayDefaults{
80 MaxResults: 20,82 MaxResults: 20,
81 }83 }
82 84
85+// EffectiveMaxRows returns the maximum number of visible rows that fit
86+// in the available space. It computes available height by subtracting the
87+// top margin (TopMarginPct of parentHeight) and an estimated chrome height
88+// (header + search entry ≈ 120px). If MaxVisibleRows worth of rows don't
89+// fit, it falls back to SmallMaxVisibleRows.
90+func EffectiveMaxRows(parentHeight, rowHeight int, sizeCfg ModalSizeConfig, defaults ListDisplayDefaults) int {
91+ if parentHeight <= 0 || rowHeight <= 0 || defaults.SmallMaxVisibleRows <= 0 {
92+ return defaults.MaxVisibleRows
93+ }
94+ topMargin := int(float64(parentHeight) * sizeCfg.TopMarginPct)
95+ const chromeHeight = 150 // header tabs + search entry + bottom padding
96+ available := parentHeight - topMargin - chromeHeight
97+ needed := defaults.MaxVisibleRows * rowHeight
98+ if available < needed {
99+ return defaults.SmallMaxVisibleRows
100+ }
101+ return defaults.MaxVisibleRows
102+}
103+
83 // CalculateModalDimensions computes width and top margin based on parent overlay.104 // CalculateModalDimensions computes width and top margin based on parent overlay.
84 // Returns calculated width and top margin in pixels.105 // Returns calculated width and top margin in pixels.
85 func CalculateModalDimensions(parent layout.OverlayWidget, cfg ModalSizeConfig) (width, marginTop int) {106 func CalculateModalDimensions(parent layout.OverlayWidget, cfg ModalSizeConfig) (width, marginTop int) {
M internal/ui/component/omnibox.go +35 -9
@@ -301,16 +301,33 @@ func (o *Omnibox) WidgetAsLayout(factory layout.WidgetFactory) layout.Widget {
301301 return factory.WrapWidget(&o.outerBox.Widget)
302302 }
303303
304+// estimateRowHeight returns the current best estimate for a single row height.
305+func (o *Omnibox) estimateRowHeight() int {
306+ if o.measuredHeights.valid && o.measuredHeights.singleRow > 0 {
307+ return o.measuredHeights.singleRow
308+ }
309+ return ScaleValue(DefaultRowHeights.Standard, o.uiScale)
310+}
311+
312+// effectiveMaxRows returns the max visible rows adapted to the current parent pane height.
313+// Must be called on the GTK main thread.
314+func (o *Omnibox) effectiveMaxRows() int {
315+ if o.parentOverlay == nil {
316+ return OmniboxListDefaults.MaxResults
317+ }
318+ return EffectiveMaxRows(o.parentOverlay.GetAllocatedHeight(), o.estimateRowHeight(), OmniboxSizeDefaults, OmniboxListDefaults)
319+}
320+
304321 // resizeAndCenter adjusts the omnibox size based on content and centers it.
305-// rowCount is the number of result rows to display (0 = no content, max 10).
322+// rowCount is the number of result rows to display (0 = no content, adapts to parent height).
306323 func (o *Omnibox) resizeAndCenter(rowCount int) {
307324 if o.parentOverlay == nil || o.outerBox == nil || o.mainBox == nil {
308325 return
309326 }
310327
311- // Cap at max results
312- if rowCount > OmniboxListDefaults.MaxVisibleRows {
313- rowCount = OmniboxListDefaults.MaxVisibleRows
328+ // Cap at effective max rows (adapts to parent pane height)
329+ if maxRows := o.effectiveMaxRows(); rowCount > maxRows {
330+ rowCount = maxRows
314331 }
315332
316333 // Schedule measurement after GTK has laid out widgets
@@ -351,8 +368,8 @@ func (o *Omnibox) measureAndResize(width, rowCount int) {
351368 contentHeight = rowCount * rowHeight
352369 }
353370
354- // Cap at max results
355- maxContentHeight := OmniboxListDefaults.MaxVisibleRows * rowHeight
371+ // Cap at effective max rows (adapts to parent pane height)
372+ maxContentHeight := o.effectiveMaxRows() * rowHeight
356373 if contentHeight > maxContentHeight {
357374 contentHeight = maxContentHeight
358375 }
@@ -1212,7 +1229,13 @@ func (o *Omnibox) performSearch() {
12121229 return
12131230 }
12141231
1215- // Perform fuzzy search
1232+ // Perform fuzzy history search in background
1233+ o.searchHistory(query, o.effectiveMaxRows())
1234+}
1235+
1236+// searchHistory runs a fuzzy history search in a background goroutine.
1237+// query is the search text; limit caps the number of results.
1238+func (o *Omnibox) searchHistory(query string, limit int) {
12161239 go func() {
12171240 ctx := o.ctx
12181241 log := logging.FromContext(ctx)
@@ -1231,7 +1254,7 @@ func (o *Omnibox) performSearch() {
12311254 go func() {
12321255 searchInput := usecase.SearchInput{
12331256 Query: query,
1234- Limit: OmniboxListDefaults.MaxResults,
1257+ Limit: limit,
12351258 }
12361259 output, err := o.historyUC.Search(ctx, searchInput)
12371260 searchCh <- searchResult{output, err}
@@ -1274,6 +1297,9 @@ func (o *Omnibox) loadInitialHistory() {
12741297 return
12751298 }
12761299
1300+ // Capture effective result limit on the GTK main thread before spawning goroutine
1301+ initialLimit := o.effectiveMaxRows()
1302+
12771303 go func() {
12781304 ctx := o.ctx
12791305 log := logging.FromContext(ctx)
@@ -1295,7 +1321,7 @@ func (o *Omnibox) loadInitialHistory() {
12951321
12961322 go func() {
12971323 // TODO: Implement GetMostVisited in use case if needed
1298- results, err := o.historyUC.GetRecent(ctx, OmniboxListDefaults.MaxResults, 0)
1324+ results, err := o.historyUC.GetRecent(ctx, initialLimit, 0)
12991325 historyCh <- historyResult{results, err}
13001326 }()
13011327
@@ -301,16 +301,33 @@ func (o *Omnibox) WidgetAsLayout(factory layout.WidgetFactory) layout.Widget {
301 return factory.WrapWidget(&o.outerBox.Widget)301 return factory.WrapWidget(&o.outerBox.Widget)
302 }302 }
303 303
304+// estimateRowHeight returns the current best estimate for a single row height.
305+func (o *Omnibox) estimateRowHeight() int {
306+ if o.measuredHeights.valid && o.measuredHeights.singleRow > 0 {
307+ return o.measuredHeights.singleRow
308+ }
309+ return ScaleValue(DefaultRowHeights.Standard, o.uiScale)
310+}
311+
312+// effectiveMaxRows returns the max visible rows adapted to the current parent pane height.
313+// Must be called on the GTK main thread.
314+func (o *Omnibox) effectiveMaxRows() int {
315+ if o.parentOverlay == nil {
316+ return OmniboxListDefaults.MaxResults
317+ }
318+ return EffectiveMaxRows(o.parentOverlay.GetAllocatedHeight(), o.estimateRowHeight(), OmniboxSizeDefaults, OmniboxListDefaults)
319+}
320+
304 // resizeAndCenter adjusts the omnibox size based on content and centers it.321 // resizeAndCenter adjusts the omnibox size based on content and centers it.
305-// rowCount is the number of result rows to display (0 = no content, max 10).322+// rowCount is the number of result rows to display (0 = no content, adapts to parent height).
306 func (o *Omnibox) resizeAndCenter(rowCount int) {323 func (o *Omnibox) resizeAndCenter(rowCount int) {
307 if o.parentOverlay == nil || o.outerBox == nil || o.mainBox == nil {324 if o.parentOverlay == nil || o.outerBox == nil || o.mainBox == nil {
308 return325 return
309 }326 }
310 327
311- // Cap at max results328+ // Cap at effective max rows (adapts to parent pane height)
312- if rowCount > OmniboxListDefaults.MaxVisibleRows {329+ if maxRows := o.effectiveMaxRows(); rowCount > maxRows {
313- rowCount = OmniboxListDefaults.MaxVisibleRows330+ rowCount = maxRows
314 }331 }
315 332
316 // Schedule measurement after GTK has laid out widgets333 // Schedule measurement after GTK has laid out widgets
@@ -351,8 +368,8 @@ func (o *Omnibox) measureAndResize(width, rowCount int) {
351 contentHeight = rowCount * rowHeight368 contentHeight = rowCount * rowHeight
352 }369 }
353 370
354- // Cap at max results371+ // Cap at effective max rows (adapts to parent pane height)
355- maxContentHeight := OmniboxListDefaults.MaxVisibleRows * rowHeight372+ maxContentHeight := o.effectiveMaxRows() * rowHeight
356 if contentHeight > maxContentHeight {373 if contentHeight > maxContentHeight {
357 contentHeight = maxContentHeight374 contentHeight = maxContentHeight
358 }375 }
@@ -1212,7 +1229,13 @@ func (o *Omnibox) performSearch() {
1212 return1229 return
1213 }1230 }
1214 1231
1215- // Perform fuzzy search1232+ // Perform fuzzy history search in background
1233+ o.searchHistory(query, o.effectiveMaxRows())
1234+}
1235+
1236+// searchHistory runs a fuzzy history search in a background goroutine.
1237+// query is the search text; limit caps the number of results.
1238+func (o *Omnibox) searchHistory(query string, limit int) {
1216 go func() {1239 go func() {
1217 ctx := o.ctx1240 ctx := o.ctx
1218 log := logging.FromContext(ctx)1241 log := logging.FromContext(ctx)
@@ -1231,7 +1254,7 @@ func (o *Omnibox) performSearch() {
1231 go func() {1254 go func() {
1232 searchInput := usecase.SearchInput{1255 searchInput := usecase.SearchInput{
1233 Query: query,1256 Query: query,
1234- Limit: OmniboxListDefaults.MaxResults,1257+ Limit: limit,
1235 }1258 }
1236 output, err := o.historyUC.Search(ctx, searchInput)1259 output, err := o.historyUC.Search(ctx, searchInput)
1237 searchCh <- searchResult{output, err}1260 searchCh <- searchResult{output, err}
@@ -1274,6 +1297,9 @@ func (o *Omnibox) loadInitialHistory() {
1274 return1297 return
1275 }1298 }
1276 1299
1300+ // Capture effective result limit on the GTK main thread before spawning goroutine
1301+ initialLimit := o.effectiveMaxRows()
1302+
1277 go func() {1303 go func() {
1278 ctx := o.ctx1304 ctx := o.ctx
1279 log := logging.FromContext(ctx)1305 log := logging.FromContext(ctx)
@@ -1295,7 +1321,7 @@ func (o *Omnibox) loadInitialHistory() {
1295 1321
1296 go func() {1322 go func() {
1297 // TODO: Implement GetMostVisited in use case if needed1323 // TODO: Implement GetMostVisited in use case if needed
1298- results, err := o.historyUC.GetRecent(ctx, OmniboxListDefaults.MaxResults, 0)1324+ results, err := o.historyUC.GetRecent(ctx, initialLimit, 0)
1299 historyCh <- historyResult{results, err}1325 historyCh <- historyResult{results, err}
1300 }()1326 }()
1301 1327

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