Skip to content

Commit 40e7d6a

Browse files
coopbriclaude
andcommitted
fix(renderer): uniform opacity and background color for TUI programs
Add dedicated replace-blend GPU pipeline for background rect quads to prevent alpha accumulation with semi-transparent windows. Track bg vertices separately in the compositor, force full damage on opacity changes, preserve opacity across OSC 11 background color changes, and treat cells matching the theme background as default so TUI programs track dynamic background updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 85f0273 commit 40e7d6a

5 files changed

Lines changed: 244 additions & 55 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@omnidotdev/terminal": patch
3+
---
4+
5+
fix(renderer): uniform opacity and background color for TUI programs
6+
7+
- Add dedicated replace-blend GPU pipeline for background rects to prevent alpha accumulation when drawing semi-transparent backgrounds over a semi-transparent clear color
8+
- Track background vertices separately in the compositor so cell backgrounds render with correct opacity
9+
- Force full damage on all contexts when opacity changes via keybinding so all cells re-render
10+
- Preserve current opacity when changing background color via OSC 11
11+
- Process background state changes before cell rendering so cells use the updated default background reference
12+
- Treat cells matching the original theme background as "default" so TUI programs that set explicit backgrounds still track OSC 11 background changes

frontends/omni-terminal/src/renderer/mod.rs

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,24 @@ impl Renderer {
187187
std::mem::swap(&mut background_color, &mut foreground_color);
188188
}
189189

