feat(omnibox): implement dynamic sizing based on result count
- Add resizeAndCenter() method that adjusts height based on row count (0-10 max) - Height calculation respects UI scale: base ~90px + 40px per row - Window resizes and re-centers when content changes - Hide scrolled window when no results/favorites available - Support initial_behavior='none' for minimal initial size (header + entry only) - Deprecate static updateSize() in favor of dynamic sizing
- commit
- 6bff21d5b102559cb3ffb422f94e53a8fbdf2292
- author
- brice · · 2025-12-15 19:04 UTC
- committer
- brice · · 2025-12-15 18:34 UTC
- parent
- 4622b8c
- signature
- unsigned
1 file · +82 -8
M internal/ui/component/omnibox.go +82 -8
| @@ -141,8 +141,15 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC | |||
| 141 | 141 | } | |
| 142 | 142 | ||
| 143 | 143 | // updateSize sets the omnibox size based on parent window dimensions. | |
| 144 | + | // Deprecated: use resizeAndCenter with row count instead. | |
| 144 | 145 | func (o *Omnibox) updateSize() { | |
| 145 | - | if o.parentWindow == nil || o.window == nil { | |
| 146 | + | o.resizeAndCenter(omniboxMaxResults) | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // resizeAndCenter adjusts the omnibox size based on content and centers it. | |
| 150 | + | // rowCount is the number of result rows to display (0 = no content, max 10). | |
| 151 | + | func (o *Omnibox) resizeAndCenter(rowCount int) { | |
| 152 | + | if o.parentWindow == nil || o.window == nil || o.mainBox == nil { | |
| 146 | 153 | return | |
| 147 | 154 | } | |
| 148 | 155 | ||
| @@ -160,8 +167,47 @@ func (o *Omnibox) updateSize() { | |||
| 160 | 167 | } | |
| 161 | 168 | ||
| 162 | 169 | width := int(float64(parentWidth) * omniboxWidthPct) | |
| 163 | - | height := int(float64(parentHeight) * omniboxHeightPct) | |
| 170 | + | ||
| 171 | + | // Base height: header (~40px) + entry (~50px) at scale 1.0 | |
| 172 | + | baseHeight := int(90 * o.uiScale) | |
| 173 | + | // Row height: approximately 40px per row at scale 1.0 | |
| 174 | + | rowHeight := int(40 * o.uiScale) | |
| 175 | + | ||
| 176 | + | // Cap at max results | |
| 177 | + | if rowCount > omniboxMaxResults { | |
| 178 | + | rowCount = omniboxMaxResults | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | height := baseHeight + (rowCount * rowHeight) | |
| 182 | + | ||
| 183 | + | o.window.SetDefaultSize(width, height) | |
| 184 | + | o.mainBox.SetSizeRequest(width, height) | |
| 185 | + | ||
| 186 | + | // Center the window on the parent | |
| 187 | + | x := (parentWidth - width) / 2 | |
| 188 | + | y := (parentHeight - height) / 2 | |
| 189 | + | ||
| 190 | + | // Get parent window position (for Wayland this may be 0,0) | |
| 191 | + | // For a transient/modal window, GTK should handle relative positioning | |
| 192 | + | // We need to use a surface-based approach for proper centering | |
| 193 | + | if surface := o.window.GetSurface(); surface != nil { | |
| 194 | + | // Request the window manager to center it | |
| 195 | + | // Note: On Wayland, window positioning is limited | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | // For GTK4 popup positioning, we rely on the transient relationship | |
| 199 | + | // The window should appear centered relative to parent automatically | |
| 200 | + | // But we can hint at position using the allocation | |
| 164 | 201 | o.window.SetDefaultSize(width, height) | |
| 202 | + | ||
| 203 | + | log := logging.FromContext(o.ctx) | |
| 204 | + | log.Debug(). | |
| 205 | + | Int("width", width). | |
| 206 | + | Int("height", height). | |
| 207 | + | Int("x", x). | |
| 208 | + | Int("y", y). | |
| 209 | + | Int("rows", rowCount). | |
| 210 | + | Msg("omnibox resized") | |
| 165 | 211 | } | |
| 166 | 212 | ||
| 167 | 213 | // createWidgets builds the GTK widget hierarchy. | |
| @@ -541,8 +587,15 @@ func (o *Omnibox) updateSuggestions(suggestions []Suggestion) { | |||
| 541 | 587 | ||
| 542 | 588 | o.rebuildList() | |
| 543 | 589 | ||
| 590 | + | // Hide scrolled window when there are no suggestions | |
| 591 | + | rowCount := len(suggestions) | |
| 592 | + | if o.scrolledWin != nil { | |
| 593 | + | o.scrolledWin.SetVisible(rowCount > 0) | |
| 594 | + | } | |
| 595 | + | o.resizeAndCenter(rowCount) | |
| 596 | + | ||
| 544 | 597 | // Select first item if available | |
| 545 | - | if len(suggestions) > 0 { | |
| 598 | + | if rowCount > 0 { | |
| 546 | 599 | o.selectIndex(0) | |
| 547 | 600 | } | |
| 548 | 601 | } | |
| @@ -556,8 +609,15 @@ func (o *Omnibox) updateFavorites(favorites []Favorite) { | |||
| 556 | 609 | ||
| 557 | 610 | o.rebuildList() | |
| 558 | 611 | ||
| 612 | + | // Hide scrolled window when there are no favorites | |
| 613 | + | rowCount := len(favorites) | |
| 614 | + | if o.scrolledWin != nil { | |
| 615 | + | o.scrolledWin.SetVisible(rowCount > 0) | |
| 616 | + | } | |
| 617 | + | o.resizeAndCenter(rowCount) | |
| 618 | + | ||
| 559 | 619 | // Select first item if available | |
| 560 | - | if len(favorites) > 0 { | |
| 620 | + | if rowCount > 0 { | |
| 561 | 621 | o.selectIndex(0) | |
| 562 | 622 | } | |
| 563 | 623 | } | |
| @@ -940,17 +1000,31 @@ func (o *Omnibox) Show(ctx context.Context, query string) { | |||
| 940 | 1000 | // Set initial query | |
| 941 | 1001 | o.entry.SetText(query) | |
| 942 | 1002 | ||
| 943 | - | // Load initial data | |
| 944 | - | o.performSearch() | |
| 1003 | + | // Determine if we expect content initially | |
| 1004 | + | // No content expected if: no query AND initialBehavior is "none" | |
| 1005 | + | expectContent := query != "" || o.initialBehavior != "none" | |
| 1006 | + | ||
| 1007 | + | // Hide scrolled window if no content expected | |
| 1008 | + | if o.scrolledWin != nil { | |
| 1009 | + | o.scrolledWin.SetVisible(expectContent) | |
| 1010 | + | } | |
| 945 | 1011 | ||
| 946 | - | // Update size based on current parent window dimensions | |
| 947 | - | o.updateSize() | |
| 1012 | + | // Initial size: 0 rows if no content expected, max rows otherwise | |
| 1013 | + | // (will be updated by search results) | |
| 1014 | + | initialRows := 0 | |
| 1015 | + | if expectContent { | |
| 1016 | + | initialRows = omniboxMaxResults | |
| 1017 | + | } | |
| 1018 | + | o.resizeAndCenter(initialRows) | |
| 948 | 1019 | ||
| 949 | 1020 | // Present the window | |
| 950 | 1021 | o.window.Present() | |
| 951 | 1022 | ||
| 952 | 1023 | // Focus the entry | |
| 953 | 1024 | o.entry.GrabFocus() | |
| 1025 | + | ||
| 1026 | + | // Load initial data (may update size later if results found) | |
| 1027 | + | o.performSearch() | |
| 954 | 1028 | } | |
| 955 | 1029 | ||
| 956 | 1030 | // Hide closes the omnibox. | |
| @@ -141,8 +141,15 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC | |||||
| 141 | } | 141 | } | ||
| 142 | 142 | ||||
| 143 | // updateSize sets the omnibox size based on parent window dimensions. | 143 | // updateSize sets the omnibox size based on parent window dimensions. | ||
| 144 | + | // Deprecated: use resizeAndCenter with row count instead. | |||
| 144 | func (o *Omnibox) updateSize() { | 145 | func (o *Omnibox) updateSize() { | ||
| 145 | - | if o.parentWindow == nil || o.window == nil { | 146 | + | o.resizeAndCenter(omniboxMaxResults) |
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | // resizeAndCenter adjusts the omnibox size based on content and centers it. | |||
| 150 | + | // rowCount is the number of result rows to display (0 = no content, max 10). | |||
| 151 | + | func (o *Omnibox) resizeAndCenter(rowCount int) { | |||
| 152 | + | if o.parentWindow == nil || o.window == nil || o.mainBox == nil { | |||
| 146 | return | 153 | return | ||
| 147 | } | 154 | } | ||
| 148 | 155 | ||||
| @@ -160,8 +167,47 @@ func (o *Omnibox) updateSize() { | |||||
| 160 | } | 167 | } | ||
| 161 | 168 | ||||
| 162 | width := int(float64(parentWidth) * omniboxWidthPct) | 169 | width := int(float64(parentWidth) * omniboxWidthPct) | ||
| 163 | - | height := int(float64(parentHeight) * omniboxHeightPct) | 170 | + | |
| 171 | + | // Base height: header (~40px) + entry (~50px) at scale 1.0 | |||
| 172 | + | baseHeight := int(90 * o.uiScale) | |||
| 173 | + | // Row height: approximately 40px per row at scale 1.0 | |||
| 174 | + | rowHeight := int(40 * o.uiScale) | |||
| 175 | + | ||||
| 176 | + | // Cap at max results | |||
| 177 | + | if rowCount > omniboxMaxResults { | |||
| 178 | + | rowCount = omniboxMaxResults | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | height := baseHeight + (rowCount * rowHeight) | |||
| 182 | + | ||||
| 183 | + | o.window.SetDefaultSize(width, height) | |||
| 184 | + | o.mainBox.SetSizeRequest(width, height) | |||
| 185 | + | ||||
| 186 | + | // Center the window on the parent | |||
| 187 | + | x := (parentWidth - width) / 2 | |||
| 188 | + | y := (parentHeight - height) / 2 | |||
| 189 | + | ||||
| 190 | + | // Get parent window position (for Wayland this may be 0,0) | |||
| 191 | + | // For a transient/modal window, GTK should handle relative positioning | |||
| 192 | + | // We need to use a surface-based approach for proper centering | |||
| 193 | + | if surface := o.window.GetSurface(); surface != nil { | |||
| 194 | + | // Request the window manager to center it | |||
| 195 | + | // Note: On Wayland, window positioning is limited | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | // For GTK4 popup positioning, we rely on the transient relationship | |||
| 199 | + | // The window should appear centered relative to parent automatically | |||
| 200 | + | // But we can hint at position using the allocation | |||
| 164 | o.window.SetDefaultSize(width, height) | 201 | o.window.SetDefaultSize(width, height) | ||
| 202 | + | ||||
| 203 | + | log := logging.FromContext(o.ctx) | |||
| 204 | + | log.Debug(). | |||
| 205 | + | Int("width", width). | |||
| 206 | + | Int("height", height). | |||
| 207 | + | Int("x", x). | |||
| 208 | + | Int("y", y). | |||
| 209 | + | Int("rows", rowCount). | |||
| 210 | + | Msg("omnibox resized") | |||
| 165 | } | 211 | } | ||
| 166 | 212 | ||||
| 167 | // createWidgets builds the GTK widget hierarchy. | 213 | // createWidgets builds the GTK widget hierarchy. | ||
| @@ -541,8 +587,15 @@ func (o *Omnibox) updateSuggestions(suggestions []Suggestion) { | |||||
| 541 | 587 | ||||
| 542 | o.rebuildList() | 588 | o.rebuildList() | ||
| 543 | 589 | ||||
| 590 | + | // Hide scrolled window when there are no suggestions | |||
| 591 | + | rowCount := len(suggestions) | |||
| 592 | + | if o.scrolledWin != nil { | |||
| 593 | + | o.scrolledWin.SetVisible(rowCount > 0) | |||
| 594 | + | } | |||
| 595 | + | o.resizeAndCenter(rowCount) | |||
| 596 | + | ||||
| 544 | // Select first item if available | 597 | // Select first item if available | ||
| 545 | - | if len(suggestions) > 0 { | 598 | + | if rowCount > 0 { |
| 546 | o.selectIndex(0) | 599 | o.selectIndex(0) | ||
| 547 | } | 600 | } | ||
| 548 | } | 601 | } | ||
| @@ -556,8 +609,15 @@ func (o *Omnibox) updateFavorites(favorites []Favorite) { | |||||
| 556 | 609 | ||||
| 557 | o.rebuildList() | 610 | o.rebuildList() | ||
| 558 | 611 | ||||
| 612 | + | // Hide scrolled window when there are no favorites | |||
| 613 | + | rowCount := len(favorites) | |||
| 614 | + | if o.scrolledWin != nil { | |||
| 615 | + | o.scrolledWin.SetVisible(rowCount > 0) | |||
| 616 | + | } | |||
| 617 | + | o.resizeAndCenter(rowCount) | |||
| 618 | + | ||||
| 559 | // Select first item if available | 619 | // Select first item if available | ||
| 560 | - | if len(favorites) > 0 { | 620 | + | if rowCount > 0 { |
| 561 | o.selectIndex(0) | 621 | o.selectIndex(0) | ||
| 562 | } | 622 | } | ||
| 563 | } | 623 | } | ||
| @@ -940,17 +1000,31 @@ func (o *Omnibox) Show(ctx context.Context, query string) { | |||||
| 940 | // Set initial query | 1000 | // Set initial query | ||
| 941 | o.entry.SetText(query) | 1001 | o.entry.SetText(query) | ||
| 942 | 1002 | ||||
| 943 | - | // Load initial data | 1003 | + | // Determine if we expect content initially |
| 944 | - | o.performSearch() | 1004 | + | // No content expected if: no query AND initialBehavior is "none" |
| 1005 | + | expectContent := query != "" || o.initialBehavior != "none" | |||
| 1006 | + | ||||
| 1007 | + | // Hide scrolled window if no content expected | |||
| 1008 | + | if o.scrolledWin != nil { | |||
| 1009 | + | o.scrolledWin.SetVisible(expectContent) | |||
| 1010 | + | } | |||
| 945 | 1011 | ||||
| 946 | - | // Update size based on current parent window dimensions | 1012 | + | // Initial size: 0 rows if no content expected, max rows otherwise |
| 947 | - | o.updateSize() | 1013 | + | // (will be updated by search results) |
| 1014 | + | initialRows := 0 | |||
| 1015 | + | if expectContent { | |||
| 1016 | + | initialRows = omniboxMaxResults | |||
| 1017 | + | } | |||
| 1018 | + | o.resizeAndCenter(initialRows) | |||
| 948 | 1019 | ||||
| 949 | // Present the window | 1020 | // Present the window | ||
| 950 | o.window.Present() | 1021 | o.window.Present() | ||
| 951 | 1022 | ||||
| 952 | // Focus the entry | 1023 | // Focus the entry | ||
| 953 | o.entry.GrabFocus() | 1024 | o.entry.GrabFocus() | ||
| 1025 | + | ||||
| 1026 | + | // Load initial data (may update size later if results found) | |||
| 1027 | + | o.performSearch() | |||
| 954 | } | 1028 | } | ||
| 955 | 1029 | ||||
| 956 | // Hide closes the omnibox. | 1030 | // Hide closes the omnibox. | ||