mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 16:02:24 +00:00
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:
@@ -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.
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user