detached d16e2ef

feat(omnibox): hide on click outside bounds

commit
d16e2efd08c2b426d9f4a8956fefdfbb1830a53b
author
bnema · · 2026-01-23 18:42 UTC
parent
5195a15
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +87 -0
M internal/ui/component/omnibox.go +87 -0
@@ -16,6 +16,7 @@ import (
1616 "github.com/bnema/dumber/internal/ui/layout"
1717 "github.com/jwijenbergh/puregotk/v4/gdk"
1818 "github.com/jwijenbergh/puregotk/v4/glib"
19+ "github.com/jwijenbergh/puregotk/v4/graphene"
1920 "github.com/jwijenbergh/puregotk/v4/gtk"
2021 "github.com/jwijenbergh/puregotk/v4/pango"
2122 )
@@ -130,6 +131,10 @@ type Omnibox struct {
130131
131132 // Callback retention: keep GTK signal callbacks reachable by Go GC.
132133 retainedCallbacks []interface{}
134+
135+ // Click outside handler
136+ clickOutsideController *gtk.GestureClick
137+ clickOutsideCb func(gtk.GestureClick, int, float64, float64)
133138 }
134139
135140 // OmniboxConfig holds configuration for creating an Omnibox.
@@ -199,6 +204,86 @@ func (o *Omnibox) SetParentOverlay(overlay layout.OverlayWidget) {
199204 o.mu.Lock()
200205 defer o.mu.Unlock()
201206 o.parentOverlay = overlay
207+ o.setupClickOutsideHandler()
208+}
209+
210+// setupClickOutsideHandler attaches a click gesture to the parent overlay
211+// that hides the omnibox when clicking outside its bounds.
212+func (o *Omnibox) setupClickOutsideHandler() {
213+ if o.parentOverlay == nil {
214+ return
215+ }
216+
217+ log := logging.FromContext(o.ctx)
218+
219+ // Create gesture click controller
220+ o.clickOutsideController = gtk.NewGestureClick()
221+ if o.clickOutsideController == nil {
222+ log.Error().Msg("failed to create click outside controller")
223+ return
224+ }
225+
226+ // Listen to primary button only
227+ o.clickOutsideController.SetButton(1) // GDK_BUTTON_PRIMARY
228+
229+ // Set capture phase so we see clicks before they reach the omnibox
230+ o.clickOutsideController.SetPropagationPhase(gtk.PhaseCaptureValue)
231+
232+ // Connect pressed handler
233+ o.clickOutsideCb = func(_ gtk.GestureClick, _ int, x, y float64) {
234+ o.handleClickOutside(x, y)
235+ }
236+ o.clickOutsideController.ConnectPressed(&o.clickOutsideCb)
237+
238+ // Add controller to parent overlay
239+ gtkWidget := o.parentOverlay.GtkWidget()
240+ if gtkWidget != nil {
241+ gtkWidget.AddController(&o.clickOutsideController.EventController)
242+ }
243+
244+ log.Debug().Msg("click outside handler attached to parent overlay")
245+}
246+
247+// handleClickOutside checks if a click is outside the omnibox bounds and hides if so.
248+func (o *Omnibox) handleClickOutside(clickX, clickY float64) {
249+ o.mu.RLock()
250+ visible := o.visible
251+ o.mu.RUnlock()
252+
253+ if !visible || o.outerBox == nil || o.parentOverlay == nil {
254+ return
255+ }
256+
257+ // Get omnibox position relative to parent overlay using graphene points
258+ srcPoint := &graphene.Point{X: 0, Y: 0}
259+ outPoint := &graphene.Point{}
260+ ok := o.outerBox.ComputePoint(o.parentOverlay.GtkWidget(), srcPoint, outPoint)
261+ if !ok {
262+ return
263+ }
264+
265+ boxX := float64(outPoint.X)
266+ boxY := float64(outPoint.Y)
267+
268+ // Get omnibox dimensions
269+ boxWidth := float64(o.outerBox.GetAllocatedWidth())
270+ boxHeight := float64(o.outerBox.GetAllocatedHeight())
271+
272+ // Check if click is inside omnibox bounds
273+ isInside := clickX >= boxX && clickX <= boxX+boxWidth &&
274+ clickY >= boxY && clickY <= boxY+boxHeight
275+
276+ if !isInside {
277+ logging.FromContext(o.ctx).Debug().
278+ Float64("clickX", clickX).
279+ Float64("clickY", clickY).
280+ Float64("boxX", boxX).
281+ Float64("boxY", boxY).
282+ Float64("boxWidth", boxWidth).
283+ Float64("boxHeight", boxHeight).
284+ Msg("click outside omnibox, hiding")
285+ o.Hide(o.ctx)
286+ }
202287 }
203288
204289 // WidgetAsLayout returns the omnibox's outer widget as a layout.Widget.
@@ -2063,6 +2148,8 @@ func (o *Omnibox) Destroy() {
20632148
20642149 o.parentOverlay = nil
20652150 o.retainedCallbacks = nil
2151+ o.clickOutsideController = nil
2152+ o.clickOutsideCb = nil
20662153
20672154 if o.outerBox != nil {
20682155 o.outerBox.Unparent()
@@ -16,6 +16,7 @@ import (
16 "github.com/bnema/dumber/internal/ui/layout"16 "github.com/bnema/dumber/internal/ui/layout"
17 "github.com/jwijenbergh/puregotk/v4/gdk"17 "github.com/jwijenbergh/puregotk/v4/gdk"
18 "github.com/jwijenbergh/puregotk/v4/glib"18 "github.com/jwijenbergh/puregotk/v4/glib"
19+ "github.com/jwijenbergh/puregotk/v4/graphene"
19 "github.com/jwijenbergh/puregotk/v4/gtk"20 "github.com/jwijenbergh/puregotk/v4/gtk"
20 "github.com/jwijenbergh/puregotk/v4/pango"21 "github.com/jwijenbergh/puregotk/v4/pango"
21 )22 )
@@ -130,6 +131,10 @@ type Omnibox struct {
130 131
131 // Callback retention: keep GTK signal callbacks reachable by Go GC.132 // Callback retention: keep GTK signal callbacks reachable by Go GC.
132 retainedCallbacks []interface{}133 retainedCallbacks []interface{}
134+
135+ // Click outside handler
136+ clickOutsideController *gtk.GestureClick
137+ clickOutsideCb func(gtk.GestureClick, int, float64, float64)
133 }138 }
134 139
135 // OmniboxConfig holds configuration for creating an Omnibox.140 // OmniboxConfig holds configuration for creating an Omnibox.
@@ -199,6 +204,86 @@ func (o *Omnibox) SetParentOverlay(overlay layout.OverlayWidget) {
199 o.mu.Lock()204 o.mu.Lock()
200 defer o.mu.Unlock()205 defer o.mu.Unlock()
201 o.parentOverlay = overlay206 o.parentOverlay = overlay
207+ o.setupClickOutsideHandler()
208+}
209+
210+// setupClickOutsideHandler attaches a click gesture to the parent overlay
211+// that hides the omnibox when clicking outside its bounds.
212+func (o *Omnibox) setupClickOutsideHandler() {
213+ if o.parentOverlay == nil {
214+ return
215+ }
216+
217+ log := logging.FromContext(o.ctx)
218+
219+ // Create gesture click controller
220+ o.clickOutsideController = gtk.NewGestureClick()
221+ if o.clickOutsideController == nil {
222+ log.Error().Msg("failed to create click outside controller")
223+ return
224+ }
225+
226+ // Listen to primary button only
227+ o.clickOutsideController.SetButton(1) // GDK_BUTTON_PRIMARY
228+
229+ // Set capture phase so we see clicks before they reach the omnibox
230+ o.clickOutsideController.SetPropagationPhase(gtk.PhaseCaptureValue)
231+
232+ // Connect pressed handler
233+ o.clickOutsideCb = func(_ gtk.GestureClick, _ int, x, y float64) {
234+ o.handleClickOutside(x, y)
235+ }
236+ o.clickOutsideController.ConnectPressed(&o.clickOutsideCb)
237+
238+ // Add controller to parent overlay
239+ gtkWidget := o.parentOverlay.GtkWidget()
240+ if gtkWidget != nil {
241+ gtkWidget.AddController(&o.clickOutsideController.EventController)
242+ }
243+
244+ log.Debug().Msg("click outside handler attached to parent overlay")
245+}
246+
247+// handleClickOutside checks if a click is outside the omnibox bounds and hides if so.
248+func (o *Omnibox) handleClickOutside(clickX, clickY float64) {
249+ o.mu.RLock()
250+ visible := o.visible
251+ o.mu.RUnlock()
252+
253+ if !visible || o.outerBox == nil || o.parentOverlay == nil {
254+ return
255+ }
256+
257+ // Get omnibox position relative to parent overlay using graphene points
258+ srcPoint := &graphene.Point{X: 0, Y: 0}
259+ outPoint := &graphene.Point{}
260+ ok := o.outerBox.ComputePoint(o.parentOverlay.GtkWidget(), srcPoint, outPoint)
261+ if !ok {
262+ return
263+ }
264+
265+ boxX := float64(outPoint.X)
266+ boxY := float64(outPoint.Y)
267+
268+ // Get omnibox dimensions
269+ boxWidth := float64(o.outerBox.GetAllocatedWidth())
270+ boxHeight := float64(o.outerBox.GetAllocatedHeight())
271+
272+ // Check if click is inside omnibox bounds
273+ isInside := clickX >= boxX && clickX <= boxX+boxWidth &&
274+ clickY >= boxY && clickY <= boxY+boxHeight
275+
276+ if !isInside {
277+ logging.FromContext(o.ctx).Debug().
278+ Float64("clickX", clickX).
279+ Float64("clickY", clickY).
280+ Float64("boxX", boxX).
281+ Float64("boxY", boxY).
282+ Float64("boxWidth", boxWidth).
283+ Float64("boxHeight", boxHeight).
284+ Msg("click outside omnibox, hiding")
285+ o.Hide(o.ctx)
286+ }
202 }287 }
203 288
204 // WidgetAsLayout returns the omnibox's outer widget as a layout.Widget.289 // WidgetAsLayout returns the omnibox's outer widget as a layout.Widget.
@@ -2063,6 +2148,8 @@ func (o *Omnibox) Destroy() {
2063 2148
2064 o.parentOverlay = nil2149 o.parentOverlay = nil
2065 o.retainedCallbacks = nil2150 o.retainedCallbacks = nil
2151+ o.clickOutsideController = nil
2152+ o.clickOutsideCb = nil
2066 2153
2067 if o.outerBox != nil {2154 if o.outerBox != nil {
2068 o.outerBox.Unparent()2155 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