feat(omnibox): hide on click outside bounds
- commit
- d16e2efd08c2b426d9f4a8956fefdfbb1830a53b
- author
- bnema · · 2026-01-23 18:42 UTC
- parent
- 5195a15
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +87 -0
M internal/ui/component/omnibox.go +87 -0
| @@ -16,6 +16,7 @@ import ( | |||
| 16 | 16 | "github.com/bnema/dumber/internal/ui/layout" | |
| 17 | 17 | "github.com/jwijenbergh/puregotk/v4/gdk" | |
| 18 | 18 | "github.com/jwijenbergh/puregotk/v4/glib" | |
| 19 | + | "github.com/jwijenbergh/puregotk/v4/graphene" | |
| 19 | 20 | "github.com/jwijenbergh/puregotk/v4/gtk" | |
| 20 | 21 | "github.com/jwijenbergh/puregotk/v4/pango" | |
| 21 | 22 | ) | |
| @@ -130,6 +131,10 @@ type Omnibox struct { | |||
| 130 | 131 | ||
| 131 | 132 | // Callback retention: keep GTK signal callbacks reachable by Go GC. | |
| 132 | 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 | 140 | // OmniboxConfig holds configuration for creating an Omnibox. | |
| @@ -199,6 +204,86 @@ func (o *Omnibox) SetParentOverlay(overlay layout.OverlayWidget) { | |||
| 199 | 204 | o.mu.Lock() | |
| 200 | 205 | defer o.mu.Unlock() | |
| 201 | 206 | 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 | 289 | // WidgetAsLayout returns the omnibox's outer widget as a layout.Widget. | |
| @@ -2063,6 +2148,8 @@ func (o *Omnibox) Destroy() { | |||
| 2063 | 2148 | ||
| 2064 | 2149 | o.parentOverlay = nil | |
| 2065 | 2150 | o.retainedCallbacks = nil | |
| 2151 | + | o.clickOutsideController = nil | |
| 2152 | + | o.clickOutsideCb = nil | |
| 2066 | 2153 | ||
| 2067 | 2154 | if o.outerBox != nil { | |
| 2068 | 2155 | 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 = overlay | 206 | 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 = nil | 2149 | o.parentOverlay = nil | ||
| 2065 | o.retainedCallbacks = nil | 2150 | 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() | ||