detached fdfddb8

Merge pull request #133 from bnema/feat/omnibox-click-outside-hide

feat(omnibox): hide on click outside bounds
commit
fdfddb88d52d1481f23664fc10a828d74fa0f765
author
bnema · · 2026-01-24 04:35 UTC
committer
GitHub · · 2026-01-24 04:35 UTC
merge of
6686c9a + d16e2ef · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
1 file · +86 -0
M internal/ui/component/omnibox.go +86 -0
@@ -133,6 +133,10 @@ type Omnibox struct {
133133
134134 // Callback retention: keep GTK signal callbacks reachable by Go GC.
135135 retainedCallbacks []interface{}
136+
137+ // Click outside handler
138+ clickOutsideController *gtk.GestureClick
139+ clickOutsideCb func(gtk.GestureClick, int, float64, float64)
136140 }
137141
138142 // OmniboxConfig holds configuration for creating an Omnibox.
@@ -202,6 +206,86 @@ func (o *Omnibox) SetParentOverlay(overlay layout.OverlayWidget) {
202206 o.mu.Lock()
203207 defer o.mu.Unlock()
204208 o.parentOverlay = overlay
209+ o.setupClickOutsideHandler()
210+}
211+
212+// setupClickOutsideHandler attaches a click gesture to the parent overlay
213+// that hides the omnibox when clicking outside its bounds.
214+func (o *Omnibox) setupClickOutsideHandler() {
215+ if o.parentOverlay == nil {
216+ return
217+ }
218+
219+ log := logging.FromContext(o.ctx)
220+
221+ // Create gesture click controller
222+ o.clickOutsideController = gtk.NewGestureClick()
223+ if o.clickOutsideController == nil {
224+ log.Error().Msg("failed to create click outside controller")
225+ return
226+ }
227+
228+ // Listen to primary button only
229+ o.clickOutsideController.SetButton(1) // GDK_BUTTON_PRIMARY
230+
231+ // Set capture phase so we see clicks before they reach the omnibox
232+ o.clickOutsideController.SetPropagationPhase(gtk.PhaseCaptureValue)
233+
234+ // Connect pressed handler
235+ o.clickOutsideCb = func(_ gtk.GestureClick, _ int, x, y float64) {
236+ o.handleClickOutside(x, y)
237+ }
238+ o.clickOutsideController.ConnectPressed(&o.clickOutsideCb)
239+
240+ // Add controller to parent overlay
241+ gtkWidget := o.parentOverlay.GtkWidget()
242+ if gtkWidget != nil {
243+ gtkWidget.AddController(&o.clickOutsideController.EventController)
244+ }
245+
246+ log.Debug().Msg("click outside handler attached to parent overlay")
247+}
248+
249+// handleClickOutside checks if a click is outside the omnibox bounds and hides if so.
250+func (o *Omnibox) handleClickOutside(clickX, clickY float64) {
251+ o.mu.RLock()
252+ visible := o.visible
253+ o.mu.RUnlock()
254+
255+ if !visible || o.outerBox == nil || o.parentOverlay == nil {
256+ return
257+ }
258+
259+ // Get omnibox position relative to parent overlay using graphene points
260+ srcPoint := &graphene.Point{X: 0, Y: 0}
261+ outPoint := &graphene.Point{}
262+ ok := o.outerBox.ComputePoint(o.parentOverlay.GtkWidget(), srcPoint, outPoint)
263+ if !ok {
264+ return
265+ }
266+
267+ boxX := float64(outPoint.X)
268+ boxY := float64(outPoint.Y)
269+
270+ // Get omnibox dimensions
271+ boxWidth := float64(o.outerBox.GetAllocatedWidth())
272+ boxHeight := float64(o.outerBox.GetAllocatedHeight())
273+
274+ // Check if click is inside omnibox bounds
275+ isInside := clickX >= boxX && clickX <= boxX+boxWidth &&
276+ clickY >= boxY && clickY <= boxY+boxHeight
277+
278+ if !isInside {
279+ logging.FromContext(o.ctx).Debug().
280+ Float64("clickX", clickX).
281+ Float64("clickY", clickY).
282+ Float64("boxX", boxX).
283+ Float64("boxY", boxY).
284+ Float64("boxWidth", boxWidth).
285+ Float64("boxHeight", boxHeight).
286+ Msg("click outside omnibox, hiding")
287+ o.Hide(o.ctx)
288+ }
205289 }
206290
207291 // WidgetAsLayout returns the omnibox's outer widget as a layout.Widget.
@@ -2098,6 +2182,8 @@ func (o *Omnibox) Destroy() {
20982182
20992183 o.parentOverlay = nil
21002184 o.retainedCallbacks = nil
2185+ o.clickOutsideController = nil
2186+ o.clickOutsideCb = nil
21012187
21022188 if o.outerBox != nil {
21032189 o.outerBox.Unparent()
@@ -133,6 +133,10 @@ type Omnibox struct {
133 133
134 // Callback retention: keep GTK signal callbacks reachable by Go GC.134 // Callback retention: keep GTK signal callbacks reachable by Go GC.
135 retainedCallbacks []interface{}135 retainedCallbacks []interface{}
136+
137+ // Click outside handler
138+ clickOutsideController *gtk.GestureClick
139+ clickOutsideCb func(gtk.GestureClick, int, float64, float64)
136 }140 }
137 141
138 // OmniboxConfig holds configuration for creating an Omnibox.142 // OmniboxConfig holds configuration for creating an Omnibox.
@@ -202,6 +206,86 @@ func (o *Omnibox) SetParentOverlay(overlay layout.OverlayWidget) {
202 o.mu.Lock()206 o.mu.Lock()
203 defer o.mu.Unlock()207 defer o.mu.Unlock()
204 o.parentOverlay = overlay208 o.parentOverlay = overlay
209+ o.setupClickOutsideHandler()
210+}
211+
212+// setupClickOutsideHandler attaches a click gesture to the parent overlay
213+// that hides the omnibox when clicking outside its bounds.
214+func (o *Omnibox) setupClickOutsideHandler() {
215+ if o.parentOverlay == nil {
216+ return
217+ }
218+
219+ log := logging.FromContext(o.ctx)
220+
221+ // Create gesture click controller
222+ o.clickOutsideController = gtk.NewGestureClick()
223+ if o.clickOutsideController == nil {
224+ log.Error().Msg("failed to create click outside controller")
225+ return
226+ }
227+
228+ // Listen to primary button only
229+ o.clickOutsideController.SetButton(1) // GDK_BUTTON_PRIMARY
230+
231+ // Set capture phase so we see clicks before they reach the omnibox
232+ o.clickOutsideController.SetPropagationPhase(gtk.PhaseCaptureValue)
233+
234+ // Connect pressed handler
235+ o.clickOutsideCb = func(_ gtk.GestureClick, _ int, x, y float64) {
236+ o.handleClickOutside(x, y)
237+ }
238+ o.clickOutsideController.ConnectPressed(&o.clickOutsideCb)
239+
240+ // Add controller to parent overlay
241+ gtkWidget := o.parentOverlay.GtkWidget()
242+ if gtkWidget != nil {
243+ gtkWidget.AddController(&o.clickOutsideController.EventController)
244+ }
245+
246+ log.Debug().Msg("click outside handler attached to parent overlay")
247+}
248+
249+// handleClickOutside checks if a click is outside the omnibox bounds and hides if so.
250+func (o *Omnibox) handleClickOutside(clickX, clickY float64) {
251+ o.mu.RLock()
252+ visible := o.visible
253+ o.mu.RUnlock()
254+
255+ if !visible || o.outerBox == nil || o.parentOverlay == nil {
256+ return
257+ }
258+
259+ // Get omnibox position relative to parent overlay using graphene points
260+ srcPoint := &graphene.Point{X: 0, Y: 0}
261+ outPoint := &graphene.Point{}
262+ ok := o.outerBox.ComputePoint(o.parentOverlay.GtkWidget(), srcPoint, outPoint)
263+ if !ok {
264+ return
265+ }
266+
267+ boxX := float64(outPoint.X)
268+ boxY := float64(outPoint.Y)
269+
270+ // Get omnibox dimensions
271+ boxWidth := float64(o.outerBox.GetAllocatedWidth())
272+ boxHeight := float64(o.outerBox.GetAllocatedHeight())
273+
274+ // Check if click is inside omnibox bounds
275+ isInside := clickX >= boxX && clickX <= boxX+boxWidth &&
276+ clickY >= boxY && clickY <= boxY+boxHeight
277+
278+ if !isInside {
279+ logging.FromContext(o.ctx).Debug().
280+ Float64("clickX", clickX).
281+ Float64("clickY", clickY).
282+ Float64("boxX", boxX).
283+ Float64("boxY", boxY).
284+ Float64("boxWidth", boxWidth).
285+ Float64("boxHeight", boxHeight).
286+ Msg("click outside omnibox, hiding")
287+ o.Hide(o.ctx)
288+ }
205 }289 }
206 290
207 // WidgetAsLayout returns the omnibox's outer widget as a layout.Widget.291 // WidgetAsLayout returns the omnibox's outer widget as a layout.Widget.
@@ -2098,6 +2182,8 @@ func (o *Omnibox) Destroy() {
2098 2182
2099 o.parentOverlay = nil2183 o.parentOverlay = nil
2100 o.retainedCallbacks = nil2184 o.retainedCallbacks = nil
2185+ o.clickOutsideController = nil
2186+ o.clickOutsideCb = nil
2101 2187
2102 if o.outerBox != nil {2188 if o.outerBox != nil {
2103 o.outerBox.Unparent()2189 o.outerBox.Unparent()

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