190-
let background_color = if self.dynamic_background.2
190+
let opacity = self.dynamic_background.1.a as f32;
191+
let matches_dynamic_bg = self.dynamic_background.2
191192
&& background_color[0] == self.dynamic_background.0[0]
192193
&& background_color[1] == self.dynamic_background.0[1]
193-
&& background_color[2] == self.dynamic_background.0[2]
194-
{
194+
&& background_color[2] == self.dynamic_background.0[2];
195+
// Also treat cells matching the original theme background as "default"
196+
// so TUI programs that set explicit bg matching the theme still track
197+
// background changes via OSC 11
198+
let matches_theme_bg = background_color[0] == self.named_colors.background.0[0]
199+
&& background_color[1] == self.named_colors.background.0[1]
200+
&& background_color[2] == self.named_colors.background.0[2];
201+
let is_default_bg = matches_dynamic_bg || matches_theme_bg;
202+
let background_color = if is_default_bg {
195203
None
204+
} else if self.dynamic_background.2 && opacity < 1.0 {
205+
let mut bg = background_color;
206+
bg[3] = opacity;
207+
Some(bg)
196208
} else {
197209
Some(background_color)
198210
};
@@ -660,14 +672,23 @@ impl Renderer {
660672
std::mem::swap(&mut background_color, &mut color);
661673
}
662674

663-
let has_dynamic_background = self.dynamic_background.2
675+
let opacity = self.dynamic_background.1.a as f32;
676+
let matches_dynamic_bg = self.dynamic_background.2
664677
&& background_color[0] == self.dynamic_background.0[0]
665678
&& background_color[1] == self.dynamic_background.0[1]
666679
&& background_color[2] == self.dynamic_background.0[2];
680+
let matches_theme_bg = background_color[0] == self.named_colors.background.0[0]
681+
&& background_color[1] == self.named_colors.background.0[1]
682+
&& background_color[2] == self.named_colors.background.0[2];
683+
let has_dynamic_background = matches_dynamic_bg || matches_theme_bg;
667684
let background_color = if has_dynamic_background
668685
&& (cursor.state.content != CursorShape::Block && is_active)
669686
{
670687
None
688+
} else if self.dynamic_background.2 && opacity < 1.0 {
689+
let mut bg = background_color;
690+
bg[3] = opacity;
691+
Some(bg)
671692
} else {
672693
Some(background_color)
673694
};
@@ -892,6 +913,45 @@ impl Renderer {
892913
self.search.rich_text_id = Some(search_rich_text);
893914
}
894915

916+
// Apply background color change BEFORE cell rendering so cells
917+
// use the updated dynamic_background for their bg color comparisons
918+
let window_update = {
919+
let current_context = context_manager.current_grid_mut().current_mut();
920+
if let Some(bg_state) = current_context.renderable_content.background.take() {
921+
use crate::context::renderable::BackgroundState;
922+
match &bg_state {
923+
BackgroundState::Set(color) => {
924+
self.dynamic_background.0 = [
925+
color.r as f32,
926+
color.g as f32,
927+
color.b as f32,
928+
color.a as f32,
929+
];
930+
let current_opacity = self.dynamic_background.1.a;
931+
let mut bg_color = *color;
932+
bg_color.a = current_opacity;
933+
self.dynamic_background.1 = bg_color;
934+
sugarloaf.set_background_color(Some(bg_color));
935+
}
936+
BackgroundState::Reset => {
937+
self.dynamic_background.0 = self.named_colors.background.0;
938+
self.dynamic_background.1 = self.named_colors.background.1;
939+
sugarloaf.set_background_color(None);
940+
}
941+
}
942+
// Force full damage so all cells re-render with the new background
943+
current_context
944+
.renderable_content
945+
.pending_update
946+
.set_ui_damage(terminal_backend::event::TerminalDamage::Full);
947+
Some(crate::context::renderable::WindowUpdate::Background(
948+
bg_state,
949+
))
950+
} else {
951+
None
952+
}
953+
};
954+
895955
let grid = context_manager.current_grid_mut();
896956
let active_key = grid.current;
897957
let mut has_active_changed = false;
@@ -1223,38 +1283,6 @@ impl Renderer {
12231283

12241284
sugarloaf.set_objects(objects);
12251285

1226-
// Apply background color from current context if changed
1227-
let current_context = context_manager.current_grid_mut().current_mut();
1228-
let window_update = if let Some(bg_state) =
1229-
current_context.renderable_content.background.take()
1230-
{
1231-
use crate::context::renderable::BackgroundState;
1232-
match bg_state {
1233-
BackgroundState::Set(color) => {
1234-
// Update dynamic_background so opacity changes use the current color
1235-
self.dynamic_background.0 = [
1236-
color.r as f32,
1237-
color.g as f32,
1238-
color.b as f32,
1239-
color.a as f32,
1240-
];
1241-
self.dynamic_background.1 = color;
1242-
sugarloaf.set_background_color(Some(color));
1243-
}
1244-
BackgroundState::Reset => {
1245-
// Restore dynamic_background to config defaults
1246-
self.dynamic_background.0 = self.named_colors.background.0;
1247-
self.dynamic_background.1 = self.named_colors.background.1;
1248-
sugarloaf.set_background_color(None);
1249-
}
1250-
}
1251-
Some(crate::context::renderable::WindowUpdate::Background(
1252-
bg_state,
1253-
))
1254-
} else {
1255-
None
1256-
};
1257-
12581286
sugarloaf.render();
12591287

12601288
// let _duration = start.elapsed();

frontends/omni-terminal/src/screen/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,17 @@ impl Screen<'_> {
574574
self.sugarloaf
575575
.set_background_color(Some(self.renderer.dynamic_background.1));
576576

577+
// Force full re-render so cell backgrounds pick up the new opacity
578+
for context_grid in self.context_manager.contexts_mut() {
579+
for context in context_grid.contexts_mut().values_mut() {
580+
context
581+
.context_mut()
582+
.renderable_content
583+
.pending_update
584+
.set_ui_damage(terminal_backend::event::TerminalDamage::Full);
585+
}
586+
}
587+
577588
self.context_manager.set_window_opacity(new_opacity);
578589
self.render();
579590
}

sugarloaf/src/components/rich_text/compositor.rs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,60 @@ use crate::layout::{FragmentStyleDecoration, UnderlineShape};
1313

1414
pub struct Compositor {
1515
pub batches: BatchManager,
16+
pub bg_vertices: Vec<Vertex>,
1617
}
1718

1819
impl Compositor {
1920
pub fn new() -> Self {
2021
Self {
2122
batches: BatchManager::new(),
23+
bg_vertices: Vec::new(),
2224
}
2325
}
2426

27+
/// Add a background rect that uses replace blending (no alpha accumulation)
28+
#[inline]
29+
pub fn add_bg_rect(&mut self, rect: &Rect, depth: f32, color: &[f32; 4]) {
30+
let x = rect.x;
31+
let y = rect.y;
32+
let w = rect.width;
33+
let h = rect.height;
34+
let layers = [0i32, 0i32];
35+
let uv_default = [0.0f32, 0.0, 1.0, 1.0];
36+
37+
let v0 = Vertex {
38+
pos: [x, y, depth],
39+
color: *color,
40+
uv: [uv_default[0], uv_default[1]],
41+
layers,
42+
};
43+
let v1 = Vertex {
44+
pos: [x, y + h, depth],
45+
color: *color,
46+
uv: [uv_default[0], uv_default[3]],
47+
layers,
48+
};
49+
let v2 = Vertex {
50+
pos: [x + w, y + h, depth],
51+
color: *color,
52+
uv: [uv_default[2], uv_default[3]],
53+
layers,
54+
};
55+
let v3 = Vertex {
56+
pos: [x + w, y, depth],
57+
color: *color,
58+
uv: [uv_default[2], uv_default[1]],
59+
layers,
60+
};
61+
62+
self.bg_vertices.push(v0);
63+
self.bg_vertices.push(v1);
64+
self.bg_vertices.push(v2);
65+
self.bg_vertices.push(v2);
66+
self.bg_vertices.push(v3);
67+
self.bg_vertices.push(v0);
68+
}
69+
2570
/// Creates an underline decoration based on the style and rect
2671
pub fn create_underline_from_decoration(
2772
&self,
@@ -107,10 +152,12 @@ impl Compositor {
107152

108153
pub fn begin(&mut self) {
109154
self.batches.reset();
155+
self.bg_vertices.clear();
110156
}
111157

112-
pub fn finish(&mut self, vertices: &mut Vec<Vertex>) {
158+
pub fn finish(&mut self, vertices: &mut Vec<Vertex>, bg_vertices: &mut Vec<Vertex>) {
113159
self.batches.build_display_list(vertices);
160+
bg_vertices.append(&mut self.bg_vertices);
114161
}
115162

116163
/// Standard draw_run method (for compatibility)
@@ -147,7 +194,7 @@ impl Compositor {
147194
if let Some(bg_color) = style.background_color {
148195
let bg_rect =
149196
Rect::new(rect.x, style.topline, rect.width, style.line_height);
150-
self.batches.add_rect(&bg_rect, depth, &bg_color);
197+
self.add_bg_rect(&bg_rect, depth, &bg_color);
151198
}
152199

153200
if let Some(cursor) = style.cursor {
@@ -173,7 +220,7 @@ impl Compositor {
173220
rect.width - 2.0,
174221
font_height - 2.0,
175222
);
176-
self.batches.add_rect(&inner_rect, depth, &bg_color);
223+
self.add_bg_rect(&inner_rect, depth, &bg_color);
177224
}
178225
}
179226
crate::SugarCursor::Caret(cursor_color) => {
@@ -245,7 +292,7 @@ impl Compositor {
245292
if let Some(bg_color) = style.background_color {
246293
let bg_rect =
247294
Rect::new(rect.x, style.topline, rect.width, style.line_height);
248-
self.batches.add_rect(&bg_rect, depth, &bg_color);
295+
self.add_bg_rect(&bg_rect, depth, &bg_color);
249296
}
250297

251298
if let Some(cursor) = style.cursor {
@@ -271,7 +318,7 @@ impl Compositor {
271318
rect.width - 2.0,
272319
font_height - 2.0,
273320
);
274-
self.batches.add_rect(&inner_rect, depth, &bg_color);
321+
self.add_bg_rect(&inner_rect, depth, &bg_color);
275322
}
276323
}
277324
crate::SugarCursor::Caret(cursor_color) => {

0 commit comments

Comments
 (0)