fix(omnibox): clip ghost text to entry bounds and fix stale check race
- commit
- 603f4ba5f5699b2d33256dc4ac8a315bf575aaa6
- author
- brice · · 2026-01-24 04:04 UTC
- parent
- 88b49b5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +12 -9
M internal/ui/component/omnibox.go +12 -9
| @@ -425,9 +425,11 @@ func (o *Omnibox) initEntry() error { | |||
| 425 | 425 | o.ghostLabel.SetValign(gtk.AlignCenterValue) // Center vertically to match entry's centered text | |
| 426 | 426 | o.ghostLabel.SetXalign(0) | |
| 427 | 427 | o.ghostLabel.SetVisible(false) | |
| 428 | - | o.ghostLabel.SetCanTarget(false) // Don't intercept clicks | |
| 428 | + | o.ghostLabel.SetCanTarget(false) // Don't intercept clicks | |
| 429 | + | o.ghostLabel.SetEllipsize(pango.EllipsizeEndValue) // Truncate with ellipsis if too long | |
| 429 | 430 | ||
| 430 | 431 | o.entryOverlay.AddOverlay(&o.ghostLabel.Widget) | |
| 432 | + | o.entryOverlay.SetClipOverlay(&o.ghostLabel.Widget, true) // Clip ghost text to entry bounds | |
| 431 | 433 | ||
| 432 | 434 | return nil | |
| 433 | 435 | } | |
| @@ -741,15 +743,8 @@ func (o *Omnibox) setGhostText(originalInput, suffix, fullText string) { | |||
| 741 | 743 | return | |
| 742 | 744 | } | |
| 743 | 745 | ||
| 746 | + | // Store ghost state optimistically - will be validated on main thread | |
| 744 | 747 | o.mu.Lock() | |
| 745 | - | currentInput := o.realInput | |
| 746 | - | // Check if input has changed since the autocomplete query was made | |
| 747 | - | // Skip this check when originalInput is empty (row selection replaces full input) | |
| 748 | - | if originalInput != "" && currentInput != originalInput { | |
| 749 | - | o.mu.Unlock() | |
| 750 | - | // Input changed, skip this stale result - a newer query will handle it | |
| 751 | - | return | |
| 752 | - | } | |
| 753 | 748 | o.ghostSuffix = suffix | |
| 754 | 749 | o.ghostFullText = fullText | |
| 755 | 750 | o.hasGhostText = true | |
| @@ -760,6 +755,14 @@ func (o *Omnibox) setGhostText(originalInput, suffix, fullText string) { | |||
| 760 | 755 | return false | |
| 761 | 756 | } | |
| 762 | 757 | ||
| 758 | + | // Check if input has changed since the autocomplete query was made | |
| 759 | + | // This check runs on GTK main thread where entry.GetText() is reliable | |
| 760 | + | // Skip this check when originalInput is empty (row selection replaces full input) | |
| 761 | + | if originalInput != "" && o.entry.GetText() != originalInput { | |
| 762 | + | // Input changed, skip this stale result - a newer query will handle it | |
| 763 | + | return false | |
| 764 | + | } | |
| 765 | + | ||
| 763 | 766 | // Get the entry's Pango context and create a layout to measure text width | |
| 764 | 767 | pangoCtx := o.entry.GetPangoContext() | |
| 765 | 768 | if pangoCtx == nil { | |
| @@ -425,9 +425,11 @@ func (o *Omnibox) initEntry() error { | |||||
| 425 | o.ghostLabel.SetValign(gtk.AlignCenterValue) // Center vertically to match entry's centered text | 425 | o.ghostLabel.SetValign(gtk.AlignCenterValue) // Center vertically to match entry's centered text | ||
| 426 | o.ghostLabel.SetXalign(0) | 426 | o.ghostLabel.SetXalign(0) | ||
| 427 | o.ghostLabel.SetVisible(false) | 427 | o.ghostLabel.SetVisible(false) | ||
| 428 | - | o.ghostLabel.SetCanTarget(false) // Don't intercept clicks | 428 | + | o.ghostLabel.SetCanTarget(false) // Don't intercept clicks |
| 429 | + | o.ghostLabel.SetEllipsize(pango.EllipsizeEndValue) // Truncate with ellipsis if too long | |||
| 429 | 430 | ||||
| 430 | o.entryOverlay.AddOverlay(&o.ghostLabel.Widget) | 431 | o.entryOverlay.AddOverlay(&o.ghostLabel.Widget) | ||
| 432 | + | o.entryOverlay.SetClipOverlay(&o.ghostLabel.Widget, true) // Clip ghost text to entry bounds | |||
| 431 | 433 | ||||
| 432 | return nil | 434 | return nil | ||
| 433 | } | 435 | } | ||
| @@ -741,15 +743,8 @@ func (o *Omnibox) setGhostText(originalInput, suffix, fullText string) { | |||||
| 741 | return | 743 | return | ||
| 742 | } | 744 | } | ||
| 743 | 745 | ||||
| 746 | + | // Store ghost state optimistically - will be validated on main thread | |||
| 744 | o.mu.Lock() | 747 | o.mu.Lock() | ||
| 745 | - | currentInput := o.realInput | |||
| 746 | - | // Check if input has changed since the autocomplete query was made | |||
| 747 | - | // Skip this check when originalInput is empty (row selection replaces full input) | |||
| 748 | - | if originalInput != "" && currentInput != originalInput { | |||
| 749 | - | o.mu.Unlock() | |||
| 750 | - | // Input changed, skip this stale result - a newer query will handle it | |||
| 751 | - | return | |||
| 752 | - | } | |||
| 753 | o.ghostSuffix = suffix | 748 | o.ghostSuffix = suffix | ||
| 754 | o.ghostFullText = fullText | 749 | o.ghostFullText = fullText | ||
| 755 | o.hasGhostText = true | 750 | o.hasGhostText = true | ||
| @@ -760,6 +755,14 @@ func (o *Omnibox) setGhostText(originalInput, suffix, fullText string) { | |||||
| 760 | return false | 755 | return false | ||
| 761 | } | 756 | } | ||
| 762 | 757 | ||||
| 758 | + | // Check if input has changed since the autocomplete query was made | |||
| 759 | + | // This check runs on GTK main thread where entry.GetText() is reliable | |||
| 760 | + | // Skip this check when originalInput is empty (row selection replaces full input) | |||
| 761 | + | if originalInput != "" && o.entry.GetText() != originalInput { | |||
| 762 | + | // Input changed, skip this stale result - a newer query will handle it | |||
| 763 | + | return false | |||
| 764 | + | } | |||
| 765 | + | ||||
| 763 | // Get the entry's Pango context and create a layout to measure text width | 766 | // Get the entry's Pango context and create a layout to measure text width | ||
| 764 | pangoCtx := o.entry.GetPangoContext() | 767 | pangoCtx := o.entry.GetPangoContext() | ||
| 765 | if pangoCtx == nil { | 768 | if pangoCtx == nil { | ||