fix(ui): stop filled children squaring off rounded corners

The cursor-shape toggles (Block / Bar / Underline) reported in #236 look
rough because the selected segment's fill covers the whole corner of the
track it caps, and its outer edge is a hard, unantialiased vertical cut.
The track's own border arc is drawn correctly and antialiased — it just
floats *inside* that square, so the corner reads as a stair-step.

The controls were relying on `overflow_hidden` to shape their end
segments' fills to the track's rounding. It cannot do that.
`gpui::ContentMask` is a bare axis-aligned `Bounds`; `Style::overflow_mask`
builds it from the element's bounds shrunk by the border widths and drops
`corner_radii` entirely, and every shader applies it as a hard
`clip_distances < 0` discard. So the mask only ever cuts a square, and it
never antialiases the cut. A container's own corners come from somewhere
else — the quad shader's SDF, `saturate(0.5 - distance)` — which is why a
plain rounded card renders smooth while anything with a filled child in
its corner does not. That divergence is the whole bug, and the reporter's
screenshot shows both halves of it: the corner with the selected fill is
square, the corner without one is a clean arc.

The fill has to carry the radius itself, so it goes down the SDF path too.
It sits one border-width inside the track, so the concentric radius is
`outer - border`. `ui::rounding` states that rule once, with the
constants and the corner-assignment helpers, and unit-tests the
invariants (inset is strictly tighter than the outer radius, clamps at
zero, only the end segments cap the track).

Applied to every place a child paints a fill into a rounded corner:

* the segmented controls (the reported one, plus the others `segmented`
  serves),
* the −/value/+ steppers' hover fills — those glyph boxes also had to be
  pinned to the track's content height, because a padded auto-height box
  measures 31px against a 22px content box and its rounded corner would
  land 4½px outside the visible strip,
* the theme picker's flush-mounted previews,
* the diff overlay's card headers and the row that closes a card.

Not reproducible locally: this is a rendering-geometry defect, not a
platform one, but it is most visible at a device pixel ratio of 1, where
the clip's hard edge is a whole physical pixel. Verified by reading the
gpui mask/shader source and the reporter's screenshot pixel by pixel, and
by the geometry tests; the on-screen result is left for visual
acceptance.

