detached 5fe539f

fix(ui): apply mode class atomically to toaster and support custom colors via CSS

commit
5fe539f2fe97585a637d98aabd69f25f2524df0d
author
brice · · 2025-12-28 07:41 UTC
parent
f0af95e
signature
unsigned
2 files · +58 -22
M internal/ui/app.go +3 -5
@@ -1307,15 +1307,13 @@ func (a *App) updateModeIndicatorToaster(ctx context.Context, mode input.Mode) {
13071307 }
13081308
13091309 // Show persistent toaster at bottom-left with mode display name.
1310- // Note: Call Show() first, then ApplyModeClass() because Show() clears custom styles.
1310+ // Mode class is applied atomically with Show() to avoid visual flicker.
1311+ modeClass := getModeToastClass(mode)
13111312 a.modeToaster.Show(ctx, mode.DisplayName(), component.ToastInfo,
13121313 component.WithDuration(0), // Persistent until mode exits.
13131314 component.WithPosition(component.ToastPositionBottomLeft),
1315+ component.WithModeClass(modeClass),
13141316 )
1315-
1316- // Apply mode-specific CSS class after Show() to override the default level styling.
1317- modeClass := getModeToastClass(mode)
1318- a.modeToaster.ApplyModeClass(modeClass)
13191317 }
13201318
13211319 // getModeToastClass returns the CSS class for the given mode's toast styling.
@@ -1307,15 +1307,13 @@ func (a *App) updateModeIndicatorToaster(ctx context.Context, mode input.Mode) {
1307 }1307 }
1308 1308
1309 // Show persistent toaster at bottom-left with mode display name.1309 // Show persistent toaster at bottom-left with mode display name.
1310- // Note: Call Show() first, then ApplyModeClass() because Show() clears custom styles.1310+ // Mode class is applied atomically with Show() to avoid visual flicker.
1311+ modeClass := getModeToastClass(mode)
1311 a.modeToaster.Show(ctx, mode.DisplayName(), component.ToastInfo,1312 a.modeToaster.Show(ctx, mode.DisplayName(), component.ToastInfo,
1312 component.WithDuration(0), // Persistent until mode exits.1313 component.WithDuration(0), // Persistent until mode exits.
1313 component.WithPosition(component.ToastPositionBottomLeft),1314 component.WithPosition(component.ToastPositionBottomLeft),
1315+ component.WithModeClass(modeClass),
1314 )1316 )
1315-
1316- // Apply mode-specific CSS class after Show() to override the default level styling.
1317- modeClass := getModeToastClass(mode)
1318- a.modeToaster.ApplyModeClass(modeClass)
1319 }1317 }
1320 1318
1321 // getModeToastClass returns the CSS class for the given mode's toast styling.1319 // getModeToastClass returns the CSS class for the given mode's toast styling.
M internal/ui/component/toaster.go +55 -17
@@ -78,6 +78,9 @@ type ToastOptions struct {
7878 TextColor string
7979 // Position determines where the toast appears on screen.
8080 Position ToastPosition
81+ // ModeClass is a CSS class for mode-specific styling (e.g., "toast-pane-mode").
82+ // When set, this class is applied atomically with Show() to avoid visual flicker.
83+ ModeClass string
8184 }
8285
8386 // ToastOption is a functional option for configuring toast display.
@@ -112,6 +115,14 @@ func WithPosition(pos ToastPosition) ToastOption {
112115 }
113116 }
114117
118+// WithModeClass sets a mode-specific CSS class for styling.
119+// This is applied atomically within Show() to avoid visual flicker.
120+func WithModeClass(class string) ToastOption {
121+ return func(o *ToastOptions) {
122+ o.ModeClass = class
123+ }
124+}
125+
115126 // defaultToastOptions returns the default toast options.
116127 func defaultToastOptions() ToastOptions {
117128 return ToastOptions{
@@ -133,11 +144,12 @@ type Toaster struct {
133144 dismissTimer uint // GLib timer source ID
134145
135146 // Track current options for cleanup
136- currentOpts ToastOptions
137- hasCustomStyle bool
138- customBgColor string // Current custom background color
139- customTextColor string // Current custom text color
140- customModeClass string // Current mode-specific CSS class (e.g., "toast-pane-mode")
147+ currentOpts ToastOptions
148+ hasCustomStyle bool
149+ customBgColor string // Current custom background color
150+ customTextColor string // Current custom text color
151+ customModeClass string // Current mode-specific CSS class (e.g., "toast-pane-mode")
152+ customCSSProvider *gtk.CssProvider // CSS provider for inline custom colors
141153
142154 mu sync.Mutex
143155 }
@@ -209,6 +221,11 @@ func (t *Toaster) Show(ctx context.Context, message string, level ToastLevel, op
209221 // Apply custom colors if specified
210222 t.applyCustomStyle(options)
211223
224+ // Apply mode class if specified (atomically to avoid flicker)
225+ if options.ModeClass != "" {
226+ t.applyModeClassInternal(options.ModeClass)
227+ }
228+
212229 // Apply position
213230 t.applyPosition(options.Position)
214231
@@ -308,8 +325,7 @@ func (t *Toaster) applyPosition(pos ToastPosition) {
308325 t.container.SetValign(valign)
309326 }
310327
311-// applyCustomStyle applies custom background and text colors.
312-// Uses a CSS class that is styled dynamically by the theme manager.
328+// applyCustomStyle applies custom background and text colors via inline CSS.
313329 func (t *Toaster) applyCustomStyle(opts ToastOptions) {
314330 if opts.BackgroundColor == "" && opts.TextColor == "" {
315331 return
@@ -319,7 +335,7 @@ func (t *Toaster) applyCustomStyle(opts ToastOptions) {
319335 t.container.AddCssClass("toast-custom")
320336 t.hasCustomStyle = true
321337
322- // Store the colors for access by theme/CSS generation
338+ // Store the colors
323339 t.customBgColor = opts.BackgroundColor
324340 t.customTextColor = opts.TextColor
325341
@@ -327,6 +343,28 @@ func (t *Toaster) applyCustomStyle(opts ToastOptions) {
327343 if opts.BackgroundColor != "" {
328344 t.container.RemoveCssClass(t.currentLevel.cssClass())
329345 }
346+
347+ // Generate inline CSS for custom colors
348+ css := ".toast-custom {"
349+ if opts.BackgroundColor != "" {
350+ css += fmt.Sprintf(" background-color: %s;", opts.BackgroundColor)
351+ }
352+ if opts.TextColor != "" {
353+ css += fmt.Sprintf(" color: %s;", opts.TextColor)
354+ }
355+ css += " }"
356+
357+ // Create or reuse CSS provider for this widget
358+ if t.customCSSProvider == nil {
359+ t.customCSSProvider = gtk.NewCssProvider()
360+ }
361+ t.customCSSProvider.LoadFromString(css)
362+
363+ // Apply to widget's style context
364+ styleCtx := t.container.GtkWidget().GetStyleContext()
365+ if styleCtx != nil {
366+ styleCtx.AddProvider(t.customCSSProvider, uint(gtk.STYLE_PROVIDER_PRIORITY_APPLICATION+1))
367+ }
330368 }
331369
332370 // clearCustomStyle removes any custom styling applied.
@@ -338,6 +376,13 @@ func (t *Toaster) clearCustomStyle() {
338376 t.container.RemoveCssClass(t.customModeClass)
339377 t.customModeClass = ""
340378 }
379+ // Remove custom CSS provider if one was added
380+ if t.customCSSProvider != nil {
381+ styleCtx := t.container.GtkWidget().GetStyleContext()
382+ if styleCtx != nil {
383+ styleCtx.RemoveProvider(t.customCSSProvider)
384+ }
385+ }
341386 // Re-add level class if it was removed due to custom background
342387 if t.customBgColor != "" {
343388 t.container.AddCssClass(t.currentLevel.cssClass())
@@ -348,15 +393,8 @@ func (t *Toaster) clearCustomStyle() {
348393 }
349394 }
350395
351-// ApplyModeClass applies a mode-specific CSS class for styling.
352-// This uses the pre-defined CSS classes that reference CSS variables.
353-func (t *Toaster) ApplyModeClass(modeClass string) {
354- t.mu.Lock()
355- defer t.mu.Unlock()
356-
357- // Clear any existing custom style first
358- t.clearCustomStyle()
359-
396+// applyModeClassInternal applies mode class without locking (must be called with lock held).
397+func (t *Toaster) applyModeClassInternal(modeClass string) {
360398 // Remove level class and add mode class
361399 t.container.RemoveCssClass(t.currentLevel.cssClass())
362400 t.container.AddCssClass(modeClass)
@@ -78,6 +78,9 @@ type ToastOptions struct {
78 TextColor string78 TextColor string
79 // Position determines where the toast appears on screen.79 // Position determines where the toast appears on screen.
80 Position ToastPosition80 Position ToastPosition
81+ // ModeClass is a CSS class for mode-specific styling (e.g., "toast-pane-mode").
82+ // When set, this class is applied atomically with Show() to avoid visual flicker.
83+ ModeClass string
81 }84 }
82 85
83 // ToastOption is a functional option for configuring toast display.86 // ToastOption is a functional option for configuring toast display.
@@ -112,6 +115,14 @@ func WithPosition(pos ToastPosition) ToastOption {
112 }115 }
113 }116 }
114 117
118+// WithModeClass sets a mode-specific CSS class for styling.
119+// This is applied atomically within Show() to avoid visual flicker.
120+func WithModeClass(class string) ToastOption {
121+ return func(o *ToastOptions) {
122+ o.ModeClass = class
123+ }
124+}
125+
115 // defaultToastOptions returns the default toast options.126 // defaultToastOptions returns the default toast options.
116 func defaultToastOptions() ToastOptions {127 func defaultToastOptions() ToastOptions {
117 return ToastOptions{128 return ToastOptions{
@@ -133,11 +144,12 @@ type Toaster struct {
133 dismissTimer uint // GLib timer source ID144 dismissTimer uint // GLib timer source ID
134 145
135 // Track current options for cleanup146 // Track current options for cleanup
136- currentOpts ToastOptions147+ currentOpts ToastOptions
137- hasCustomStyle bool148+ hasCustomStyle bool
138- customBgColor string // Current custom background color149+ customBgColor string // Current custom background color
139- customTextColor string // Current custom text color150+ customTextColor string // Current custom text color
140- customModeClass string // Current mode-specific CSS class (e.g., "toast-pane-mode")151+ customModeClass string // Current mode-specific CSS class (e.g., "toast-pane-mode")
152+ customCSSProvider *gtk.CssProvider // CSS provider for inline custom colors
141 153
142 mu sync.Mutex154 mu sync.Mutex
143 }155 }
@@ -209,6 +221,11 @@ func (t *Toaster) Show(ctx context.Context, message string, level ToastLevel, op
209 // Apply custom colors if specified221 // Apply custom colors if specified
210 t.applyCustomStyle(options)222 t.applyCustomStyle(options)
211 223
224+ // Apply mode class if specified (atomically to avoid flicker)
225+ if options.ModeClass != "" {
226+ t.applyModeClassInternal(options.ModeClass)
227+ }
228+
212 // Apply position229 // Apply position
213 t.applyPosition(options.Position)230 t.applyPosition(options.Position)
214 231
@@ -308,8 +325,7 @@ func (t *Toaster) applyPosition(pos ToastPosition) {
308 t.container.SetValign(valign)325 t.container.SetValign(valign)
309 }326 }
310 327
311-// applyCustomStyle applies custom background and text colors.328+// applyCustomStyle applies custom background and text colors via inline CSS.
312-// Uses a CSS class that is styled dynamically by the theme manager.
313 func (t *Toaster) applyCustomStyle(opts ToastOptions) {329 func (t *Toaster) applyCustomStyle(opts ToastOptions) {
314 if opts.BackgroundColor == "" && opts.TextColor == "" {330 if opts.BackgroundColor == "" && opts.TextColor == "" {
315 return331 return
@@ -319,7 +335,7 @@ func (t *Toaster) applyCustomStyle(opts ToastOptions) {
319 t.container.AddCssClass("toast-custom")335 t.container.AddCssClass("toast-custom")
320 t.hasCustomStyle = true336 t.hasCustomStyle = true
321 337
322- // Store the colors for access by theme/CSS generation338+ // Store the colors
323 t.customBgColor = opts.BackgroundColor339 t.customBgColor = opts.BackgroundColor
324 t.customTextColor = opts.TextColor340 t.customTextColor = opts.TextColor
325 341
@@ -327,6 +343,28 @@ func (t *Toaster) applyCustomStyle(opts ToastOptions) {
327 if opts.BackgroundColor != "" {343 if opts.BackgroundColor != "" {
328 t.container.RemoveCssClass(t.currentLevel.cssClass())344 t.container.RemoveCssClass(t.currentLevel.cssClass())
329 }345 }
346+
347+ // Generate inline CSS for custom colors
348+ css := ".toast-custom {"
349+ if opts.BackgroundColor != "" {
350+ css += fmt.Sprintf(" background-color: %s;", opts.BackgroundColor)
351+ }
352+ if opts.TextColor != "" {
353+ css += fmt.Sprintf(" color: %s;", opts.TextColor)
354+ }
355+ css += " }"
356+
357+ // Create or reuse CSS provider for this widget
358+ if t.customCSSProvider == nil {
359+ t.customCSSProvider = gtk.NewCssProvider()
360+ }
361+ t.customCSSProvider.LoadFromString(css)
362+
363+ // Apply to widget's style context
364+ styleCtx := t.container.GtkWidget().GetStyleContext()
365+ if styleCtx != nil {
366+ styleCtx.AddProvider(t.customCSSProvider, uint(gtk.STYLE_PROVIDER_PRIORITY_APPLICATION+1))
367+ }
330 }368 }
331 369
332 // clearCustomStyle removes any custom styling applied.370 // clearCustomStyle removes any custom styling applied.
@@ -338,6 +376,13 @@ func (t *Toaster) clearCustomStyle() {
338 t.container.RemoveCssClass(t.customModeClass)376 t.container.RemoveCssClass(t.customModeClass)
339 t.customModeClass = ""377 t.customModeClass = ""
340 }378 }
379+ // Remove custom CSS provider if one was added
380+ if t.customCSSProvider != nil {
381+ styleCtx := t.container.GtkWidget().GetStyleContext()
382+ if styleCtx != nil {
383+ styleCtx.RemoveProvider(t.customCSSProvider)
384+ }
385+ }
341 // Re-add level class if it was removed due to custom background386 // Re-add level class if it was removed due to custom background
342 if t.customBgColor != "" {387 if t.customBgColor != "" {
343 t.container.AddCssClass(t.currentLevel.cssClass())388 t.container.AddCssClass(t.currentLevel.cssClass())
@@ -348,15 +393,8 @@ func (t *Toaster) clearCustomStyle() {
348 }393 }
349 }394 }
350 395
351-// ApplyModeClass applies a mode-specific CSS class for styling.396+// applyModeClassInternal applies mode class without locking (must be called with lock held).
352-// This uses the pre-defined CSS classes that reference CSS variables.397+func (t *Toaster) applyModeClassInternal(modeClass string) {
353-func (t *Toaster) ApplyModeClass(modeClass string) {
354- t.mu.Lock()
355- defer t.mu.Unlock()
356-
357- // Clear any existing custom style first
358- t.clearCustomStyle()
359-
360 // Remove level class and add mode class398 // Remove level class and add mode class
361 t.container.RemoveCssClass(t.currentLevel.cssClass())399 t.container.RemoveCssClass(t.currentLevel.cssClass())
362 t.container.AddCssClass(modeClass)400 t.container.AddCssClass(modeClass)

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