refactor(theme): add reusable truecolor dimmer
- commit
- 699a23261a54412f2563477826b4488c19c9c843
- author
- brice · · 2026-07-15 04:33 UTC
- parent
- 201f7b0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +225 -40
M internal/usecase/daemon/floating_render.go +1 -1
| @@ -90,7 +90,7 @@ func composeCapturedFloatingFrame(base renderer.Frame, baseDamage []renderer.Dam | |||
| 90 | 90 | legacyLayout := tabLayoutSnapshot{placements: layoutSnap.placements, area: layoutSnap.area, focus: layoutSnap.focus, ok: layoutSnap.valid} | |
| 91 | 91 | (overlayBackdrop{DimPaneContents: true}).apply(frame, content, legacyLayout, theme) | |
| 92 | 92 | geometry := floating.geometry.translate(content.X, content.Y) | |
| 93 | - | blitPaneFrame(frame, geometry.Inner, floating.pane.frame, false, theme) | |
| 93 | + | blitPaneFrame(frame, geometry.Inner, floating.pane.frame, false, themeui.NewDimmer(theme)) | |
| 94 | 94 | damage := append([]renderer.Damage(nil), baseDamage...) | |
| 95 | 95 | for _, d := range floating.pane.damage { | |
| 96 | 96 | damage = append(damage, translatePaneDamage(d, geometry.Inner, content)...) | |
| @@ -90,7 +90,7 @@ func composeCapturedFloatingFrame(base renderer.Frame, baseDamage []renderer.Dam | |||||
| 90 | legacyLayout := tabLayoutSnapshot{placements: layoutSnap.placements, area: layoutSnap.area, focus: layoutSnap.focus, ok: layoutSnap.valid} | 90 | legacyLayout := tabLayoutSnapshot{placements: layoutSnap.placements, area: layoutSnap.area, focus: layoutSnap.focus, ok: layoutSnap.valid} | ||
| 91 | (overlayBackdrop{DimPaneContents: true}).apply(frame, content, legacyLayout, theme) | 91 | (overlayBackdrop{DimPaneContents: true}).apply(frame, content, legacyLayout, theme) | ||
| 92 | geometry := floating.geometry.translate(content.X, content.Y) | 92 | geometry := floating.geometry.translate(content.X, content.Y) | ||
| 93 | - | blitPaneFrame(frame, geometry.Inner, floating.pane.frame, false, theme) | 93 | + | blitPaneFrame(frame, geometry.Inner, floating.pane.frame, false, themeui.NewDimmer(theme)) |
| 94 | damage := append([]renderer.Damage(nil), baseDamage...) | 94 | damage := append([]renderer.Damage(nil), baseDamage...) | ||
| 95 | for _, d := range floating.pane.damage { | 95 | for _, d := range floating.pane.damage { | ||
| 96 | damage = append(damage, translatePaneDamage(d, geometry.Inner, content)...) | 96 | damage = append(damage, translatePaneDamage(d, geometry.Inner, content)...) | ||
M internal/usecase/daemon/floating_render_legacy_test.go +1 -1
| @@ -44,7 +44,7 @@ func composeFloatingFrame(base renderer.Frame, baseDamage []renderer.Damage, p * | |||
| 44 | 44 | p.mu.Unlock() | |
| 45 | 45 | title := captured.title | |
| 46 | 46 | titleGeneration := captured.titleGeneration | |
| 47 | - | blitPaneFrame(frame, geometry.Inner, captured.frame, false, theme) | |
| 47 | + | blitPaneFrame(frame, geometry.Inner, captured.frame, false, themeui.NewDimmer(theme)) | |
| 48 | 48 | damage := append([]renderer.Damage(nil), baseDamage...) | |
| 49 | 49 | for _, d := range captured.damage { | |
| 50 | 50 | damage = append(damage, translatePaneDamage(d, geometry.Inner, content)...) | |
| @@ -44,7 +44,7 @@ func composeFloatingFrame(base renderer.Frame, baseDamage []renderer.Damage, p * | |||||
| 44 | p.mu.Unlock() | 44 | p.mu.Unlock() | ||
| 45 | title := captured.title | 45 | title := captured.title | ||
| 46 | titleGeneration := captured.titleGeneration | 46 | titleGeneration := captured.titleGeneration | ||
| 47 | - | blitPaneFrame(frame, geometry.Inner, captured.frame, false, theme) | 47 | + | blitPaneFrame(frame, geometry.Inner, captured.frame, false, themeui.NewDimmer(theme)) |
| 48 | damage := append([]renderer.Damage(nil), baseDamage...) | 48 | damage := append([]renderer.Damage(nil), baseDamage...) | ||
| 49 | for _, d := range captured.damage { | 49 | for _, d := range captured.damage { | ||
| 50 | damage = append(damage, translatePaneDamage(d, geometry.Inner, content)...) | 50 | damage = append(damage, translatePaneDamage(d, geometry.Inner, content)...) | ||
M internal/usecase/daemon/overlay_backdrop.go +2 -1
| @@ -36,6 +36,7 @@ func (b overlayBackdrop) apply(frame renderer.Frame, contentArea domain.Rect, la | |||
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | 38 | func dimFrameRect(frame renderer.Frame, rect domain.Rect, theme themeui.Theme) { | |
| 39 | + | dimmer := themeui.NewDimmer(theme) | |
| 39 | 40 | x0 := max(rect.X, 0) | |
| 40 | 41 | y0 := max(rect.Y, 0) | |
| 41 | 42 | x1 := min(rect.X+rect.Width, frame.Width) | |
| @@ -43,7 +44,7 @@ func dimFrameRect(frame renderer.Frame, rect domain.Rect, theme themeui.Theme) { | |||
| 43 | 44 | for y := y0; y < y1; y++ { | |
| 44 | 45 | for x := x0; x < x1; x++ { | |
| 45 | 46 | cell := frame.At(x, y) | |
| 46 | - | cell.Style = themeui.DimStyle(cell.Style, theme) | |
| 47 | + | cell.Style = dimmer.Dim(cell.Style) | |
| 47 | 48 | frame.Set(x, y, cell) | |
| 48 | 49 | } | |
| 49 | 50 | } | |
| @@ -36,6 +36,7 @@ func (b overlayBackdrop) apply(frame renderer.Frame, contentArea domain.Rect, la | |||||
| 36 | } | 36 | } | ||
| 37 | 37 | ||||
| 38 | func dimFrameRect(frame renderer.Frame, rect domain.Rect, theme themeui.Theme) { | 38 | func dimFrameRect(frame renderer.Frame, rect domain.Rect, theme themeui.Theme) { | ||
| 39 | + | dimmer := themeui.NewDimmer(theme) | |||
| 39 | x0 := max(rect.X, 0) | 40 | x0 := max(rect.X, 0) | ||
| 40 | y0 := max(rect.Y, 0) | 41 | y0 := max(rect.Y, 0) | ||
| 41 | x1 := min(rect.X+rect.Width, frame.Width) | 42 | x1 := min(rect.X+rect.Width, frame.Width) | ||
| @@ -43,7 +44,7 @@ func dimFrameRect(frame renderer.Frame, rect domain.Rect, theme themeui.Theme) { | |||||
| 43 | for y := y0; y < y1; y++ { | 44 | for y := y0; y < y1; y++ { | ||
| 44 | for x := x0; x < x1; x++ { | 45 | for x := x0; x < x1; x++ { | ||
| 45 | cell := frame.At(x, y) | 46 | cell := frame.At(x, y) | ||
| 46 | - | cell.Style = themeui.DimStyle(cell.Style, theme) | 47 | + | cell.Style = dimmer.Dim(cell.Style) |
| 47 | frame.Set(x, y, cell) | 48 | frame.Set(x, y, cell) | ||
| 48 | } | 49 | } | ||
| 49 | } | 50 | } | ||
M internal/usecase/daemon/overlay_backdrop_test.go +1 -1
| @@ -71,7 +71,7 @@ func TestOverlayBackdropClipsPlacementsToFrame(t *testing.T) { | |||
| 71 | 71 | (overlayBackdrop{DimPaneContents: true}).apply(frame, domain.Rect{Width: 4, Height: 4}, snap, backdropTheme()) | |
| 72 | 72 | ||
| 73 | 73 | for _, point := range tt.affected { | |
| 74 | - | require.Equal(t, themeui.DimStyle(original, backdropTheme()), frame.At(point[0], point[1]).Style) | |
| 74 | + | require.Equal(t, themeui.NewDimmer(backdropTheme()).Dim(original), frame.At(point[0], point[1]).Style) | |
| 75 | 75 | } | |
| 76 | 76 | for _, point := range tt.unaffected { | |
| 77 | 77 | require.Equal(t, original, frame.At(point[0], point[1]).Style) | |
| @@ -71,7 +71,7 @@ func TestOverlayBackdropClipsPlacementsToFrame(t *testing.T) { | |||||
| 71 | (overlayBackdrop{DimPaneContents: true}).apply(frame, domain.Rect{Width: 4, Height: 4}, snap, backdropTheme()) | 71 | (overlayBackdrop{DimPaneContents: true}).apply(frame, domain.Rect{Width: 4, Height: 4}, snap, backdropTheme()) | ||
| 72 | 72 | ||||
| 73 | for _, point := range tt.affected { | 73 | for _, point := range tt.affected { | ||
| 74 | - | require.Equal(t, themeui.DimStyle(original, backdropTheme()), frame.At(point[0], point[1]).Style) | 74 | + | require.Equal(t, themeui.NewDimmer(backdropTheme()).Dim(original), frame.At(point[0], point[1]).Style) |
| 75 | } | 75 | } | ||
| 76 | for _, point := range tt.unaffected { | 76 | for _, point := range tt.unaffected { | ||
| 77 | require.Equal(t, original, frame.At(point[0], point[1]).Style) | 77 | require.Equal(t, original, frame.At(point[0], point[1]).Style) | ||
M internal/usecase/daemon/render.go +2 -2
| @@ -423,14 +423,14 @@ func offsetPlacement(pl layout.Placement, dx, dy int) layout.Placement { | |||
| 423 | 423 | return pl | |
| 424 | 424 | } | |
| 425 | 425 | ||
| 426 | - | func blitPaneFrame(dst renderer.Frame, r domain.Rect, src renderer.Frame, dim bool, theme themeui.Theme) { | |
| 426 | + | func blitPaneFrame(dst renderer.Frame, r domain.Rect, src renderer.Frame, dim bool, dimmer themeui.Dimmer) { | |
| 427 | 427 | rows := min(r.Height, src.Height) | |
| 428 | 428 | cols := min(r.Width, src.Width) | |
| 429 | 429 | for y := range rows { | |
| 430 | 430 | for x := range cols { | |
| 431 | 431 | cell := src.At(x, y) | |
| 432 | 432 | if dim { | |
| 433 | - | cell.Style = themeui.DimStyle(cell.Style, theme) | |
| 433 | + | cell.Style = dimmer.Dim(cell.Style) | |
| 434 | 434 | } | |
| 435 | 435 | dst.Set(r.X+x, r.Y+y, cell) | |
| 436 | 436 | } | |
| @@ -423,14 +423,14 @@ func offsetPlacement(pl layout.Placement, dx, dy int) layout.Placement { | |||||
| 423 | return pl | 423 | return pl | ||
| 424 | } | 424 | } | ||
| 425 | 425 | ||||
| 426 | - | func blitPaneFrame(dst renderer.Frame, r domain.Rect, src renderer.Frame, dim bool, theme themeui.Theme) { | 426 | + | func blitPaneFrame(dst renderer.Frame, r domain.Rect, src renderer.Frame, dim bool, dimmer themeui.Dimmer) { |
| 427 | rows := min(r.Height, src.Height) | 427 | rows := min(r.Height, src.Height) | ||
| 428 | cols := min(r.Width, src.Width) | 428 | cols := min(r.Width, src.Width) | ||
| 429 | for y := range rows { | 429 | for y := range rows { | ||
| 430 | for x := range cols { | 430 | for x := range cols { | ||
| 431 | cell := src.At(x, y) | 431 | cell := src.At(x, y) | ||
| 432 | if dim { | 432 | if dim { | ||
| 433 | - | cell.Style = themeui.DimStyle(cell.Style, theme) | 433 | + | cell.Style = dimmer.Dim(cell.Style) |
| 434 | } | 434 | } | ||
| 435 | dst.Set(r.X+x, r.Y+y, cell) | 435 | dst.Set(r.X+x, r.Y+y, cell) | ||
| 436 | } | 436 | } | ||
M internal/usecase/daemon/render_legacy_test.go +4 -4
| @@ -176,7 +176,7 @@ func composeTabFrameIntoWithLayoutOptions(tb *tab, frame renderer.Frame, area do | |||
| 176 | 176 | clear(titleGenerations) | |
| 177 | 177 | } | |
| 178 | 178 | if ok && !cacheValid { | |
| 179 | - | drawDividers(frame, root, area, themeui.DimStyle(newThemeStyles(theme).border, theme)) | |
| 179 | + | drawDividers(frame, root, area, themeui.NewDimmer(theme).Dim(newThemeStyles(theme).border)) | |
| 180 | 180 | } | |
| 181 | 181 | var damage []renderer.Damage | |
| 182 | 182 | for _, pl := range placements { | |
| @@ -222,7 +222,7 @@ func composeTabFrameIntoWithLayoutOptions(tb *tab, frame renderer.Frame, area do | |||
| 222 | 222 | p.mu.Lock() | |
| 223 | 223 | paneDamage = p.screen.Damage() | |
| 224 | 224 | if !cacheValid || len(paneDamage) > 0 { | |
| 225 | - | blitPaneFrame(frame, pl.Content, p.screen.Frame, !focused, theme) | |
| 225 | + | blitPaneFrame(frame, pl.Content, p.screen.Frame, !focused, themeui.NewDimmer(theme, themeui.WithForegroundDimming(inactivePaneForegroundDimming))) | |
| 226 | 226 | } | |
| 227 | 227 | for _, d := range paneDamage { | |
| 228 | 228 | localContent := pl.Content | |
| @@ -234,7 +234,7 @@ func composeTabFrameIntoWithLayoutOptions(tb *tab, frame renderer.Frame, area do | |||
| 234 | 234 | continue | |
| 235 | 235 | } | |
| 236 | 236 | if !cacheValid || len(paneDamage) > 0 { | |
| 237 | - | blitPaneFrame(frame, pl.Content, paneFrame, !focused, theme) | |
| 237 | + | blitPaneFrame(frame, pl.Content, paneFrame, !focused, themeui.NewDimmer(theme, themeui.WithForegroundDimming(inactivePaneForegroundDimming))) | |
| 238 | 238 | } | |
| 239 | 239 | for _, d := range paneDamage { | |
| 240 | 240 | localContent := pl.Content | |
| @@ -265,7 +265,7 @@ func drawPaneTitleBar(frame renderer.Frame, pl layout.Placement, p *pane, focuse | |||
| 265 | 265 | if focused { | |
| 266 | 266 | style = styles.statusBar | |
| 267 | 267 | } else { | |
| 268 | - | style = themeui.DimStyle(style, theme) | |
| 268 | + | style = themeui.NewDimmer(theme).Dim(style) | |
| 269 | 269 | } | |
| 270 | 270 | for x := pl.TitleBar.X; x < pl.TitleBar.X+pl.TitleBar.Width && x < frame.Width; x++ { | |
| 271 | 271 | frame.Set(x, pl.TitleBar.Y, renderer.Cell{Rune: ' ', Style: style}) | |
| @@ -176,7 +176,7 @@ func composeTabFrameIntoWithLayoutOptions(tb *tab, frame renderer.Frame, area do | |||||
| 176 | clear(titleGenerations) | 176 | clear(titleGenerations) | ||
| 177 | } | 177 | } | ||
| 178 | if ok && !cacheValid { | 178 | if ok && !cacheValid { | ||
| 179 | - | drawDividers(frame, root, area, themeui.DimStyle(newThemeStyles(theme).border, theme)) | 179 | + | drawDividers(frame, root, area, themeui.NewDimmer(theme).Dim(newThemeStyles(theme).border)) |
| 180 | } | 180 | } | ||
| 181 | var damage []renderer.Damage | 181 | var damage []renderer.Damage | ||
| 182 | for _, pl := range placements { | 182 | for _, pl := range placements { | ||
| @@ -222,7 +222,7 @@ func composeTabFrameIntoWithLayoutOptions(tb *tab, frame renderer.Frame, area do | |||||
| 222 | p.mu.Lock() | 222 | p.mu.Lock() | ||
| 223 | paneDamage = p.screen.Damage() | 223 | paneDamage = p.screen.Damage() | ||
| 224 | if !cacheValid || len(paneDamage) > 0 { | 224 | if !cacheValid || len(paneDamage) > 0 { | ||
| 225 | - | blitPaneFrame(frame, pl.Content, p.screen.Frame, !focused, theme) | 225 | + | blitPaneFrame(frame, pl.Content, p.screen.Frame, !focused, themeui.NewDimmer(theme, themeui.WithForegroundDimming(inactivePaneForegroundDimming))) |
| 226 | } | 226 | } | ||
| 227 | for _, d := range paneDamage { | 227 | for _, d := range paneDamage { | ||
| 228 | localContent := pl.Content | 228 | localContent := pl.Content | ||
| @@ -234,7 +234,7 @@ func composeTabFrameIntoWithLayoutOptions(tb *tab, frame renderer.Frame, area do | |||||
| 234 | continue | 234 | continue | ||
| 235 | } | 235 | } | ||
| 236 | if !cacheValid || len(paneDamage) > 0 { | 236 | if !cacheValid || len(paneDamage) > 0 { | ||
| 237 | - | blitPaneFrame(frame, pl.Content, paneFrame, !focused, theme) | 237 | + | blitPaneFrame(frame, pl.Content, paneFrame, !focused, themeui.NewDimmer(theme, themeui.WithForegroundDimming(inactivePaneForegroundDimming))) |
| 238 | } | 238 | } | ||
| 239 | for _, d := range paneDamage { | 239 | for _, d := range paneDamage { | ||
| 240 | localContent := pl.Content | 240 | localContent := pl.Content | ||
| @@ -265,7 +265,7 @@ func drawPaneTitleBar(frame renderer.Frame, pl layout.Placement, p *pane, focuse | |||||
| 265 | if focused { | 265 | if focused { | ||
| 266 | style = styles.statusBar | 266 | style = styles.statusBar | ||
| 267 | } else { | 267 | } else { | ||
| 268 | - | style = themeui.DimStyle(style, theme) | 268 | + | style = themeui.NewDimmer(theme).Dim(style) |
| 269 | } | 269 | } | ||
| 270 | for x := pl.TitleBar.X; x < pl.TitleBar.X+pl.TitleBar.Width && x < frame.Width; x++ { | 270 | for x := pl.TitleBar.X; x < pl.TitleBar.X+pl.TitleBar.Width && x < frame.Width; x++ { | ||
| 271 | frame.Set(x, pl.TitleBar.Y, renderer.Cell{Rune: ' ', Style: style}) | 271 | frame.Set(x, pl.TitleBar.Y, renderer.Cell{Rune: ' ', Style: style}) | ||
M internal/usecase/daemon/render_pipeline.go +9 -6
| @@ -52,6 +52,8 @@ type composedRenderFrame struct { | |||
| 52 | 52 | // composeFrame is pure with respect to daemon ownership: it consumes only the | |
| 53 | 53 | // capture, the last committed cache, and an independent attachment-owned | |
| 54 | 54 | // scratch cache. It returns a replacement cache without mutating committed. | |
| 55 | + | const inactivePaneForegroundDimming = 55 | |
| 56 | + | ||
| 55 | 57 | func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ...composeCacheInput) composedRenderFrame { | |
| 56 | 58 | scratch := composeCacheInput{} | |
| 57 | 59 | if len(scratchIn) > 0 { | |
| @@ -74,11 +76,13 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ... | |||
| 74 | 76 | } | |
| 75 | 77 | } | |
| 76 | 78 | styles := newThemeStyles(state.theme) | |
| 79 | + | defaultDimmer := themeui.NewDimmer(state.theme) | |
| 80 | + | inactivePaneDimmer := themeui.NewDimmer(state.theme, themeui.WithForegroundDimming(inactivePaneForegroundDimming)) | |
| 77 | 81 | drawTopBarSnapshot(frame.Row(0), state.bars.status, state.bars.attentionFrame, state.bars.topRight, styles) | |
| 78 | 82 | drawStatusBarState(frame.Row(rows+1), state.bars, styles) | |
| 79 | 83 | content := domain.Rect{Y: 1, Width: width, Height: rows} | |
| 80 | 84 | if state.layout.valid && state.layout.root != nil { | |
| 81 | - | drawDividers(frame, state.layout.root, content, themeui.DimStyle(styles.border, state.theme)) | |
| 85 | + | drawDividers(frame, state.layout.root, content, defaultDimmer.Dim(styles.border)) | |
| 82 | 86 | } | |
| 83 | 87 | ||
| 84 | 88 | full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme || in.floatingVisible != state.floating.visible | |
| @@ -90,14 +94,14 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ... | |||
| 90 | 94 | for _, pane := range state.panes { | |
| 91 | 95 | pl := offsetPlacement(pane.placement, 0, 1) | |
| 92 | 96 | if pl.TitleBar.Height > 0 { | |
| 93 | - | drawCapturedPaneTitleBar(frame, pl, pane.title, pane.focused, state.theme) | |
| 97 | + | drawCapturedPaneTitleBar(frame, pl, pane.title, pane.focused, styles, defaultDimmer) | |
| 94 | 98 | if !full && in.titleGenerations[pane.id] != pane.titleGeneration { | |
| 95 | 99 | damage = append(damage, renderer.Damage{Kind: renderer.DamageText, X: pl.TitleBar.X, Y: pl.TitleBar.Y, Width: pl.TitleBar.Width, Height: pl.TitleBar.Height}) | |
| 96 | 100 | } | |
| 97 | 101 | titles[pane.id] = pane.titleGeneration | |
| 98 | 102 | } | |
| 99 | 103 | if !pl.Collapsed && pl.Content.Width > 0 && pl.Content.Height > 0 && (full || len(pane.damage) > 0) { | |
| 100 | - | blitPaneFrame(frame, pl.Content, pane.frame, !pane.focused, state.theme) | |
| 104 | + | blitPaneFrame(frame, pl.Content, pane.frame, !pane.focused, inactivePaneDimmer) | |
| 101 | 105 | } | |
| 102 | 106 | for _, d := range pane.damage { | |
| 103 | 107 | if d.Kind != renderer.DamageFullRedraw { | |
| @@ -141,13 +145,12 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ... | |||
| 141 | 145 | return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()} | |
| 142 | 146 | } | |
| 143 | 147 | ||
| 144 | - | func drawCapturedPaneTitleBar(frame renderer.Frame, pl layout.Placement, title string, focused bool, theme themeui.Theme) { | |
| 145 | - | styles := newThemeStyles(theme) | |
| 148 | + | func drawCapturedPaneTitleBar(frame renderer.Frame, pl layout.Placement, title string, focused bool, styles themeStyles, dimmer themeui.Dimmer) { | |
| 146 | 149 | style := styles.border | |
| 147 | 150 | if focused { | |
| 148 | 151 | style = styles.statusBar | |
| 149 | 152 | } else { | |
| 150 | - | style = themeui.DimStyle(style, theme) | |
| 153 | + | style = dimmer.Dim(style) | |
| 151 | 154 | } | |
| 152 | 155 | for x := pl.TitleBar.X; x < pl.TitleBar.X+pl.TitleBar.Width && x < frame.Width; x++ { | |
| 153 | 156 | frame.Set(x, pl.TitleBar.Y, renderer.Cell{Rune: ' ', Style: style}) | |
| @@ -52,6 +52,8 @@ type composedRenderFrame struct { | |||||
| 52 | // composeFrame is pure with respect to daemon ownership: it consumes only the | 52 | // composeFrame is pure with respect to daemon ownership: it consumes only the | ||
| 53 | // capture, the last committed cache, and an independent attachment-owned | 53 | // capture, the last committed cache, and an independent attachment-owned | ||
| 54 | // scratch cache. It returns a replacement cache without mutating committed. | 54 | // scratch cache. It returns a replacement cache without mutating committed. | ||
| 55 | + | const inactivePaneForegroundDimming = 55 | |||
| 56 | + | ||||
| 55 | func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ...composeCacheInput) composedRenderFrame { | 57 | func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ...composeCacheInput) composedRenderFrame { | ||
| 56 | scratch := composeCacheInput{} | 58 | scratch := composeCacheInput{} | ||
| 57 | if len(scratchIn) > 0 { | 59 | if len(scratchIn) > 0 { | ||
| @@ -74,11 +76,13 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ... | |||||
| 74 | } | 76 | } | ||
| 75 | } | 77 | } | ||
| 76 | styles := newThemeStyles(state.theme) | 78 | styles := newThemeStyles(state.theme) | ||
| 79 | + | defaultDimmer := themeui.NewDimmer(state.theme) | |||
| 80 | + | inactivePaneDimmer := themeui.NewDimmer(state.theme, themeui.WithForegroundDimming(inactivePaneForegroundDimming)) | |||
| 77 | drawTopBarSnapshot(frame.Row(0), state.bars.status, state.bars.attentionFrame, state.bars.topRight, styles) | 81 | drawTopBarSnapshot(frame.Row(0), state.bars.status, state.bars.attentionFrame, state.bars.topRight, styles) | ||
| 78 | drawStatusBarState(frame.Row(rows+1), state.bars, styles) | 82 | drawStatusBarState(frame.Row(rows+1), state.bars, styles) | ||
| 79 | content := domain.Rect{Y: 1, Width: width, Height: rows} | 83 | content := domain.Rect{Y: 1, Width: width, Height: rows} | ||
| 80 | if state.layout.valid && state.layout.root != nil { | 84 | if state.layout.valid && state.layout.root != nil { | ||
| 81 | - | drawDividers(frame, state.layout.root, content, themeui.DimStyle(styles.border, state.theme)) | 85 | + | drawDividers(frame, state.layout.root, content, defaultDimmer.Dim(styles.border)) |
| 82 | } | 86 | } | ||
| 83 | 87 | ||||
| 84 | full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme || in.floatingVisible != state.floating.visible | 88 | full := state.reset || !in.valid || in.frame.Width != width || in.frame.Height != rows+2 || in.layoutFingerprint != state.layout.fingerprint || in.theme != state.theme || in.floatingVisible != state.floating.visible | ||
| @@ -90,14 +94,14 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ... | |||||
| 90 | for _, pane := range state.panes { | 94 | for _, pane := range state.panes { | ||
| 91 | pl := offsetPlacement(pane.placement, 0, 1) | 95 | pl := offsetPlacement(pane.placement, 0, 1) | ||
| 92 | if pl.TitleBar.Height > 0 { | 96 | if pl.TitleBar.Height > 0 { | ||
| 93 | - | drawCapturedPaneTitleBar(frame, pl, pane.title, pane.focused, state.theme) | 97 | + | drawCapturedPaneTitleBar(frame, pl, pane.title, pane.focused, styles, defaultDimmer) |
| 94 | if !full && in.titleGenerations[pane.id] != pane.titleGeneration { | 98 | if !full && in.titleGenerations[pane.id] != pane.titleGeneration { | ||
| 95 | damage = append(damage, renderer.Damage{Kind: renderer.DamageText, X: pl.TitleBar.X, Y: pl.TitleBar.Y, Width: pl.TitleBar.Width, Height: pl.TitleBar.Height}) | 99 | damage = append(damage, renderer.Damage{Kind: renderer.DamageText, X: pl.TitleBar.X, Y: pl.TitleBar.Y, Width: pl.TitleBar.Width, Height: pl.TitleBar.Height}) | ||
| 96 | } | 100 | } | ||
| 97 | titles[pane.id] = pane.titleGeneration | 101 | titles[pane.id] = pane.titleGeneration | ||
| 98 | } | 102 | } | ||
| 99 | if !pl.Collapsed && pl.Content.Width > 0 && pl.Content.Height > 0 && (full || len(pane.damage) > 0) { | 103 | if !pl.Collapsed && pl.Content.Width > 0 && pl.Content.Height > 0 && (full || len(pane.damage) > 0) { | ||
| 100 | - | blitPaneFrame(frame, pl.Content, pane.frame, !pane.focused, state.theme) | 104 | + | blitPaneFrame(frame, pl.Content, pane.frame, !pane.focused, inactivePaneDimmer) |
| 101 | } | 105 | } | ||
| 102 | for _, d := range pane.damage { | 106 | for _, d := range pane.damage { | ||
| 103 | if d.Kind != renderer.DamageFullRedraw { | 107 | if d.Kind != renderer.DamageFullRedraw { | ||
| @@ -141,13 +145,12 @@ func composeFrame(state capturedRenderState, in composeCacheInput, scratchIn ... | |||||
| 141 | return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()} | 145 | return composedRenderFrame{frame: frame, damage: damage, cursor: cursor, cache: outCache, reset: state.reset || state.overlays.active()} | ||
| 142 | } | 146 | } | ||
| 143 | 147 | ||||
| 144 | - | func drawCapturedPaneTitleBar(frame renderer.Frame, pl layout.Placement, title string, focused bool, theme themeui.Theme) { | 148 | + | func drawCapturedPaneTitleBar(frame renderer.Frame, pl layout.Placement, title string, focused bool, styles themeStyles, dimmer themeui.Dimmer) { |
| 145 | - | styles := newThemeStyles(theme) | |||
| 146 | style := styles.border | 149 | style := styles.border | ||
| 147 | if focused { | 150 | if focused { | ||
| 148 | style = styles.statusBar | 151 | style = styles.statusBar | ||
| 149 | } else { | 152 | } else { | ||
| 150 | - | style = themeui.DimStyle(style, theme) | 153 | + | style = dimmer.Dim(style) |
| 151 | } | 154 | } | ||
| 152 | for x := pl.TitleBar.X; x < pl.TitleBar.X+pl.TitleBar.Width && x < frame.Width; x++ { | 155 | for x := pl.TitleBar.X; x < pl.TitleBar.X+pl.TitleBar.Width && x < frame.Width; x++ { | ||
| 153 | frame.Set(x, pl.TitleBar.Y, renderer.Cell{Rune: ' ', Style: style}) | 156 | frame.Set(x, pl.TitleBar.Y, renderer.Cell{Rune: ' ', Style: style}) | ||
M internal/usecase/daemon/render_test.go +4 -3
| @@ -46,7 +46,7 @@ func TestPaletteBackdropDimsSimultaneousCopyMode(t *testing.T) { | |||
| 46 | 46 | mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput)) | |
| 47 | 47 | dimmed := client.Frame.At(0, 1) | |
| 48 | 48 | require.Equal(t, undimmed.Rune, dimmed.Rune, "palette backdrop must preserve copy content") | |
| 49 | - | require.Equal(t, themeui.DimStyle(undimmed.Style, theme), dimmed.Style, "palette backdrop must dim the composed copy frame") | |
| 49 | + | require.Equal(t, themeui.NewDimmer(theme).Dim(undimmed.Style), dimmed.Style, "palette backdrop must dim the composed copy frame") | |
| 50 | 50 | require.Equal(t, copyBar, client.Frame.Row(client.Frame.Height-1), "copy status bar must remain crisp") | |
| 51 | 51 | paletteVisible := false | |
| 52 | 52 | for y := range client.Frame.Height { | |
| @@ -110,7 +110,7 @@ func TestPaletteBackdropKeepsSimultaneousPickerCrisp(t *testing.T) { | |||
| 110 | 110 | d.enterPalette(sess, ac) | |
| 111 | 111 | mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput)) | |
| 112 | 112 | require.Equal(t, pickerTitle, client.Frame.At(36, 2), "picker composed with palette must remain crisp") | |
| 113 | - | require.Equal(t, themeui.DimStyle(undimmedPane.Style, backdropTheme()), client.Frame.At(0, 1).Style, "pane content outside overlays must use the theme dim style") | |
| 113 | + | require.Equal(t, themeui.NewDimmer(backdropTheme()).Dim(undimmedPane.Style), client.Frame.At(0, 1).Style, "pane content outside overlays must use the theme dim style") | |
| 114 | 114 | } | |
| 115 | 115 | ||
| 116 | 116 | func TestPaletteBackdropProductionRenderAndDismissal(t *testing.T) { | |
| @@ -131,7 +131,7 @@ func TestPaletteBackdropProductionRenderAndDismissal(t *testing.T) { | |||
| 131 | 131 | mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput)) | |
| 132 | 132 | dimmed := client.Frame.At(0, 1) | |
| 133 | 133 | require.Equal(t, 'X', dimmed.Rune) | |
| 134 | - | require.Equal(t, themeui.DimStyle(undimmed.Style, backdropTheme()), dimmed.Style, "open palette must use the theme dim style") | |
| 134 | + | require.Equal(t, themeui.NewDimmer(backdropTheme()).Dim(undimmed.Style), dimmed.Style, "open palette must use the theme dim style") | |
| 135 | 135 | require.Equal(t, topBar, client.Frame.At(0, 0), "top chrome remains crisp") | |
| 136 | 136 | require.Equal(t, bottomBar, client.Frame.At(0, 24), "bottom chrome remains crisp") | |
| 137 | 137 | ||
| @@ -825,6 +825,7 @@ func TestComposeTabFrameTwoPaneSplitBlitsDividersDimsAndTranslatesDamage(t *test | |||
| 825 | 825 | require.Equal(t, '│', frame.At(20, 0).Rune) | |
| 826 | 826 | require.Equal(t, 'R', frame.At(21, 0).Rune) | |
| 827 | 827 | require.True(t, frame.At(0, 0).Style.HasForegroundRGB, "unfocused left pane should be dimmed during blit") | |
| 828 | + | require.Equal(t, renderer.RGB{R: 96, G: 96, B: 96}, frame.At(0, 0).Style.ForegroundRGB, "inactive panes should use stronger foreground dimming") | |
| 828 | 829 | require.False(t, left.screen.Frame.At(0, 0).Style.HasForegroundRGB, "dimming must not mutate vt.Screen") | |
| 829 | 830 | require.Contains(t, damage, renderer.Damage{Kind: renderer.DamageText, X: 22, Y: 0, Width: 1, Height: 1, Count: 1}) | |
| 830 | 831 | } | |
| @@ -46,7 +46,7 @@ func TestPaletteBackdropDimsSimultaneousCopyMode(t *testing.T) { | |||||
| 46 | mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput)) | 46 | mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput)) | ||
| 47 | dimmed := client.Frame.At(0, 1) | 47 | dimmed := client.Frame.At(0, 1) | ||
| 48 | require.Equal(t, undimmed.Rune, dimmed.Rune, "palette backdrop must preserve copy content") | 48 | require.Equal(t, undimmed.Rune, dimmed.Rune, "palette backdrop must preserve copy content") | ||
| 49 | - | require.Equal(t, themeui.DimStyle(undimmed.Style, theme), dimmed.Style, "palette backdrop must dim the composed copy frame") | 49 | + | require.Equal(t, themeui.NewDimmer(theme).Dim(undimmed.Style), dimmed.Style, "palette backdrop must dim the composed copy frame") |
| 50 | require.Equal(t, copyBar, client.Frame.Row(client.Frame.Height-1), "copy status bar must remain crisp") | 50 | require.Equal(t, copyBar, client.Frame.Row(client.Frame.Height-1), "copy status bar must remain crisp") | ||
| 51 | paletteVisible := false | 51 | paletteVisible := false | ||
| 52 | for y := range client.Frame.Height { | 52 | for y := range client.Frame.Height { | ||
| @@ -110,7 +110,7 @@ func TestPaletteBackdropKeepsSimultaneousPickerCrisp(t *testing.T) { | |||||
| 110 | d.enterPalette(sess, ac) | 110 | d.enterPalette(sess, ac) | ||
| 111 | mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput)) | 111 | mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput)) | ||
| 112 | require.Equal(t, pickerTitle, client.Frame.At(36, 2), "picker composed with palette must remain crisp") | 112 | require.Equal(t, pickerTitle, client.Frame.At(36, 2), "picker composed with palette must remain crisp") | ||
| 113 | - | require.Equal(t, themeui.DimStyle(undimmedPane.Style, backdropTheme()), client.Frame.At(0, 1).Style, "pane content outside overlays must use the theme dim style") | 113 | + | require.Equal(t, themeui.NewDimmer(backdropTheme()).Dim(undimmedPane.Style), client.Frame.At(0, 1).Style, "pane content outside overlays must use the theme dim style") |
| 114 | } | 114 | } | ||
| 115 | 115 | ||||
| 116 | func TestPaletteBackdropProductionRenderAndDismissal(t *testing.T) { | 116 | func TestPaletteBackdropProductionRenderAndDismissal(t *testing.T) { | ||
| @@ -131,7 +131,7 @@ func TestPaletteBackdropProductionRenderAndDismissal(t *testing.T) { | |||||
| 131 | mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput)) | 131 | mustApplyOutput(t, client, awaitFrame(t, sends, ports.MsgOutput)) | ||
| 132 | dimmed := client.Frame.At(0, 1) | 132 | dimmed := client.Frame.At(0, 1) | ||
| 133 | require.Equal(t, 'X', dimmed.Rune) | 133 | require.Equal(t, 'X', dimmed.Rune) | ||
| 134 | - | require.Equal(t, themeui.DimStyle(undimmed.Style, backdropTheme()), dimmed.Style, "open palette must use the theme dim style") | 134 | + | require.Equal(t, themeui.NewDimmer(backdropTheme()).Dim(undimmed.Style), dimmed.Style, "open palette must use the theme dim style") |
| 135 | require.Equal(t, topBar, client.Frame.At(0, 0), "top chrome remains crisp") | 135 | require.Equal(t, topBar, client.Frame.At(0, 0), "top chrome remains crisp") | ||
| 136 | require.Equal(t, bottomBar, client.Frame.At(0, 24), "bottom chrome remains crisp") | 136 | require.Equal(t, bottomBar, client.Frame.At(0, 24), "bottom chrome remains crisp") | ||
| 137 | 137 | ||||
| @@ -825,6 +825,7 @@ func TestComposeTabFrameTwoPaneSplitBlitsDividersDimsAndTranslatesDamage(t *test | |||||
| 825 | require.Equal(t, '│', frame.At(20, 0).Rune) | 825 | require.Equal(t, '│', frame.At(20, 0).Rune) | ||
| 826 | require.Equal(t, 'R', frame.At(21, 0).Rune) | 826 | require.Equal(t, 'R', frame.At(21, 0).Rune) | ||
| 827 | require.True(t, frame.At(0, 0).Style.HasForegroundRGB, "unfocused left pane should be dimmed during blit") | 827 | require.True(t, frame.At(0, 0).Style.HasForegroundRGB, "unfocused left pane should be dimmed during blit") | ||
| 828 | + | require.Equal(t, renderer.RGB{R: 96, G: 96, B: 96}, frame.At(0, 0).Style.ForegroundRGB, "inactive panes should use stronger foreground dimming") | |||
| 828 | require.False(t, left.screen.Frame.At(0, 0).Style.HasForegroundRGB, "dimming must not mutate vt.Screen") | 829 | require.False(t, left.screen.Frame.At(0, 0).Style.HasForegroundRGB, "dimming must not mutate vt.Screen") | ||
| 829 | require.Contains(t, damage, renderer.Damage{Kind: renderer.DamageText, X: 22, Y: 0, Width: 1, Height: 1, Count: 1}) | 830 | require.Contains(t, damage, renderer.Damage{Kind: renderer.DamageText, X: 22, Y: 0, Width: 1, Height: 1, Count: 1}) | ||
| 830 | } | 831 | } | ||
M internal/usecase/theme/theme.go +117 -14
| @@ -373,28 +373,131 @@ func MutedVariantStyle(base renderer.Style, t Theme) renderer.Style { | |||
| 373 | 373 | return out | |
| 374 | 374 | } | |
| 375 | 375 | ||
| 376 | - | func DimStyle(style renderer.Style, t Theme) renderer.Style { | |
| 377 | - | if !usable(t) { | |
| 376 | + | const defaultDimmingPercent = 35 | |
| 377 | + | ||
| 378 | + | // Dimmer transforms resolved terminal colors for subdued UI states. Construct | |
| 379 | + | // it once and reuse it while rendering cells with the same theme and policy. | |
| 380 | + | type Dimmer struct { | |
| 381 | + | theme Theme | |
| 382 | + | backgroundPercent int | |
| 383 | + | foregroundPercent int | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | type dimmingTarget uint8 | |
| 387 | + | ||
| 388 | + | const ( | |
| 389 | + | dimBackground dimmingTarget = iota | |
| 390 | + | dimForeground | |
| 391 | + | ) | |
| 392 | + | ||
| 393 | + | // DimmerOption customizes a Dimmer without allocating a closure. | |
| 394 | + | type DimmerOption struct { | |
| 395 | + | target dimmingTarget | |
| 396 | + | percent int | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | // WithBackgroundDimming overrides the default 35 percent background fade. | |
| 400 | + | func WithBackgroundDimming(percent int) DimmerOption { | |
| 401 | + | return DimmerOption{target: dimBackground, percent: percent} | |
| 402 | + | } | |
| 403 | + | ||
| 404 | + | // WithForegroundDimming overrides the default 35 percent foreground fade. | |
| 405 | + | func WithForegroundDimming(percent int) DimmerOption { | |
| 406 | + | return DimmerOption{target: dimForeground, percent: percent} | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | // NewDimmer returns a reusable dimmer with optional per-channel overrides. | |
| 410 | + | func NewDimmer(t Theme, opts ...DimmerOption) Dimmer { | |
| 411 | + | d := Dimmer{ | |
| 412 | + | theme: t, | |
| 413 | + | backgroundPercent: defaultDimmingPercent, | |
| 414 | + | foregroundPercent: defaultDimmingPercent, | |
| 415 | + | } | |
| 416 | + | for _, opt := range opts { | |
| 417 | + | switch opt.target { | |
| 418 | + | case dimBackground: | |
| 419 | + | d.backgroundPercent = opt.percent | |
| 420 | + | case dimForeground: | |
| 421 | + | d.foregroundPercent = opt.percent | |
| 422 | + | } | |
| 423 | + | } | |
| 424 | + | d.backgroundPercent = clampPercent(d.backgroundPercent) | |
| 425 | + | d.foregroundPercent = clampPercent(d.foregroundPercent) | |
| 426 | + | return d | |
| 427 | + | } | |
| 428 | + | ||
| 429 | + | // Dim fades a style's resolved background toward the terminal background, | |
| 430 | + | // then fades its foreground and custom underline toward that dimmed background. | |
| 431 | + | func (d Dimmer) Dim(style renderer.Style) renderer.Style { | |
| 432 | + | if !usable(d.theme) { | |
| 378 | 433 | return style | |
| 379 | 434 | } | |
| 435 | + | ||
| 436 | + | foreground := resolveForeground(style, d.theme) | |
| 437 | + | background := resolveBackground(style, d.theme) | |
| 438 | + | if style.Inverse { | |
| 439 | + | foreground, background = background, foreground | |
| 440 | + | } | |
| 441 | + | background = blendPercent(background, d.theme.Background, d.backgroundPercent) | |
| 442 | + | ||
| 380 | 443 | out := style | |
| 444 | + | out.Inverse = false | |
| 381 | 445 | out.HasForegroundRGB = true | |
| 446 | + | out.ForegroundRGB = blendPercent(foreground, background, d.foregroundPercent) | |
| 447 | + | out.HasBackgroundRGB = true | |
| 448 | + | out.BackgroundRGB = background | |
| 449 | + | if style.HasUnderlineColorRGB || style.HasUnderlineColor { | |
| 450 | + | underline := style.UnderlineColorRGB | |
| 451 | + | if !style.HasUnderlineColorRGB { | |
| 452 | + | underline = xterm256Color(style.UnderlineColor) | |
| 453 | + | } | |
| 454 | + | out.HasUnderlineColor = false | |
| 455 | + | out.HasUnderlineColorRGB = true | |
| 456 | + | out.UnderlineColorRGB = blendPercent(underline, background, d.foregroundPercent) | |
| 457 | + | } | |
| 458 | + | return out | |
| 459 | + | } | |
| 460 | + | ||
| 461 | + | func resolveForeground(style renderer.Style, t Theme) renderer.RGB { | |
| 382 | 462 | if style.HasForegroundRGB { | |
| 383 | - | out.ForegroundRGB = Blend(style.ForegroundRGB, t.Background, 0.35) | |
| 384 | - | } else if style.Foreground >= 0 { | |
| 385 | - | out.ForegroundRGB = Blend(xterm256Color(style.Foreground), t.Background, 0.35) | |
| 386 | - | } else { | |
| 387 | - | out.ForegroundRGB = Blend(t.Foreground, t.Background, 0.35) | |
| 463 | + | return style.ForegroundRGB | |
| 388 | 464 | } | |
| 389 | - | out.HasBackgroundRGB = true | |
| 465 | + | if style.Foreground >= 0 { | |
| 466 | + | return xterm256Color(style.Foreground) | |
| 467 | + | } | |
| 468 | + | return t.Foreground | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | func resolveBackground(style renderer.Style, t Theme) renderer.RGB { | |
| 390 | 472 | if style.HasBackgroundRGB { | |
| 391 | - | out.BackgroundRGB = Blend(style.BackgroundRGB, t.Background, 0.35) | |
| 392 | - | } else if style.Background >= 0 { | |
| 393 | - | out.BackgroundRGB = Blend(xterm256Color(style.Background), t.Background, 0.35) | |
| 394 | - | } else { | |
| 395 | - | out.BackgroundRGB = Blend(t.Background, t.Background, 0.35) | |
| 473 | + | return style.BackgroundRGB | |
| 396 | 474 | } | |
| 397 | - | return out | |
| 475 | + | if style.Background >= 0 { | |
| 476 | + | return xterm256Color(style.Background) | |
| 477 | + | } | |
| 478 | + | return t.Background | |
| 479 | + | } | |
| 480 | + | ||
| 481 | + | func blendPercent(a, b renderer.RGB, percent int) renderer.RGB { | |
| 482 | + | return renderer.RGB{ | |
| 483 | + | R: blendBytePercent(a.R, b.R, percent), | |
| 484 | + | G: blendBytePercent(a.G, b.G, percent), | |
| 485 | + | B: blendBytePercent(a.B, b.B, percent), | |
| 486 | + | } | |
| 487 | + | } | |
| 488 | + | ||
| 489 | + | func blendBytePercent(a, b uint8, percent int) uint8 { | |
| 490 | + | return uint8((int(a)*(100-percent) + int(b)*percent + 50) / 100) | |
| 491 | + | } | |
| 492 | + | ||
| 493 | + | func clampPercent(percent int) int { | |
| 494 | + | if percent < 0 { | |
| 495 | + | return 0 | |
| 496 | + | } | |
| 497 | + | if percent > 100 { | |
| 498 | + | return 100 | |
| 499 | + | } | |
| 500 | + | return percent | |
| 398 | 501 | } | |
| 399 | 502 | ||
| 400 | 503 | var ( | |
| @@ -373,28 +373,131 @@ func MutedVariantStyle(base renderer.Style, t Theme) renderer.Style { | |||||
| 373 | return out | 373 | return out | ||
| 374 | } | 374 | } | ||
| 375 | 375 | ||||
| 376 | - | func DimStyle(style renderer.Style, t Theme) renderer.Style { | 376 | + | const defaultDimmingPercent = 35 |
| 377 | - | if !usable(t) { | 377 | + | |
| 378 | + | // Dimmer transforms resolved terminal colors for subdued UI states. Construct | |||
| 379 | + | // it once and reuse it while rendering cells with the same theme and policy. | |||
| 380 | + | type Dimmer struct { | |||
| 381 | + | theme Theme | |||
| 382 | + | backgroundPercent int | |||
| 383 | + | foregroundPercent int | |||
| 384 | + | } | |||
| 385 | + | ||||
| 386 | + | type dimmingTarget uint8 | |||
| 387 | + | ||||
| 388 | + | const ( | |||
| 389 | + | dimBackground dimmingTarget = iota | |||
| 390 | + | dimForeground | |||
| 391 | + | ) | |||
| 392 | + | ||||
| 393 | + | // DimmerOption customizes a Dimmer without allocating a closure. | |||
| 394 | + | type DimmerOption struct { | |||
| 395 | + | target dimmingTarget | |||
| 396 | + | percent int | |||
| 397 | + | } | |||
| 398 | + | ||||
| 399 | + | // WithBackgroundDimming overrides the default 35 percent background fade. | |||
| 400 | + | func WithBackgroundDimming(percent int) DimmerOption { | |||
| 401 | + | return DimmerOption{target: dimBackground, percent: percent} | |||
| 402 | + | } | |||
| 403 | + | ||||
| 404 | + | // WithForegroundDimming overrides the default 35 percent foreground fade. | |||
| 405 | + | func WithForegroundDimming(percent int) DimmerOption { | |||
| 406 | + | return DimmerOption{target: dimForeground, percent: percent} | |||
| 407 | + | } | |||
| 408 | + | ||||
| 409 | + | // NewDimmer returns a reusable dimmer with optional per-channel overrides. | |||
| 410 | + | func NewDimmer(t Theme, opts ...DimmerOption) Dimmer { | |||
| 411 | + | d := Dimmer{ | |||
| 412 | + | theme: t, | |||
| 413 | + | backgroundPercent: defaultDimmingPercent, | |||
| 414 | + | foregroundPercent: defaultDimmingPercent, | |||
| 415 | + | } | |||
| 416 | + | for _, opt := range opts { | |||
| 417 | + | switch opt.target { | |||
| 418 | + | case dimBackground: | |||
| 419 | + | d.backgroundPercent = opt.percent | |||
| 420 | + | case dimForeground: | |||
| 421 | + | d.foregroundPercent = opt.percent | |||
| 422 | + | } | |||
| 423 | + | } | |||
| 424 | + | d.backgroundPercent = clampPercent(d.backgroundPercent) | |||
| 425 | + | d.foregroundPercent = clampPercent(d.foregroundPercent) | |||
| 426 | + | return d | |||
| 427 | + | } | |||
| 428 | + | ||||
| 429 | + | // Dim fades a style's resolved background toward the terminal background, | |||
| 430 | + | // then fades its foreground and custom underline toward that dimmed background. | |||
| 431 | + | func (d Dimmer) Dim(style renderer.Style) renderer.Style { | |||
| 432 | + | if !usable(d.theme) { | |||
| 378 | return style | 433 | return style | ||
| 379 | } | 434 | } | ||
| 435 | + | ||||
| 436 | + | foreground := resolveForeground(style, d.theme) | |||
| 437 | + | background := resolveBackground(style, d.theme) | |||
| 438 | + | if style.Inverse { | |||
| 439 | + | foreground, background = background, foreground | |||
| 440 | + | } | |||
| 441 | + | background = blendPercent(background, d.theme.Background, d.backgroundPercent) | |||
| 442 | + | ||||
| 380 | out := style | 443 | out := style | ||
| 444 | + | out.Inverse = false | |||
| 381 | out.HasForegroundRGB = true | 445 | out.HasForegroundRGB = true | ||
| 446 | + | out.ForegroundRGB = blendPercent(foreground, background, d.foregroundPercent) | |||
| 447 | + | out.HasBackgroundRGB = true | |||
| 448 | + | out.BackgroundRGB = background | |||
| 449 | + | if style.HasUnderlineColorRGB || style.HasUnderlineColor { | |||
| 450 | + | underline := style.UnderlineColorRGB | |||
| 451 | + | if !style.HasUnderlineColorRGB { | |||
| 452 | + | underline = xterm256Color(style.UnderlineColor) | |||
| 453 | + | } | |||
| 454 | + | out.HasUnderlineColor = false | |||
| 455 | + | out.HasUnderlineColorRGB = true | |||
| 456 | + | out.UnderlineColorRGB = blendPercent(underline, background, d.foregroundPercent) | |||
| 457 | + | } | |||
| 458 | + | return out | |||
| 459 | + | } | |||
| 460 | + | ||||
| 461 | + | func resolveForeground(style renderer.Style, t Theme) renderer.RGB { | |||
| 382 | if style.HasForegroundRGB { | 462 | if style.HasForegroundRGB { | ||
| 383 | - | out.ForegroundRGB = Blend(style.ForegroundRGB, t.Background, 0.35) | 463 | + | return style.ForegroundRGB |
| 384 | - | } else if style.Foreground >= 0 { | |||
| 385 | - | out.ForegroundRGB = Blend(xterm256Color(style.Foreground), t.Background, 0.35) | |||
| 386 | - | } else { | |||
| 387 | - | out.ForegroundRGB = Blend(t.Foreground, t.Background, 0.35) | |||
| 388 | } | 464 | } | ||
| 389 | - | out.HasBackgroundRGB = true | 465 | + | if style.Foreground >= 0 { |
| 466 | + | return xterm256Color(style.Foreground) | |||
| 467 | + | } | |||
| 468 | + | return t.Foreground | |||
| 469 | + | } | |||
| 470 | + | ||||
| 471 | + | func resolveBackground(style renderer.Style, t Theme) renderer.RGB { | |||
| 390 | if style.HasBackgroundRGB { | 472 | if style.HasBackgroundRGB { | ||
| 391 | - | out.BackgroundRGB = Blend(style.BackgroundRGB, t.Background, 0.35) | 473 | + | return style.BackgroundRGB |
| 392 | - | } else if style.Background >= 0 { | |||
| 393 | - | out.BackgroundRGB = Blend(xterm256Color(style.Background), t.Background, 0.35) | |||
| 394 | - | } else { | |||
| 395 | - | out.BackgroundRGB = Blend(t.Background, t.Background, 0.35) | |||
| 396 | } | 474 | } | ||
| 397 | - | return out | 475 | + | if style.Background >= 0 { |
| 476 | + | return xterm256Color(style.Background) | |||
| 477 | + | } | |||
| 478 | + | return t.Background | |||
| 479 | + | } | |||
| 480 | + | ||||
| 481 | + | func blendPercent(a, b renderer.RGB, percent int) renderer.RGB { | |||
| 482 | + | return renderer.RGB{ | |||
| 483 | + | R: blendBytePercent(a.R, b.R, percent), | |||
| 484 | + | G: blendBytePercent(a.G, b.G, percent), | |||
| 485 | + | B: blendBytePercent(a.B, b.B, percent), | |||
| 486 | + | } | |||
| 487 | + | } | |||
| 488 | + | ||||
| 489 | + | func blendBytePercent(a, b uint8, percent int) uint8 { | |||
| 490 | + | return uint8((int(a)*(100-percent) + int(b)*percent + 50) / 100) | |||
| 491 | + | } | |||
| 492 | + | ||||
| 493 | + | func clampPercent(percent int) int { | |||
| 494 | + | if percent < 0 { | |||
| 495 | + | return 0 | |||
| 496 | + | } | |||
| 497 | + | if percent > 100 { | |||
| 498 | + | return 100 | |||
| 499 | + | } | |||
| 500 | + | return percent | |||
| 398 | } | 501 | } | ||
| 399 | 502 | ||||
| 400 | var ( | 503 | var ( | ||
M internal/usecase/theme/theme_test.go +84 -7
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "testing" | |
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/vev/pkg/renderer" | |
| 9 | + | "github.com/stretchr/testify/require" | |
| 9 | 10 | ) | |
| 10 | 11 | ||
| 11 | 12 | func TestParseXColor(t *testing.T) { | |
| @@ -261,7 +262,83 @@ func TestScannerFlushesOverflowingPartialQueue(t *testing.T) { | |||
| 261 | 262 | } | |
| 262 | 263 | } | |
| 263 | 264 | ||
| 264 | - | func TestDimStyle(t *testing.T) { | |
| 265 | + | func TestDimmer(t *testing.T) { | |
| 266 | + | theme := Theme{ | |
| 267 | + | Foreground: renderer.RGB{R: 240, G: 240, B: 240}, | |
| 268 | + | Background: renderer.RGB{R: 10, G: 20, B: 30}, | |
| 269 | + | HasFG: true, HasBG: true, Known: true, TrueColor: true, | |
| 270 | + | } | |
| 271 | + | dimmer := NewDimmer(theme, WithForegroundDimming(55)) | |
| 272 | + | ||
| 273 | + | t.Run("fades foreground toward the faded cell background", func(t *testing.T) { | |
| 274 | + | style := renderer.Style{ | |
| 275 | + | Foreground: -1, Background: -1, | |
| 276 | + | HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 250, G: 100, B: 50}, | |
| 277 | + | HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 50, G: 150, B: 250}, | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | got := dimmer.Dim(style) | |
| 281 | + | ||
| 282 | + | require.Equal(t, renderer.RGB{R: 36, G: 105, B: 173}, got.BackgroundRGB) | |
| 283 | + | require.Equal(t, renderer.RGB{R: 132, G: 103, B: 118}, got.ForegroundRGB) | |
| 284 | + | }) | |
| 285 | + | ||
| 286 | + | t.Run("resolves inverse and custom underline colors before fading", func(t *testing.T) { | |
| 287 | + | style := renderer.Style{ | |
| 288 | + | Bold: true, Italic: true, Inverse: true, Attrs: renderer.AttrUnderline, | |
| 289 | + | Foreground: -1, Background: -1, | |
| 290 | + | HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 250, G: 100, B: 50}, | |
| 291 | + | HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 50, G: 150, B: 250}, | |
| 292 | + | HasUnderlineColorRGB: true, UnderlineColorRGB: renderer.RGB{R: 200, G: 10, B: 100}, | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | got := dimmer.Dim(style) | |
| 296 | + | ||
| 297 | + | require.False(t, got.Inverse) | |
| 298 | + | require.Equal(t, renderer.RGB{R: 166, G: 72, B: 43}, got.BackgroundRGB) | |
| 299 | + | require.Equal(t, renderer.RGB{R: 114, G: 107, B: 136}, got.ForegroundRGB) | |
| 300 | + | require.Equal(t, renderer.RGB{R: 181, G: 44, B: 69}, got.UnderlineColorRGB) | |
| 301 | + | require.True(t, got.Bold) | |
| 302 | + | require.True(t, got.Italic) | |
| 303 | + | require.Equal(t, renderer.AttrUnderline, got.Attrs) | |
| 304 | + | }) | |
| 305 | + | ||
| 306 | + | t.Run("resolves indexed and default colors", func(t *testing.T) { | |
| 307 | + | style := renderer.Style{ | |
| 308 | + | Foreground: 196, | |
| 309 | + | Background: -1, | |
| 310 | + | HasUnderlineColor: true, | |
| 311 | + | UnderlineColor: 46, | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | got := dimmer.Dim(style) | |
| 315 | + | ||
| 316 | + | require.Equal(t, renderer.RGB{R: 10, G: 20, B: 30}, got.BackgroundRGB) | |
| 317 | + | require.Equal(t, renderer.RGB{R: 120, G: 11, B: 17}, got.ForegroundRGB) | |
| 318 | + | require.True(t, got.HasUnderlineColorRGB) | |
| 319 | + | require.Equal(t, renderer.RGB{R: 6, G: 126, B: 17}, got.UnderlineColorRGB) | |
| 320 | + | }) | |
| 321 | + | ||
| 322 | + | t.Run("clamps percentages", func(t *testing.T) { | |
| 323 | + | style := renderer.Style{ | |
| 324 | + | Foreground: -1, Background: -1, | |
| 325 | + | HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 250, G: 100, B: 50}, | |
| 326 | + | HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 50, G: 150, B: 250}, | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | got := NewDimmer(theme, WithBackgroundDimming(-1), WithForegroundDimming(101)).Dim(style) | |
| 330 | + | ||
| 331 | + | require.Equal(t, style.BackgroundRGB, got.BackgroundRGB) | |
| 332 | + | require.Equal(t, style.BackgroundRGB, got.ForegroundRGB) | |
| 333 | + | }) | |
| 334 | + | ||
| 335 | + | t.Run("leaves style unchanged without a usable truecolor theme", func(t *testing.T) { | |
| 336 | + | style := renderer.Style{Foreground: 196, Background: 21, Inverse: true} | |
| 337 | + | require.Equal(t, style, NewDimmer(Theme{}, WithForegroundDimming(55)).Dim(style)) | |
| 338 | + | }) | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | func TestDimmerDefaults(t *testing.T) { | |
| 265 | 342 | theme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | |
| 266 | 343 | tests := []struct { | |
| 267 | 344 | name string | |
| @@ -311,9 +388,9 @@ func TestDimStyle(t *testing.T) { | |||
| 311 | 388 | Foreground: -1, | |
| 312 | 389 | Background: 21, | |
| 313 | 390 | HasForegroundRGB: true, | |
| 314 | - | ForegroundRGB: Blend(theme.Foreground, theme.Background, 0.35), | |
| 391 | + | ForegroundRGB: renderer.RGB{R: 131, G: 132, B: 192}, | |
| 315 | 392 | HasBackgroundRGB: true, | |
| 316 | - | BackgroundRGB: Blend(renderer.RGB{R: 0, G: 0, B: 255}, theme.Background, 0.35), | |
| 393 | + | BackgroundRGB: renderer.RGB{R: 4, G: 7, B: 176}, | |
| 317 | 394 | }, | |
| 318 | 395 | }, | |
| 319 | 396 | { | |
| @@ -330,18 +407,18 @@ func TestDimStyle(t *testing.T) { | |||
| 330 | 407 | Foreground: -1, | |
| 331 | 408 | Background: -1, | |
| 332 | 409 | HasForegroundRGB: true, | |
| 333 | - | ForegroundRGB: Blend(renderer.RGB{R: 100, G: 50, B: 25}, theme.Background, 0.35), | |
| 410 | + | ForegroundRGB: renderer.RGB{R: 71, G: 58, B: 61}, | |
| 334 | 411 | HasBackgroundRGB: true, | |
| 335 | - | BackgroundRGB: Blend(renderer.RGB{R: 20, G: 100, B: 180}, theme.Background, 0.35), | |
| 412 | + | BackgroundRGB: renderer.RGB{R: 17, G: 72, B: 128}, | |
| 336 | 413 | }, | |
| 337 | 414 | }, | |
| 338 | 415 | } | |
| 339 | 416 | ||
| 340 | 417 | for _, tt := range tests { | |
| 341 | 418 | t.Run(tt.name, func(t *testing.T) { | |
| 342 | - | got := DimStyle(tt.in, theme) | |
| 419 | + | got := NewDimmer(theme).Dim(tt.in) | |
| 343 | 420 | if !got.Equal(tt.want) { | |
| 344 | - | t.Fatalf("DimStyle()=%+v want %+v", got, tt.want) | |
| 421 | + | t.Fatalf("Dimmer.Dim()=%+v want %+v", got, tt.want) | |
| 345 | 422 | } | |
| 346 | 423 | }) | |
| 347 | 424 | } | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "testing" | 6 | "testing" | ||
| 7 | 7 | ||||
| 8 | "github.com/bnema/vev/pkg/renderer" | 8 | "github.com/bnema/vev/pkg/renderer" | ||
| 9 | + | "github.com/stretchr/testify/require" | |||
| 9 | ) | 10 | ) | ||
| 10 | 11 | ||||
| 11 | func TestParseXColor(t *testing.T) { | 12 | func TestParseXColor(t *testing.T) { | ||
| @@ -261,7 +262,83 @@ func TestScannerFlushesOverflowingPartialQueue(t *testing.T) { | |||||
| 261 | } | 262 | } | ||
| 262 | } | 263 | } | ||
| 263 | 264 | ||||
| 264 | - | func TestDimStyle(t *testing.T) { | 265 | + | func TestDimmer(t *testing.T) { |
| 266 | + | theme := Theme{ | |||
| 267 | + | Foreground: renderer.RGB{R: 240, G: 240, B: 240}, | |||
| 268 | + | Background: renderer.RGB{R: 10, G: 20, B: 30}, | |||
| 269 | + | HasFG: true, HasBG: true, Known: true, TrueColor: true, | |||
| 270 | + | } | |||
| 271 | + | dimmer := NewDimmer(theme, WithForegroundDimming(55)) | |||
| 272 | + | ||||
| 273 | + | t.Run("fades foreground toward the faded cell background", func(t *testing.T) { | |||
| 274 | + | style := renderer.Style{ | |||
| 275 | + | Foreground: -1, Background: -1, | |||
| 276 | + | HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 250, G: 100, B: 50}, | |||
| 277 | + | HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 50, G: 150, B: 250}, | |||
| 278 | + | } | |||
| 279 | + | ||||
| 280 | + | got := dimmer.Dim(style) | |||
| 281 | + | ||||
| 282 | + | require.Equal(t, renderer.RGB{R: 36, G: 105, B: 173}, got.BackgroundRGB) | |||
| 283 | + | require.Equal(t, renderer.RGB{R: 132, G: 103, B: 118}, got.ForegroundRGB) | |||
| 284 | + | }) | |||
| 285 | + | ||||
| 286 | + | t.Run("resolves inverse and custom underline colors before fading", func(t *testing.T) { | |||
| 287 | + | style := renderer.Style{ | |||
| 288 | + | Bold: true, Italic: true, Inverse: true, Attrs: renderer.AttrUnderline, | |||
| 289 | + | Foreground: -1, Background: -1, | |||
| 290 | + | HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 250, G: 100, B: 50}, | |||
| 291 | + | HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 50, G: 150, B: 250}, | |||
| 292 | + | HasUnderlineColorRGB: true, UnderlineColorRGB: renderer.RGB{R: 200, G: 10, B: 100}, | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | got := dimmer.Dim(style) | |||
| 296 | + | ||||
| 297 | + | require.False(t, got.Inverse) | |||
| 298 | + | require.Equal(t, renderer.RGB{R: 166, G: 72, B: 43}, got.BackgroundRGB) | |||
| 299 | + | require.Equal(t, renderer.RGB{R: 114, G: 107, B: 136}, got.ForegroundRGB) | |||
| 300 | + | require.Equal(t, renderer.RGB{R: 181, G: 44, B: 69}, got.UnderlineColorRGB) | |||
| 301 | + | require.True(t, got.Bold) | |||
| 302 | + | require.True(t, got.Italic) | |||
| 303 | + | require.Equal(t, renderer.AttrUnderline, got.Attrs) | |||
| 304 | + | }) | |||
| 305 | + | ||||
| 306 | + | t.Run("resolves indexed and default colors", func(t *testing.T) { | |||
| 307 | + | style := renderer.Style{ | |||
| 308 | + | Foreground: 196, | |||
| 309 | + | Background: -1, | |||
| 310 | + | HasUnderlineColor: true, | |||
| 311 | + | UnderlineColor: 46, | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | got := dimmer.Dim(style) | |||
| 315 | + | ||||
| 316 | + | require.Equal(t, renderer.RGB{R: 10, G: 20, B: 30}, got.BackgroundRGB) | |||
| 317 | + | require.Equal(t, renderer.RGB{R: 120, G: 11, B: 17}, got.ForegroundRGB) | |||
| 318 | + | require.True(t, got.HasUnderlineColorRGB) | |||
| 319 | + | require.Equal(t, renderer.RGB{R: 6, G: 126, B: 17}, got.UnderlineColorRGB) | |||
| 320 | + | }) | |||
| 321 | + | ||||
| 322 | + | t.Run("clamps percentages", func(t *testing.T) { | |||
| 323 | + | style := renderer.Style{ | |||
| 324 | + | Foreground: -1, Background: -1, | |||
| 325 | + | HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 250, G: 100, B: 50}, | |||
| 326 | + | HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 50, G: 150, B: 250}, | |||
| 327 | + | } | |||
| 328 | + | ||||
| 329 | + | got := NewDimmer(theme, WithBackgroundDimming(-1), WithForegroundDimming(101)).Dim(style) | |||
| 330 | + | ||||
| 331 | + | require.Equal(t, style.BackgroundRGB, got.BackgroundRGB) | |||
| 332 | + | require.Equal(t, style.BackgroundRGB, got.ForegroundRGB) | |||
| 333 | + | }) | |||
| 334 | + | ||||
| 335 | + | t.Run("leaves style unchanged without a usable truecolor theme", func(t *testing.T) { | |||
| 336 | + | style := renderer.Style{Foreground: 196, Background: 21, Inverse: true} | |||
| 337 | + | require.Equal(t, style, NewDimmer(Theme{}, WithForegroundDimming(55)).Dim(style)) | |||
| 338 | + | }) | |||
| 339 | + | } | |||
| 340 | + | ||||
| 341 | + | func TestDimmerDefaults(t *testing.T) { | |||
| 265 | theme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | 342 | theme := Theme{Foreground: renderer.RGB{R: 200, G: 200, B: 200}, Background: renderer.RGB{R: 10, G: 20, B: 30}, HasFG: true, HasBG: true, Known: true, TrueColor: true} | ||
| 266 | tests := []struct { | 343 | tests := []struct { | ||
| 267 | name string | 344 | name string | ||
| @@ -311,9 +388,9 @@ func TestDimStyle(t *testing.T) { | |||||
| 311 | Foreground: -1, | 388 | Foreground: -1, | ||
| 312 | Background: 21, | 389 | Background: 21, | ||
| 313 | HasForegroundRGB: true, | 390 | HasForegroundRGB: true, | ||
| 314 | - | ForegroundRGB: Blend(theme.Foreground, theme.Background, 0.35), | 391 | + | ForegroundRGB: renderer.RGB{R: 131, G: 132, B: 192}, |
| 315 | HasBackgroundRGB: true, | 392 | HasBackgroundRGB: true, | ||
| 316 | - | BackgroundRGB: Blend(renderer.RGB{R: 0, G: 0, B: 255}, theme.Background, 0.35), | 393 | + | BackgroundRGB: renderer.RGB{R: 4, G: 7, B: 176}, |
| 317 | }, | 394 | }, | ||
| 318 | }, | 395 | }, | ||
| 319 | { | 396 | { | ||
| @@ -330,18 +407,18 @@ func TestDimStyle(t *testing.T) { | |||||
| 330 | Foreground: -1, | 407 | Foreground: -1, | ||
| 331 | Background: -1, | 408 | Background: -1, | ||
| 332 | HasForegroundRGB: true, | 409 | HasForegroundRGB: true, | ||
| 333 | - | ForegroundRGB: Blend(renderer.RGB{R: 100, G: 50, B: 25}, theme.Background, 0.35), | 410 | + | ForegroundRGB: renderer.RGB{R: 71, G: 58, B: 61}, |
| 334 | HasBackgroundRGB: true, | 411 | HasBackgroundRGB: true, | ||
| 335 | - | BackgroundRGB: Blend(renderer.RGB{R: 20, G: 100, B: 180}, theme.Background, 0.35), | 412 | + | BackgroundRGB: renderer.RGB{R: 17, G: 72, B: 128}, |
| 336 | }, | 413 | }, | ||
| 337 | }, | 414 | }, | ||
| 338 | } | 415 | } | ||
| 339 | 416 | ||||
| 340 | for _, tt := range tests { | 417 | for _, tt := range tests { | ||
| 341 | t.Run(tt.name, func(t *testing.T) { | 418 | t.Run(tt.name, func(t *testing.T) { | ||
| 342 | - | got := DimStyle(tt.in, theme) | 419 | + | got := NewDimmer(theme).Dim(tt.in) |
| 343 | if !got.Equal(tt.want) { | 420 | if !got.Equal(tt.want) { | ||
| 344 | - | t.Fatalf("DimStyle()=%+v want %+v", got, tt.want) | 421 | + | t.Fatalf("Dimmer.Dim()=%+v want %+v", got, tt.want) |
| 345 | } | 422 | } | ||
| 346 | }) | 423 | }) | ||
| 347 | } | 424 | } | ||