Refs #236
This commit is contained in:
l0ng-ai
2026-07-29 00:25:33 +08:00
parent 9ca3319239
commit cfcf5f79fa
6 changed files with 378 additions and 24 deletions
+18
View File
@@ -0,0 +1,18 @@
# Project agent memory
This file is the project's committed home for project-intrinsic agent knowledge: build, test, release, architecture, and sharp-edge notes that should travel with the code.
- Add durable project-specific notes here as they are discovered through real work.
- **gpui's `overflow_hidden` does not round-clip.** Its content mask is an
axis-aligned rectangle applied as a hard per-fragment discard, so a child that
paints a background into a rounded container's corner squares that corner off
with no anti-aliasing. Any such child must carry its own radius, inset one
border-width. `src/ui/rounding.rs` holds the rule, the constants and the
tests; read it before adding a filled band or segment to a rounded track.
## Maintaining this file
Keep this file for knowledge useful to almost every future agent session in this project.
Do not repeat what the codebase already shows; point to the authoritative file or command instead.
Prefer rewriting or pruning existing entries over appending new ones.
When updating this file, preserve this bar for all agents and keep entries concise.
+19
View File
@@ -58,6 +58,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
session" means: paste it into `codex resume`, a bug report, or another tool.
(#211)
### Fixed
- **Rounded UI controls no longer square off their corners**
([#236](https://github.com/l0ng-ai/tty7/issues/236)) — the cursor-shape
toggles (Block / Bar / Underline) are the clearest case: the selected
segment's fill filled the whole corner of the track it caps, with the track's
own anti-aliased border arc floating *inside* that square. The controls were
relying on `overflow_hidden` to shape those fills to the track's rounding, and
it cannot: gpui's overflow mask is an axis-aligned rectangle with no corner
radii, tested per fragment as a hard discard, so it can only ever cut a square
and never anti-aliases the cut. A container's own corners come from the quad
shader's distance field instead, which is why a plain card looked smooth while
anything with a filled child in the corner did not. Every such fill now
carries its own radius, inset one border-width so it nests inside the border
rather than bulging past it: the segmented controls, the /value/+ steppers'
hover fills, the theme picker's flush previews, and the diff overlay's card
headers and closing rows. Most visible at a device pixel ratio of 1, where the
hard clip edge is a whole physical pixel wide.
## [26.7.6] - 2026-07-28
### Added
+71 -9
View File
@@ -21,7 +21,9 @@
use std::collections::HashSet;
use std::path::PathBuf;
use gpui::{AnyElement, FocusHandle, FontWeight, KeyDownEvent, Window, div, prelude::*, px};
use gpui::{
AnyElement, FocusHandle, FontWeight, KeyDownEvent, Pixels, Window, div, prelude::*, px,
};
use gpui_component::button::Button;
use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_flex};
@@ -29,6 +31,8 @@ use crate::terminal::git_diff::{
self, AUTO_COLLAPSE_LINES, DiffSnapshot, FileDiff, FileStatus, LineKind,
};
use crate::ui::app::Tty7App;
use crate::ui::rounding;
use crate::ui::rounding::RoundedCorners as _;
/// What the overlay currently shows: probing, a parsed snapshot, or the
/// answer that the cwd stopped being a repo.
@@ -562,6 +566,17 @@ impl Tty7App {
None => file.path.clone(),
};
// The header paints a solid band flush into the card's corners, and the
// card's `overflow_hidden` cannot round it — that clip is a square,
// unantialiased scissor (issue #236, see `ui::rounding`). So the band
// carries the radius: top two when a body follows it, all four when the
// card is collapsed and the header *is* the card.
let header_corners = rounding::stack_corners(
0,
if expanded { 2 } else { 1 },
rounding::CARD_RADIUS,
rounding::HAIRLINE,
);
let mut header = h_flex()
.id(("diff-file-header", idx))
.w_full()
@@ -569,6 +584,7 @@ impl Tty7App {
.gap_2()
.px_2p5()
.py_1p5()
.rounded_corners(header_corners)
.bg(cx.theme().secondary)
.when(expandable, |h| {
let path = file.path.clone();
@@ -649,13 +665,31 @@ impl Tty7App {
.w_full()
.border_1()
.border_color(cx.theme().border)
.rounded_md()
.rounded(rounding::CARD_RADIUS)
// Overflow backstop only; the bands inside round themselves.
.overflow_hidden()
.child(header);
if expanded {
let mut body = v_flex().w_full();
for hunk in &file.hunks {
// Split every hunk up front so the *last* row is knowable: a diff
// cell paints a tint, and the card's clip is square, so the row that
// ends the card has to draw the bottom corners itself. A truncation
// notice (no fill of its own) takes that job away again.
let hunks: Vec<_> = file
.hunks
.iter()
.map(|hunk| (hunk, split_hunk(&hunk.lines)))
.collect();
let closing_row = if file.truncated {
None
} else {
hunks
.iter()
.rposition(|(_, rows)| !rows.is_empty())
.map(|h| (h, hunks[h].1.len() - 1))
};
for (h, (hunk, rows)) in hunks.iter().enumerate() {
body = body.child(
div()
.w_full()
@@ -668,8 +702,8 @@ impl Tty7App {
.truncate()
.child(hunk.header.clone()),
);
for row in split_hunk(&hunk.lines) {
body = body.child(self.diff_split_row(&row, cx));
for (r, row) in rows.iter().enumerate() {
body = body.child(self.diff_split_row(row, closing_row == Some((h, r)), cx));
}
}
if file.truncated {
@@ -694,7 +728,16 @@ impl Tty7App {
/// One side-by-side row: the old (left) and new (right) cells with a hairline
/// splitter between them. A `None` cell — no counterpart on that side —
/// paints a muted placeholder so a pure add/remove reads as one column empty.
fn diff_split_row(&self, row: &SplitRow, cx: &Context<Self>) -> AnyElement {
///
/// `closes_card` marks the row that sits on the card's bottom edge; its two
/// outer cells then round their outer bottom corner, since the card's clip
/// cannot do it for them (see `ui::rounding`).
fn diff_split_row(&self, row: &SplitRow, closes_card: bool, cx: &Context<Self>) -> AnyElement {
let radius = if closes_card {
rounding::inner_radius(rounding::CARD_RADIUS, rounding::HAIRLINE)
} else {
px(0.)
};
h_flex()
.w_full()
// Fixed row height so blank diff lines don't collapse.
@@ -702,21 +745,30 @@ impl Tty7App {
.items_stretch()
.text_xs()
.font_family(self.font_family.clone())
.child(self.diff_split_cell(row.left.as_ref(), Side::Old, cx))
.child(self.diff_split_cell(row.left.as_ref(), Side::Old, radius, cx))
.child(div().flex_shrink_0().w(px(1.)).bg(cx.theme().border))
.child(self.diff_split_cell(row.right.as_ref(), Side::New, cx))
.child(self.diff_split_cell(row.right.as_ref(), Side::New, radius, cx))
.into_any_element()
}
/// One half of a split row: a right-aligned line-number gutter, then the
/// marker and text in the terminal font, tinted green/red when changed.
///
/// `outer_radius` rounds the cell's own outer bottom corner — non-zero only
/// on the row that closes the card, whose tint would otherwise square that
/// corner off (see `ui::rounding`).
fn diff_split_cell(
&self,
cell: Option<&SplitCell>,
side: Side,
outer_radius: Pixels,
cx: &Context<Self>,
) -> AnyElement {
let base = h_flex().flex_1().min_w_0().h_full().items_center();
let base = match side {
Side::Old => base.rounded_bl(outer_radius),
Side::New => base.rounded_br(outer_radius),
};
let Some(cell) = cell else {
return base.bg(cx.theme().muted.opacity(0.3)).into_any_element();
};
@@ -749,17 +801,27 @@ impl Tty7App {
/// has no blob to diff a never-added file against, but hiding them would
/// read as lost work (agents create files constantly).
fn diff_untracked_section(&self, untracked: &[String], cx: &Context<Self>) -> AnyElement {
// Same filled-band-in-a-rounded-card shape as `diff_file_card`, so the
// header owns the corners it sits in. The rows below it paint no fill,
// which is why only the top pair is ever non-zero here.
let header_corners = rounding::stack_corners(
0,
if untracked.is_empty() { 1 } else { 2 },
rounding::CARD_RADIUS,
rounding::HAIRLINE,
);
let mut section = v_flex()
.w_full()
.border_1()
.border_color(cx.theme().border)
.rounded_md()
.rounded(rounding::CARD_RADIUS)
.overflow_hidden()
.child(
div()
.w_full()
.px_2p5()
.py_1p5()
.rounded_corners(header_corners)
.bg(cx.theme().secondary)
.text_xs()
.text_color(cx.theme().muted_foreground)
+1
View File
@@ -31,6 +31,7 @@ pub mod remote_connect;
pub mod remote_workspace;
pub mod reorder;
pub mod right_panel;
pub mod rounding;
pub mod scrollbar;
pub mod settings;
pub mod sftp;
+214
View File
@@ -0,0 +1,214 @@
//! Corner radii for children that paint a fill at the end of a rounded track.
//!
//! # The bug this exists to prevent (issue #236)
//!
//! gpui's `overflow_hidden` does **not** round-clip. [`gpui::ContentMask`] is a
//! plain axis-aligned `Bounds` with no corner radii (`Style::overflow_mask`
//! builds it from the element's bounds, shrunk by the border widths, and throws
//! `corner_radii` away), and every shader tests it as a hard per-fragment
//! `clip_distances < 0` discard. So the mask can only ever cut a square, and it
//! cuts it without a hint of anti-aliasing.
//!
//! A container's *own* rounded corners come from somewhere else entirely: the
//! quad shader's signed-distance field, whose `saturate(0.5 - distance)` gives
//! a clean one-device-pixel edge. That is why a plain rounded card looks
//! smooth. The moment a **child** paints a background into that same corner,
//! the two paths diverge:
//!
//! | | corner comes from | anti-aliased |
//! |---|---|---|
//! | the track's border/fill | quad SDF | yes |
//! | a child's fill in that corner | rectangular content mask | **no** |
//!
//! The child fills the whole square corner, the track's border arc floats
//! *inside* that square, and the child's outer edge is a hard vertical cut. It
//! reads exactly like the report: "anti-aliasing is insufficient".
//!
//! # The rule
//!
//! A child that lands in a corner has to carry its own radius, so its fill is
//! drawn by the SDF path too. It sits one border-width inside the track, so the
//! radius that nests concentrically is `outer - border` — see [`inner_radius`].
//!
//! The clip is still worth keeping as a backstop for content overflow; it just
//! can't be the thing that shapes a corner.
use gpui::{Corners, Pixels, Styled, px};
/// `rounded_*` one corner at a time is what gpui offers; this takes the whole
/// [`Corners`] the helpers below return, so a caller never has to spell out four
/// setters and risk transposing two of them.
pub(crate) trait RoundedCorners: Styled + Sized {
fn rounded_corners(self, corners: Corners<Pixels>) -> Self {
self.rounded_tl(corners.top_left)
.rounded_tr(corners.top_right)
.rounded_bl(corners.bottom_left)
.rounded_br(corners.bottom_right)
}
}
impl<T: Styled + Sized> RoundedCorners for T {}
/// gpui's `rounded_lg`, resolved. The Tailwind scale is in `rems`, and every
/// radius here has to do arithmetic against a `px` border width, so the tracks
/// this module serves state their radius in pixels. tty7 never calls
/// `set_rem_size`, so this is the same 8px `rounded_lg()` paints.
pub(crate) const TRACK_RADIUS: Pixels = px(8.);
/// gpui's `rounded_md`, resolved — see [`TRACK_RADIUS`].
pub(crate) const CARD_RADIUS: Pixels = px(6.);
/// The hairline every outlined track in the app draws (`border_1()`).
pub(crate) const HAIRLINE: Pixels = px(1.);
/// The radius a child needs so its fill follows the *inside* of a rounded,
/// bordered track instead of squaring off the corner it sits in.
///
/// The child's box starts one border-width in from the track's outer edge (that
/// is also where `Style::overflow_mask` puts the clip), so the concentric arc —
/// same centre, tighter by the border — has radius `outer - border`. Any larger
/// and the child bulges past the border and gets square-clipped again; any
/// smaller and a sliver of track shows between the border and the fill.
///
/// Clamped at zero: a border wider than the radius leaves a square corner,
/// which is what a concentric inset actually gives you there.
pub(crate) fn inner_radius(outer: Pixels, border: Pixels) -> Pixels {
let inset = outer - border;
if inset > px(0.) { inset } else { px(0.) }
}
/// Which corners segment `i` of `count` owns in a horizontal track.
///
/// The two end segments cap the track and take its rounding; everything between
/// them is square, because its corners are interior seams. A one-option track
/// is both ends at once, so it takes all four.
///
/// `count == 0` never renders a segment, but returning square corners keeps the
/// function total rather than making callers guard it.
pub(crate) fn segment_corners(
i: usize,
count: usize,
outer: Pixels,
border: Pixels,
) -> Corners<Pixels> {
let r = inner_radius(outer, border);
let zero = px(0.);
// An index past the end is not an end cap — `i < count` keeps a degenerate
// or out-of-range call square rather than rounding a corner that has no
// segment to draw it.
let first = i < count && i == 0;
let last = i < count && i + 1 == count;
Corners {
top_left: if first { r } else { zero },
bottom_left: if first { r } else { zero },
top_right: if last { r } else { zero },
bottom_right: if last { r } else { zero },
}
}
/// [`segment_corners`] for a vertical stack — a card whose children are bands
/// stacked top to bottom. The first band caps the top of the card, the last caps
/// the bottom; a lone band caps both.
pub(crate) fn stack_corners(
i: usize,
count: usize,
outer: Pixels,
border: Pixels,
) -> Corners<Pixels> {
let r = inner_radius(outer, border);
let zero = px(0.);
// An index past the end is not an end cap — `i < count` keeps a degenerate
// or out-of-range call square rather than rounding a corner that has no
// segment to draw it.
let first = i < count && i == 0;
let last = i < count && i + 1 == count;
Corners {
top_left: if first { r } else { zero },
top_right: if first { r } else { zero },
bottom_left: if last { r } else { zero },
bottom_right: if last { r } else { zero },
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The whole point: the child's arc is concentric with the border's inner
/// edge, so it is *strictly* tighter than the track's outer radius. A child
/// that reused the track's own radius would bulge past the border and hit
/// the square content mask again — the shape of issue #236.
#[test]
fn inner_radius_insets_by_the_border() {
assert_eq!(inner_radius(px(8.), px(1.)), px(7.));
assert_eq!(inner_radius(px(6.), px(1.)), px(5.));
assert!(inner_radius(TRACK_RADIUS, HAIRLINE) < TRACK_RADIUS);
assert!(inner_radius(CARD_RADIUS, HAIRLINE) < CARD_RADIUS);
}
/// A border at least as wide as the radius eats the curve; the inset must
/// bottom out at a square corner rather than going negative, which gpui
/// would carry straight into the shader.
#[test]
fn inner_radius_never_goes_negative() {
assert_eq!(inner_radius(px(1.), px(1.)), px(0.));
assert_eq!(inner_radius(px(2.), px(6.)), px(0.));
}
#[test]
fn end_segments_cap_the_track_and_the_middle_stays_square() {
let r = inner_radius(TRACK_RADIUS, HAIRLINE);
let zero = px(0.);
let first = segment_corners(0, 3, TRACK_RADIUS, HAIRLINE);
assert_eq!((first.top_left, first.bottom_left), (r, r));
assert_eq!((first.top_right, first.bottom_right), (zero, zero));
let middle = segment_corners(1, 3, TRACK_RADIUS, HAIRLINE);
assert_eq!(middle, Corners::all(zero));
let last = segment_corners(2, 3, TRACK_RADIUS, HAIRLINE);
assert_eq!((last.top_right, last.bottom_right), (r, r));
assert_eq!((last.top_left, last.bottom_left), (zero, zero));
}
/// One option is both ends of the track, so it has to round all four —
/// otherwise a single-segment control squares off both sides.
#[test]
fn a_lone_segment_takes_every_corner() {
let r = inner_radius(TRACK_RADIUS, HAIRLINE);
assert_eq!(
segment_corners(0, 1, TRACK_RADIUS, HAIRLINE),
Corners::all(r)
);
}
/// Total for the degenerate count rather than a panic: no segment renders,
/// so no corner is an end.
#[test]
fn an_empty_track_has_no_end_caps() {
assert_eq!(
segment_corners(0, 0, TRACK_RADIUS, HAIRLINE),
Corners::all(px(0.))
);
}
/// A stack caps top and bottom where a segmented track caps left and right —
/// same rule, rotated.
#[test]
fn a_stack_caps_its_first_and_last_band() {
let r = inner_radius(CARD_RADIUS, HAIRLINE);
let zero = px(0.);
let top = stack_corners(0, 2, CARD_RADIUS, HAIRLINE);
assert_eq!((top.top_left, top.top_right), (r, r));
assert_eq!((top.bottom_left, top.bottom_right), (zero, zero));
let bottom = stack_corners(1, 2, CARD_RADIUS, HAIRLINE);
assert_eq!((bottom.bottom_left, bottom.bottom_right), (r, r));
assert_eq!((bottom.top_left, bottom.top_right), (zero, zero));
// A collapsed card is one band, so its header owns every corner.
assert_eq!(stack_corners(0, 1, CARD_RADIUS, HAIRLINE), Corners::all(r));
}
}
+55 -15
View File
@@ -42,6 +42,8 @@ use crate::ui::app::{
};
use crate::ui::host_ops::HostId;
use crate::ui::presets;
use crate::ui::rounding;
use crate::ui::rounding::RoundedCorners as _;
/// Which section of the settings panel is currently selected in the sidebar.
/// Sections are named for the *object* being configured (the appearance, the
@@ -1385,10 +1387,11 @@ impl Tty7App {
// forward rules) can carry an id derived from their index.
let id: SharedString = id.into();
let on_pick = std::rc::Rc::new(on_pick);
let count = options.len();
h_flex()
.id(gpui::ElementId::Name(id.clone()))
.h(px(24.))
.rounded_lg()
.rounded(rounding::TRACK_RADIUS)
.border_1()
.border_color(border)
// The track paints its own ground rather than letting the sheet show
@@ -1397,12 +1400,21 @@ impl Tty7App {
// leaves its ground to whatever it happens to be composited over is
// the shape of the bug this whole change is about.
.bg(gpui::rgb(sf.base))
// One track, clipped so the end segments' fills follow its rounding
// instead of squaring off the corners they sit in.
// A backstop for content overflow, and nothing more. This used to be
// what shaped the end segments' fills to the track's rounding, and it
// cannot do that: gpui's overflow mask is a square, unantialiased
// scissor (issue #236, see `ui::rounding`). The segments carry their
// own radii below.
.overflow_hidden()
.children(options.iter().enumerate().map(|(i, label)| {
let active = i == selected;
let on_pick = on_pick.clone();
// The two end segments cap the track, so their fills have to draw
// the corner themselves — one border-width tighter than the
// track's own radius, so the arc nests inside the border instead
// of bulging past it into the square clip.
let corners =
rounding::segment_corners(i, count, rounding::TRACK_RADIUS, rounding::HAIRLINE);
h_flex()
// A per-segment id keeps each one unique across the several
// segmented controls on the page.
@@ -1413,6 +1425,7 @@ impl Tty7App {
.px_2p5()
.text_sm()
.cursor_pointer()
.rounded_corners(corners)
// Hairlines *between* segments only — the track already owns
// its outer edge, and a border on the first segment would
// double it.
@@ -1471,16 +1484,32 @@ impl Tty7App {
.any(|(tag, value)| tag == "liga" && *value != 0)
});
// Unified /value/+ stepper plus a quiet Reset.
let step = move |id: &'static str, glyph: &'static str, divider: bool| {
div()
// Unified /value/+ stepper plus a quiet Reset. `slot` is the glyph's
// place in the three-slot track (−│value│+): it draws the internal
// hairline, and — because a hover fill in an end slot would otherwise
// square off the track's corner (issue #236) — the corner radii.
//
// `h_full` rather than `py_1` is load-bearing for that second job. A
// padded, auto-height glyph box measures 31px (14px text × gpui's φ line
// height, plus 8px of padding) against a 22px content box, so the track's
// `items_center` centres it and `overflow_hidden` crops the overhang —
// the fill still reaches the corner, but the box's own rounded corner is
// 4½px outside the visible strip, where it does nothing. Pinning the box
// to the track's content height puts the arc back where the corner is.
let step = move |id: &'static str, glyph: &'static str, slot: usize| {
let corners =
rounding::segment_corners(slot, 3, rounding::TRACK_RADIUS, rounding::HAIRLINE);
h_flex()
.id(id)
.items_center()
.justify_center()
.h_full()
.px_2p5()
.py_1()
.text_sm()
.cursor_pointer()
.text_color(foreground)
.when(divider, |s| s.border_l_1().border_color(border))
.when(slot > 0, |s| s.border_l_1().border_color(border))
.rounded_corners(corners)
.hover(|h| h.bg(hover_bg))
.child(glyph)
};
@@ -1503,10 +1532,13 @@ impl Tty7App {
h_flex()
.items_center()
.h(control_h)
.rounded_lg()
.rounded(rounding::TRACK_RADIUS)
.bg(stepper_bg)
.border_1()
.border_color(border)
// Overflow backstop only — `step` rounds its own
// corners, because this clip is square (see
// `ui::rounding`).
.overflow_hidden()
.child(dec)
.child(
@@ -1527,11 +1559,11 @@ impl Tty7App {
.into_any_element()
};
let font_size_control = stepper_row(
step("font-dec", "", false).on_click(
step("font-dec", "", 0).on_click(
cx.listener(|this, _, _w, cx| this.change_font_size(-FONT_SIZE_STEP, cx)),
),
format!("{:.0}", font_size),
step("font-inc", "+", true)
step("font-inc", "+", 2)
.on_click(cx.listener(|this, _, _w, cx| this.change_font_size(FONT_SIZE_STEP, cx))),
Button::new("font-reset")
.label("Reset")
@@ -1542,11 +1574,11 @@ impl Tty7App {
let line_height = self.line_height;
let line_height_control = stepper_row(
step("lh-dec", "", false).on_click(
step("lh-dec", "", 0).on_click(
cx.listener(|this, _, _w, cx| this.change_line_height(-LINE_HEIGHT_STEP, cx)),
),
format!("{:.2}", line_height),
step("lh-inc", "+", true).on_click(
step("lh-inc", "+", 2).on_click(
cx.listener(|this, _, _w, cx| this.change_line_height(LINE_HEIGHT_STEP, cx)),
),
Button::new("lh-reset")
@@ -4940,7 +4972,15 @@ impl Tty7App {
}
let id = p.id.clone();
let is_active = active_id == id;
let preview = self.theme_preview(&p);
// Here the preview sits *flush* inside the card's border (the
// "Current theme" card pads it, so it keeps its own 8px there). Flush
// means its corner has to nest one hairline inside the card's, or it
// bulges past the border into the square overflow clip — the corner
// then reads as a hard step instead of an arc (issue #236).
let preview = self.theme_preview(&p).rounded(rounding::inner_radius(
rounding::TRACK_RADIUS,
rounding::HAIRLINE,
));
let click_id = id.clone();
list = list.child(
v_flex()
@@ -4954,7 +4994,7 @@ impl Tty7App {
// the search box above is sized explicitly.
div()
.w(px(268.))
.rounded_lg()
.rounded(rounding::TRACK_RADIUS)
.overflow_hidden()
.border_1()
.border_color(if is_active {