Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 65 additions & 9 deletions frontends/rioterm/src/grid_emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
//! `font::shaper::run::RunIterator`.

use rio_backend::config::colors::term::TermColors;
use rio_backend::config::colors::{AnsiColor, NamedColor};
use rio_backend::crosswords::grid::row::Row;
use rio_backend::crosswords::pos::{Column, Line, Pos};
use rio_backend::crosswords::search::Match;
Expand Down Expand Up @@ -836,30 +837,85 @@ fn decoration_color(
}
}

/// Per-cell background color including the alpha-routing logic that
/// makes window opacity actually visible:
///
/// - Cells with an **explicit** bg (BgRgb / BgPalette inline encoding,
/// or a non-default `style.bg`) → `alpha = 255`. Stays opaque so
/// syntax-highlighted regions, TUI panels, etc. don't bleed through.
/// With `window.opacity-cells = true`, the per-frame opacity gets
/// applied here too — for users who want their Neovim / tmux UI to
/// share the window translucency.
/// - Cells with the **terminal default** bg (`style.bg ==
/// AnsiColor::Named(NamedColor::Background)` and no INVERSE) →
/// `alpha = 0`. The grid bg pass blends premultiplied-over so writing
/// `(0,0,0,0)` is a no-op — the drawable's clear color (which
/// carries `window.opacity` via `dynamic_background.1.a`) shows
/// through. This is what gives the user a translucent window.
/// - INVERSE flag → fg/bg swap promotes the cell to "has explicit bg"
/// so it stays opaque. INVERSE bypasses `opacity-cells` to keep
/// cursor / inverted-text readable.
///
/// Selection / hint highlights are applied at the `build_row_bg` slow
/// path with their own (always opaque) bg colors, so they don't go
/// through this function.
pub fn cell_bg(
sq: Square,
style_set: &StyleSet,
renderer: &Renderer,
term_colors: &TermColors,
) -> [u8; 4] {
let color = match sq.content_tag() {
// Alpha for cells that paint an explicit bg. Default = fully
// opaque (keeps TUI contrast). With `window.opacity-cells = true`
// and a transparent window, we multiply by the window opacity so
// the explicit-bg cells stay proportionally translucent. INVERSE
// always uses 255.
let explicit_bg_alpha = if renderer.opacity_cells {
renderer.cell_bg_alpha
} else {
255
};

match sq.content_tag() {
ContentTag::BgRgb => {
let (r, g, b) = sq.bg_rgb();
return [r, g, b, 255];
[r, g, b, explicit_bg_alpha]
}
ContentTag::BgPalette => {
let idx = sq.bg_palette_index() as usize;
renderer.color(idx, term_colors)
let color = renderer.color(idx, term_colors);
let [r, g, b, _] = normalized_to_u8(color);
[r, g, b, explicit_bg_alpha]
}
ContentTag::Codepoint => {
let mut style = style_set.get(sq.style_id());
if style.flags.contains(StyleFlags::INVERSE) {
std::mem::swap(&mut style.fg, &mut style.bg);
let style = style_set.get(sq.style_id());
let inverse = style.flags.contains(StyleFlags::INVERSE);
// "Default bg": the cell carries the terminal-default bg
// sentinel with no SGR override. INVERSE flips fg/bg,
// which always produces a non-default effective bg, so
// treat as explicit.
let has_default_bg =
!inverse && matches!(style.bg, AnsiColor::Named(NamedColor::Background));
if has_default_bg {
// Skip painting → the translucent clear (or opaque
// global bg, for non-transparent windows) shows
// through unchanged. Premultiplied blending makes
// (0,0,0,0) a no-op.
return [0, 0, 0, 0];
}
renderer.compute_bg_color(&style, term_colors)
// Resolve the explicit bg (with INVERSE swap applied).
let mut resolved = style;
if inverse {
std::mem::swap(&mut resolved.fg, &mut resolved.bg);
}
let color = renderer.compute_bg_color(&resolved, term_colors);
let [r, g, b, _] = normalized_to_u8(color);
// INVERSE always opaque — keep cursor / inverted text
// readable regardless of the opacity-cells flag.
let alpha = if inverse { 255 } else { explicit_bg_alpha };
[r, g, b, alpha]
}
};
normalized_to_u8(color)
}
}

#[inline]
Expand Down
53 changes: 50 additions & 3 deletions frontends/rioterm/src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ use rio_backend::sugarloaf::Sugarloaf;
use std::collections::BTreeSet;
use std::ops::RangeInclusive;

/// The window-bg clear alpha that flows into sugarloaf's
/// `set_background_color`. Stored on the renderer and re-applied on
/// every `effective_bg` write so OSC 11 changes don't reset
/// transparency to 1.0.
///
/// - Glass blur styles force `0.0` so the macOS-26 `NSGlassEffectView`
/// under the metal layer is what shows through.
/// - Otherwise it's the configured `window.opacity`, clamped to
/// `[0, 1]`.
#[inline]
fn window_bg_alpha(config: &Config) -> f32 {
if config.window.blur.is_glass() {
0.0
} else {
config.window.opacity.clamp(0.0, 1.0)
}
}

pub struct Renderer {
is_vi_mode_enabled: bool,
is_game_mode_enabled: bool,
Expand Down Expand Up @@ -60,6 +78,16 @@ pub struct Renderer {
// Dynamic background keep track of the original bg color and
// the same r,g,b with the mutated alpha channel.
pub dynamic_background: ([f32; 4], rio_backend::sugarloaf::Color, bool),
/// `window.opacity-cells` — apply window opacity to cells with an
/// SGR-set background too. Off by default. `cell_bg_alpha` is the
/// precomputed `(window.opacity * 255) as u8` to avoid a multiply
/// per cell.
pub opacity_cells: bool,
pub cell_bg_alpha: u8,
/// Target alpha for the window-bg clear (`0..=1`). 0 in glass
/// mode, otherwise `window.opacity`. Re-applied to `effective_bg`
/// every frame so OSC 11 doesn't undo the user's transparency.
pub window_bg_alpha: f32,
pub custom_mouse_cursor: bool,
pub trail_cursor_enabled: bool,
pub trail_cursor: trail_cursor::TrailCursor,
Expand All @@ -72,8 +100,18 @@ impl Renderer {

let mut dynamic_background =
(named_colors.background.0, named_colors.background.1, false);
if config.window.opacity < 1. {
dynamic_background.1.a = config.window.opacity as f64;
// Window-bg target alpha. Cached here at init and re-applied
// to every OSC-11-driven `effective_bg` refresh in
// `Renderer::run` so a runtime bg change doesn't reset
// transparency to 1.0. Glass styles force alpha = 0 so the
// NSGlassEffectView underneath the metal layer can provide
// the actual translucent bg.
let target_bg_alpha = window_bg_alpha(config);
if config.window.blur.is_glass() {
dynamic_background.1.a = 0.0;
dynamic_background.2 = true;
} else if config.window.opacity < 1. {
dynamic_background.1.a = target_bg_alpha as f64;
dynamic_background.2 = true;
} else if config.window.background_image.is_some() {
dynamic_background.1 = rio_backend::sugarloaf::Color::TRANSPARENT;
Expand Down Expand Up @@ -115,6 +153,9 @@ impl Renderer {
},
named_colors,
dynamic_background,
opacity_cells: config.window.opacity_cells,
cell_bg_alpha: (config.window.opacity.clamp(0.0, 1.0) * 255.0).round() as u8,
window_bg_alpha: target_bg_alpha,
search: search::SearchOverlay::default(),
assistant: assistant::AssistantOverlay::default(),
scrollbar: scrollbar::Scrollbar::new(config.enable_scroll_bar),
Expand Down Expand Up @@ -651,7 +692,7 @@ impl Renderer {
// want it to follow focus the way does (each surface's
// `terminal.colors.background` drives its own window chrome).
let current_context = context_manager.current_grid_mut().current_mut();
let effective_bg = match &current_context.renderable_content.background {
let mut effective_bg = match &current_context.renderable_content.background {
Some(crate::context::renderable::BackgroundState::Set(color)) => *color,
// Explicit OSC 111 reset OR panel that never ran OSC 11 →
// fall back to the config / dynamic_background (honors
Expand All @@ -660,6 +701,12 @@ impl Renderer {
self.dynamic_background.1
}
};
// Re-apply the configured window-bg alpha. Without this, an
// OSC 11 sequence that sets a new bg color resets the alpha
// to 1.0 and the window goes opaque even when
// `window.opacity < 1`. Glass mode forces alpha 0 so the
// backdrop view shows through.
effective_bg.a = self.window_bg_alpha as f64;

let window_update = if self.last_window_bg != Some(effective_bg) {
sugarloaf.set_background_color(Some(effective_bg));
Expand Down
14 changes: 12 additions & 2 deletions frontends/rioterm/src/router/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub fn create_window_builder(
.with_resizable(true)
.with_decorations(true)
.with_transparent(config.window.opacity < 1.)
.with_blur(config.window.blur)
.with_blur(config.window.blur.into())
.with_window_icon(Some(icon));

match config.window.decorations {
Expand Down Expand Up @@ -244,5 +244,15 @@ pub fn configure_window(winit_window: &Window, config: &Config) {
winit_window.set_title(title);
}

winit_window.set_blur(config.window.blur);
// macOS-only: stash the opacity value before set_blur so the
// initial glass install reads the correct tint. No-op on macOS <
// 26 / non-macOS — only matters when liquid-glass actually
// engages.
#[cfg(target_os = "macos")]
{
use rio_window::platform::macos::WindowExtMacOS;
winit_window.set_glass_opacity(config.window.opacity as f64);
}

winit_window.set_blur(config.window.blur.into());
}
24 changes: 24 additions & 0 deletions frontends/rioterm/src/screen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ pub struct ScreenWindowProperties {
pub window_id: rio_window::window::WindowId,
}

/// Whether the render surface should run in macOS compositor's
/// opaque-window fast path. Non-opaque iff the user actually
/// configured transparency (`window.opacity < 1`) or a glass
/// background effect — system blur on its own is not enough, since
/// without `opacity < 1` there's nothing transparent for the blur to
/// read through, so we keep the fast path for that case. Default =
/// opaque.
#[inline]
fn window_should_be_opaque(config: &rio_backend::config::Config) -> bool {
config.window.opacity >= 1.0 && !config.window.blur.is_glass()
}

impl Screen<'_> {
pub fn new<'screen>(
window_properties: ScreenWindowProperties,
Expand Down Expand Up @@ -273,6 +285,13 @@ impl Screen<'_> {
sugarloaf_errors,
)?;

// Window is opaque (compositor fast path) unless the user
// actually configured transparency. The render surface can
// hold per-pixel alpha either way — see `cell_bg` in
// `grid_emit.rs` — but flipping the layer to non-opaque is
// what makes the OS treat those alpha bits as see-through.
sugarloaf.set_window_opaque(window_should_be_opaque(config));

sugarloaf.set_background_color(Some(renderer.dynamic_background.1));

if let Some(image) = &config.window.background_image {
Expand Down Expand Up @@ -511,6 +530,11 @@ impl Screen<'_> {
// Update keyboard config in context manager
self.context_manager.config.keyboard = config.keyboard;

// Re-evaluate the opaque flag — toggling `window.opacity` /
// `window.blur` at runtime should flip the compositor mode.
self.sugarloaf
.set_window_opaque(window_should_be_opaque(config));

self.sugarloaf
.set_background_color(Some(self.renderer.dynamic_background.1));

Expand Down
4 changes: 2 additions & 2 deletions rio-backend/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,7 @@ mod tests {
assert_eq!(result.window.width, 800);
assert_eq!(result.window.height, 600);
assert_eq!(result.window.opacity, 0.75);
assert!(result.window.blur);
assert!(result.window.blur.is_enabled());
}

#[test]
Expand Down Expand Up @@ -1519,7 +1519,7 @@ mod tests {

// Window: opacity and blur overridden, others preserved
assert_eq!(result.window.opacity, 1.0);
assert!(result.window.blur);
assert!(result.window.blur.is_enabled());
assert_eq!(result.window.width, 1024);
assert_eq!(result.window.height, 768);

Expand Down
2 changes: 1 addition & 1 deletion rio-backend/src/config/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub struct PlatformWindow {
#[serde(default = "Option::default")]
pub opacity: Option<f32>,
#[serde(default = "Option::default")]
pub blur: Option<bool>,
pub blur: Option<window::WindowBlur>,
#[serde(
default = "Option::default",
rename = "background-image",
Expand Down
Loading
Loading