fix(ui): dim panes by blending terminal colors toward the window background (#464)

* fix(ui): dim panes by blending terminal colors toward the window background

* fix merge

* fix(ui): tighten the pane-dim blend
This commit is contained in:
ARNO
2026-08-10 23:07:37 +08:00
committed by GitHub
parent c216b389ae
commit 12df66fb0f
3 changed files with 490 additions and 20 deletions
+438 -4
View File
@@ -232,6 +232,21 @@ fn active_selection_bg(cx: &gpui::App) -> Rgb {
}
}
/// Blends a finished cell's colours toward `under` for a dimmed pane. The
/// blend has to happen on the *cell*, not on the palette: truecolour cells
/// (the direct `38;2;…`/`48;2;…` SGR a prompt like starship emits for its
/// segments) carry their own `Spec` colour that a palette-level dim would
/// never see, leaving a truecolour prompt at full brightness while indexed
/// content around it dims.
fn dim_cell(mut rc: RenderCell, dim: f32, under: Rgba) -> RenderCell {
rc.fg = blend_toward(rc.fg, dim, under);
rc.bg = blend_toward(rc.bg, dim, under);
if let Some(u) = rc.underline_color.as_mut() {
*u = blend_toward(*u, dim, under);
}
rc
}
/// What a search match is washed with.
///
/// The theme's accent, not the terminal palette's selection colour. A hit and a
@@ -259,6 +274,104 @@ struct PaintColors {
bg_rgb: Rgb,
}
/// The under-colour a dimmed pane blends its content toward: the window
/// background as it actually sits in the frame, i.e. the active preset fill
/// premultiplied by the window's own opacity. Dimming by blending every colour
/// toward this value (instead of alpha-multiplying each primitive) keeps the
/// composite of stacked layers — a powerline separator path over its segment
/// quad, text over a tint — exactly as dimmed as any single layer, so the
/// decorations of a prompt stay continuous when the pane goes inactive.
///
/// The fill comes from the active preset rather than the flat
/// `theme().background` token because the workspace root paints the preset
/// (see `theme::workspace_background`): with a gradient or wallpaper preset
/// the actual backdrop is not the theme token, and blending toward the wrong
/// colour would leave a tinted slab inside the dimmed pane. Solid fills match
/// exactly; gradients are approximated by their midpoint stop; a wallpaper
/// image rides over the fill at low opacity, so the fill stays the best
/// available target. Cells whose background is not painted (the default
/// terminal background) intentionally keep showing the un-dimmed window
/// background through them, so the backdrop material stays visible.
///
/// Two trade-offs follow from painting opaque, pre-blended colours instead of
/// the old element-opacity style. On a translucent window the desktop no
/// longer shows through a dimmed pane's painted cells — the old style left
/// them at alpha `dim` and let the backdrop contribute; the new style paints
/// them opaque, blended toward `fill × window_opacity`, which ignores the
/// backdrop's own contribution. And only the terminal element's cells are
/// dimmed: the search bar, completion menu and integration notice render
/// outside the grid and stay at full brightness, where the old style faded
/// the whole `TerminalView` — the same "the fading is worn by what it holds"
/// rule the drag grip follows.
fn dim_under(cx: &gpui::App) -> Rgba {
// Mirror `workspace_background`: the active preset fill when a preset is
// installed, the theme token otherwise. The alpha is the window opacity
// in both arms, so the premultiply is uniform.
match cx.try_global::<crate::ui::presets::ActiveBackground>() {
Some(bg) => fill_under(&bg.fill, bg.opacity),
None => theme_under(cx.theme().background),
}
}
/// The premultiplied under-colour for an active preset's fill at the window's
/// own opacity. Solid fills are exact; gradients are approximated by their
/// midpoint stop (a wallpaper image rides over the fill at low opacity, so
/// the fill stays the best available target).
fn fill_under(fill: &crate::ui::presets::Fill, opacity: Option<f32>) -> Rgba {
let packed = match fill {
crate::ui::presets::Fill::Solid(c) => *c,
crate::ui::presets::Fill::Vertical { top, bottom } => {
crate::ui::presets::mix(*top, *bottom, 0.5)
}
crate::ui::presets::Fill::Horizontal { left, right } => {
crate::ui::presets::mix(*left, *right, 0.5)
}
};
premultiplied(packed, opacity.unwrap_or(1.))
}
/// The premultiplied under-colour for the theme background token, used when
/// no preset is installed — the `None` arm of `workspace_background`.
fn theme_under(bg: Hsla) -> Rgba {
let bg = Rgba::from(bg);
Rgba {
r: bg.r * bg.a,
g: bg.g * bg.a,
b: bg.b * bg.a,
a: 1.,
}
}
/// The premultiplied under-colour for a packed fill colour at the window's
/// own opacity — exactly the colour a `Solid` workspace fill paints over the
/// OS backdrop, ignoring the backdrop's own contribution.
fn premultiplied(packed: u32, opacity: f32) -> Rgba {
Rgba {
r: ((packed >> 16) & 0xff) as f32 / 255. * opacity,
g: ((packed >> 8) & 0xff) as f32 / 255. * opacity,
b: (packed & 0xff) as f32 / 255. * opacity,
a: 1.,
}
}
/// Blends `c` toward `under` in RGB space, keeping `c`'s own alpha. `under`
/// is passed pre-converted to `Rgba` because it is a frame constant: every
/// cell blends fg, bg and an optional underline colour against it, and the
/// HSL↔RGB round trip would otherwise run once per colour per cell. Linear
/// in the composited result: painting `blend_toward(a)` over `blend_toward(b)`
/// equals `blend_toward(a over b)`, which is exactly what a uniform opacity
/// of `dim` over the window background would produce.
fn blend_toward(c: Hsla, dim: f32, under: Rgba) -> Hsla {
let c = Rgba::from(c);
Rgba {
r: dim * c.r + (1. - dim) * under.r,
g: dim * c.g + (1. - dim) * under.g,
b: dim * c.b + (1. - dim) * under.b,
a: c.a,
}
.into()
}
impl PaintColors {
fn resolve(theme: &gpui_component::Theme, cx: &gpui::App) -> Self {
let default_fg = theme.foreground;
@@ -300,6 +413,28 @@ impl PaintColors {
bg_rgb,
}
}
/// Blends every colour toward `under` for a dimmed pane. The alpha of each
/// colour is left alone — the blend happens in RGB, so translucent tints
/// (selection, matches) composite over the already-blended cell colours
/// exactly as dimmed as opaque content does.
///
/// `fg_rgb`/`bg_rgb` are deliberately left raw: they only feed `resolve`
/// for the named foreground/background cells, and those cells get blended
/// by `dim_cell` like every other cell — blending them here as well would
/// dim the named colours twice.
fn dimmed(&self, dim: f32, under: Rgba) -> Self {
Self {
default_fg: blend_toward(self.default_fg, dim, under),
default_bg: blend_toward(self.default_bg, dim, under),
caret: blend_toward(self.caret, dim, under),
selection_bg: blend_toward(self.selection_bg, dim, under),
match_bg: blend_toward(self.match_bg, dim, under),
current_match_bg: blend_toward(self.current_match_bg, dim, under),
fg_rgb: self.fg_rgb,
bg_rgb: self.bg_rgb,
}
}
}
fn paint_backgrounds(window: &mut Window, geom: &CellGeom, buf: &[RenderCell]) {
@@ -1102,6 +1237,8 @@ impl TerminalElement {
cols: usize,
want_sliver: bool,
cx: &App,
dim: f32,
under: Rgba,
) -> GridSnapshot {
buf.clear();
buf.resize(rows * cols, RenderCell::default());
@@ -1154,26 +1291,34 @@ impl TerminalElement {
}
let rc = snapshot_cell(cell.cell, cell.point, &palette, colors, selection.as_ref());
any_selected |= rc.selected;
buf[row as usize * cols + col] = rc;
buf[row as usize * cols + col] = if dim < 1. {
dim_cell(rc, dim, under)
} else {
rc
};
}
if want_sliver && (display_offset as usize) < term.grid().history_size() {
let line = AlacLine(-display_offset - 1);
let mut row_buf = vec![RenderCell::default(); cols];
for (col, rc) in row_buf
for (col, slot) in row_buf
.iter_mut()
.enumerate()
.take(term.columns().min(cols))
{
let point = AlacPoint::new(line, AlacColumn(col));
*rc = snapshot_cell(
let mut rc = snapshot_cell(
&term.grid()[line][AlacColumn(col)],
point,
&palette,
colors,
selection.as_ref(),
);
if dim < 1. {
rc = dim_cell(rc, dim, under);
}
any_selected |= rc.selected;
*slot = rc;
}
sliver = Some(row_buf);
}
@@ -1528,9 +1673,34 @@ impl Element for TerminalElement {
let cursor_visible = self.view.read(cx).cursor_visible;
let bell_flash = self.view.read(cx).bell_flash;
let editor_active = self.view.read(cx).input_active();
// The pane leaf stores its per-frame dim here (see `TerminalView::dim`).
// Blending the palette and paint colours toward the window background is
// what actually dims the pane; the pane no longer wraps the terminal in
// an element-opacity style, whose per-primitive alpha multiplication
// would leave stacked decorations with a seam against the segments
// below them.
let dim = self.view.read(cx).dim.clamp(0., 1.);
let (colors, dim, under) = if dim < 1. {
let under = dim_under(cx);
(colors.dimmed(dim, under), dim, under)
} else {
// `under` is only read while `dim < 1.` (per-cell in the grid and
// over bitmaps), so a default keeps the common rest frame from
// paying for the preset lookup and colour math.
(colors, 1., Rgba::default())
};
let mut buf = GRID_BUF.with(|b| std::mem::take(&mut *b.borrow_mut()));
let snap = self.build_grid(&colors, &mut buf, geom.rows, geom.cols, frac > 0., cx);
let snap = self.build_grid(
&colors,
&mut buf,
geom.rows,
geom.cols,
frac > 0.,
cx,
dim,
under,
);
let cursor = snap.cursor;
let sliver = snap.sliver.as_ref();
@@ -1660,6 +1830,14 @@ impl Element for TerminalElement {
size: size(geom.cell_width * span_cols, geom.line_height * span_rows),
};
let _ = window.paint_image(bounds, Corners::default(), img.data.clone(), 0, false);
// A bitmap is a single layer, so its dim cannot come from the
// pre-blended palette; blend the image itself toward the under
// the same way the surrounding cells are blended.
if dim < 1. {
let mut c: Hsla = under.into();
c.a = 1. - dim;
window.paint_quad(fill(bounds, c));
}
}
// Evict superseded / deleted frames from the sprite atlas. Without
// this a browser re-transmitting at 60fps would leak one GPU tile per
@@ -1732,6 +1910,16 @@ impl Element for TerminalElement {
if let Some(start) = fps_start {
super::fps::record(start.elapsed());
}
// The dim is a per-frame hand-off from the pane leaf to this paint
// (see `TerminalView::set_dim`): the pane writes it while rendering,
// this paint reads it above. Reset it here so the hand-off is
// structural — any frame that ends without a render site having set
// the dim paints at full brightness next, instead of carrying a stale
// value. A render site that forgets to set it (a maximized pane has
// no chrome to set it) therefore degrades to a single stale frame at
// worst, never a permanently dimmed terminal.
self.view.update(cx, |v, _cx| v.set_dim(1.));
}
}
@@ -2699,6 +2887,252 @@ mod tests {
);
}
#[test]
fn blend_toward_mixes_in_rgb_space_and_keeps_alpha() {
let under = Rgba {
r: 16. / 255.,
g: 18. / 255.,
b: 19. / 255.,
a: 1.,
};
let red = to_hsla(Rgb {
r: 218,
g: 98,
b: 125,
});
let half = Rgba::from(blend_toward(red, 0.5, under));
assert!((half.r - 0.5 * (218. / 255.) - 0.5 * (16. / 255.)).abs() < 1e-3);
assert!((half.g - 0.5 * (98. / 255.) - 0.5 * (18. / 255.)).abs() < 1e-3);
assert!((half.b - 0.5 * (125. / 255.) - 0.5 * (19. / 255.)).abs() < 1e-3);
assert_eq!(half.a, 1.0, "an opaque colour stays opaque");
// A translucent tint keeps its own alpha: only the rgb is blended.
let tint = Hsla { a: 0.24, ..red };
let dimmed = Rgba::from(blend_toward(tint, 0.55, under));
assert!((dimmed.a - 0.24).abs() < 1e-3, "alpha is untouched");
assert!(
(dimmed.r - 0.55 * (218. / 255.) - 0.45 * (16. / 255.)).abs() < 1e-3,
"rgb still blends toward the under"
);
}
#[test]
fn dimming_a_stacked_fill_stays_continuous_with_its_segment() {
// The reported bug: a powerline separator (fill) drawn over its
// segment (bg) dimmed unevenly because the old pane element-opacity
// alpha-multiplied each layer, so the separator kept the segment's own
// dim visible through its (1 - dim) and landed with a seam against
// the segment (brighter on this palette). Blending every colour
// toward the under first keeps the composite exactly as dimmed as any
// single layer.
let under = Rgba {
r: 16. / 255.,
g: 18. / 255.,
b: 19. / 255.,
a: 1.,
};
let segment = to_hsla(Rgb {
r: 218,
g: 98,
b: 125,
});
let fill = to_hsla(Rgb {
r: 154,
g: 52,
b: 142,
});
let dim = 0.55;
let over = |top: Rgba, bottom: Rgba| Rgba {
r: top.r * top.a + bottom.r * (1. - top.a),
g: top.g * top.a + bottom.g * (1. - top.a),
b: top.b * top.a + bottom.b * (1. - top.a),
a: top.a + bottom.a * (1. - top.a),
};
// New rendering: both layers are pre-blended and stay opaque, so the
// fill over the segment composites to exactly the dimmed fill — the
// same value the segment itself renders as, with no seam between them.
let dimmed_segment = Rgba::from(blend_toward(segment, dim, under));
let dimmed_fill = Rgba::from(blend_toward(fill, dim, under));
let new_composite = over(dimmed_fill, dimmed_segment);
assert!((new_composite.r - dimmed_fill.r).abs() < 1e-6);
assert!((new_composite.g - dimmed_fill.g).abs() < 1e-6);
assert!((new_composite.b - dimmed_fill.b).abs() < 1e-6);
// Old rendering: the pane opacity turned both layers translucent, so
// the fill showed the already-dimmed segment underneath and painted
// with a seam against the segment next to it.
let old_fill = Rgba {
a: dim,
..Rgba::from(fill)
};
let old_segment = Rgba {
a: dim,
..Rgba::from(segment)
};
let old_composite = over(old_fill, old_segment);
assert!(
old_composite.r > new_composite.r + 0.05,
"on this palette the old alpha-multiplied separator was visibly brighter than the segment"
);
}
#[test]
fn dim_cell_blends_truecolor_cells_toward_the_under() {
// Starship's prompt paints its segments with direct 38;2;/48;2; SGR
// colours that a palette-level dim would never see; the pane dim must
// reach those cells too, or the whole prompt would stay at full
// brightness while indexed content around it dims.
let under = Rgba {
r: 16. / 255.,
g: 18. / 255.,
b: 19. / 255.,
a: 1.,
};
let mut cell = RenderCell::default();
cell.c = 'x';
cell.fg = to_hsla(Rgb {
r: 218,
g: 98,
b: 125,
});
cell.bg = to_hsla(Rgb {
r: 154,
g: 52,
b: 142,
});
cell.draw_bg = true;
let dimmed = dim_cell(cell, 0.55, under);
assert_eq!(dimmed.c, 'x', "the character itself survives");
assert!(dimmed.draw_bg, "the explicit background flag survives");
let fg = Rgba::from(dimmed.fg);
assert!((fg.r - (0.55 * 218. + 0.45 * 16.) / 255.).abs() < 1e-3);
assert!((fg.g - (0.55 * 98. + 0.45 * 18.) / 255.).abs() < 1e-3);
assert!((fg.b - (0.55 * 125. + 0.45 * 19.) / 255.).abs() < 1e-3);
let bg = Rgba::from(dimmed.bg);
assert!((bg.r - (0.55 * 154. + 0.45 * 16.) / 255.).abs() < 1e-3);
assert_eq!(fg.a, 1.0, "opaque cell colours stay opaque");
// A DIM-flagged cell keeps its reduced alpha through the blend.
let mut dim_flag = RenderCell::default();
dim_flag.fg = to_hsla(Rgb {
r: 218,
g: 98,
b: 125,
});
dim_flag.fg.a = DIM_OPACITY;
let dimmed_flag = dim_cell(dim_flag, 0.55, under);
assert!(
(dimmed_flag.fg.a - DIM_OPACITY).abs() < 1e-3,
"the SGR dim alpha survives the pane dim"
);
}
#[test]
fn paint_colors_dimmed_keeps_translucent_tint_alphas() {
let colors = caret_colors();
let under = Rgba {
r: 16. / 255.,
g: 18. / 255.,
b: 19. / 255.,
a: 1.,
};
let dimmed = colors.dimmed(0.55, under);
assert!((dimmed.caret.a - colors.caret.a).abs() < 1e-3);
assert!(
(dimmed.selection_bg.a - colors.selection_bg.a).abs() < 1e-3,
"translucent overlays keep their own alpha so they still tint"
);
assert_ne!(dimmed.default_fg, colors.default_fg);
// The named foreground/background rgb feed `resolve` for default cells,
// which `dim_cell` blends — pre-blending them here would double-dim.
assert_eq!(dimmed.fg_rgb, colors.fg_rgb);
assert_eq!(dimmed.bg_rgb, colors.bg_rgb);
}
#[test]
fn premultiplied_under_scales_rgb_by_the_window_opacity_and_stays_opaque() {
// A dimmed pane blends toward the colour the workspace actually paints
// behind it: the fill scaled by the window's own opacity (premultiplied
// alpha), never toward a colour that includes the OS backdrop.
let u = premultiplied(0xda_62_7d, 0.82);
assert_eq!(u.a, 1.0, "the under must stay an opaque paint colour");
assert!((u.r - 0.82 * (218. / 255.)).abs() < 1e-6);
assert!((u.g - 0.82 * (98. / 255.)).abs() < 1e-6);
assert!((u.b - 0.82 * (125. / 255.)).abs() < 1e-6);
// A fully opaque window keeps the fill untouched.
let opaque = premultiplied(0xda_62_7d, 1.0);
assert!((opaque.r - 218. / 255.).abs() < 1e-6);
assert!((opaque.g - 98. / 255.).abs() < 1e-6);
assert!((opaque.b - 125. / 255.).abs() < 1e-6);
}
#[test]
fn fill_under_maps_preset_fills_to_their_premultiplied_under() {
use crate::ui::presets::Fill;
// A solid fill is exactly the colour the workspace paints behind the
// terminal, scaled by the window's own opacity.
let solid = fill_under(&Fill::Solid(0xda_62_7d), Some(0.82));
assert!((solid.r - 0.82 * (218. / 255.)).abs() < 1e-6);
assert!((solid.g - 0.82 * (98. / 255.)).abs() < 1e-6);
assert!((solid.b - 0.82 * (125. / 255.)).abs() < 1e-6);
assert_eq!(solid.a, 1.0);
// A gradient is approximated by its midpoint stop (`mix` rounds each
// channel, so 0x00…ff lands on 128/255 rather than exactly 0.5).
let vertical = fill_under(
&Fill::Vertical {
top: 0x00_00_00,
bottom: 0xff_ff_ff,
},
None,
);
assert!((vertical.r - 128. / 255.).abs() < 1e-6);
assert_eq!(vertical.r, vertical.g);
assert_eq!(vertical.r, vertical.b);
let horizontal = fill_under(
&Fill::Horizontal {
left: 0xff_00_00,
right: 0x00_00_ff,
},
None,
);
assert!((horizontal.r - 128. / 255.).abs() < 1e-6);
assert!((horizontal.b - 128. / 255.).abs() < 1e-6);
// No explicit opacity means the fill is used at full strength.
let full = fill_under(&Fill::Solid(0xda_62_7d), None);
assert!((full.r - 218. / 255.).abs() < 1e-6);
}
#[test]
fn theme_under_premultiplies_by_the_theme_background_alpha() {
// The no-preset fallback mirrors `theme().background`, whose alpha is
// the window's own opacity.
let bg = to_hsla(Rgb {
r: 218,
g: 98,
b: 125,
});
let mut bg = bg;
bg.a = 0.82;
let under = theme_under(bg);
assert_eq!(under.a, 1.0);
assert!((under.r - 0.82 * (218. / 255.)).abs() < 1e-6);
assert!((under.g - 0.82 * (98. / 255.)).abs() < 1e-6);
assert!((under.b - 0.82 * (125. / 255.)).abs() < 1e-6);
// An opaque theme token is used untouched.
let opaque = theme_under(to_hsla(Rgb {
r: 218,
g: 98,
b: 125,
}));
assert!((opaque.r - 218. / 255.).abs() < 1e-6);
}
#[test]
fn underline_flag_bits_map_to_their_variants() {
let palette = [Rgb { r: 0, g: 0, b: 0 }; 256];
+25
View File
@@ -229,6 +229,19 @@ pub struct TerminalView {
editor_drag_word: Option<(usize, usize)>,
editor_goal_col: Option<usize>,
pub(super) hovered_link: Option<HoveredLink>,
/// How opaque the pane wants this terminal painted this frame: 1.0 at
/// rest, [`crate::ui::pane::INACTIVE_DIM`] for an unfocused pane in a
/// split, [`crate::ui::pane::LIFTED_DIM`] while the pane is being
/// dragged. The pane leaf computes it and stores it here, because the
/// dim is applied by blending the terminal's own colours toward the
/// window background instead of the pane's element opacity — an opacity
/// style would alpha-multiply every quad and path separately, so a
/// powerline triangle stacked over a segment quad would show the
/// already-dimmed segment through its own (1-dim) alpha and land with a
/// visible seam where it meets the segment. The terminal element resets
/// the field to 1.0 at the end of every paint, so the value never
/// outlives the frame it was written for.
pub(super) dim: f32,
_focus_subs: Vec<gpui::Subscription>,
}
@@ -1123,6 +1136,7 @@ impl TerminalView {
search: None,
cursor_visible: true,
focused: true,
dim: 1.,
search_focused: false,
search_case_sensitive: false,
search_regex: false,
@@ -1215,6 +1229,17 @@ impl TerminalView {
self.terminal.foreground_cwd()
}
/// Sets how opaque the pane wants this terminal painted; the pane leaf
/// calls this every frame while rendering, and the terminal element
/// blends its colours toward the window background during paint (see
/// [`Self::dim`] for why that beats an element-opacity style). The
/// element resets the value to 1.0 at the end of every paint, so a
/// render site that forgets to set it gets full brightness next frame —
/// never a stale dim.
pub(crate) fn set_dim(&mut self, dim: f32) {
self.dim = dim;
}
pub fn remote_context(&self) -> Option<RemoteContext> {
self.terminal.remote_context()
}
+27 -16
View File
@@ -12,6 +12,15 @@ const MIN_RATIO: f32 = 0.1;
const MAX_RATIO: f32 = 0.9;
const DIVIDER_THICKNESS: f32 = 5.;
/// How opaque a dragged (lifted) pane's terminal paints, blended toward the
/// window background. The terminal reads it through `TerminalView::dim`; kept
/// here so that field's docs can point at the real numbers instead of
/// duplicating them.
pub(crate) const LIFTED_DIM: f32 = 0.45;
/// How opaque an unfocused pane in a split paints while `dim_inactive_panes`
/// is on.
pub(crate) const INACTIVE_DIM: f32 = 0.55;
#[derive(Clone)]
pub enum PaneSlot {
Ready(Entity<TerminalView>),
@@ -831,24 +840,18 @@ impl Pane<PaneSlot> {
let focused = v.contains_focused(window, cx);
let lifted = chrome.lifted == Some(id);
let grip = chrome.rearrangeable && chrome.lifted.is_none();
// Whatever fading the pane is under is worn by what it holds,
// not by the pane: a grip dimmed along with the pane it belongs
// to would be faintest on exactly the panes being reached for.
let fade = if lifted {
Some(0.45)
// The terminal renders itself at this opacity by blending its
// colours toward the window background (`TerminalView::dim`,
// whose field docs explain why that beats an element-opacity
// style). The connecting screen has no stacked content, so it
// keeps the plain opacity style.
let dim = if lifted {
LIFTED_DIM
} else if chrome.dim_inactive && !focused {
Some(0.55)
INACTIVE_DIM
} else {
None
1.0
};
let content =
div()
.size_full()
.when_some(fade, |d, f| d.opacity(f))
.map(|d| match v {
PaneSlot::Ready(t) => d.child(t.clone()),
PaneSlot::Connecting(p) => d.child(p.clone()),
});
div()
.size_full()
.relative()
@@ -856,7 +859,15 @@ impl Pane<PaneSlot> {
.when(chrome.rearrangeable, |d| {
d.pt(px(crate::ui::pane_drag::HANDLE_STRIP))
})
.child(content)
.map(|d| match v {
PaneSlot::Ready(t) => {
t.update(cx, |v, _cx| v.set_dim(dim));
d.child(t.clone())
}
PaneSlot::Connecting(p) => {
d.when(dim < 1., |d| d.opacity(dim)).child(p.clone())
}
})
.when(grip, |d| {
d.child(crate::ui::pane_drag::reveal_band(id, &chrome.hovered))
.child(crate::ui::pane_drag::handle(