Port · Parity: every page matches gpui 0.2.2's; the rules re-tested

Every page and scripted state was shot with gpui 0.2.2 and with the
port, light and dark, full width and at 280px, judged pair by pair by
agents with skeptics, and every page's log diffed. What differed is
fixed, each with a test that fails without it where the test platform
can show it:

- The gallery's harness takes posted mouse moves (gpui_macos listens
  to a tracking area), posts each drag move twice (it no longer
  replays a held drag), and waits three frames before each shot.
- Rolling digits: a block column, so taffy 0.13 lends the cell no
  baseline, and a line of whole device pixels, since gpui snaps each
  length on its own. DescriptionList's row aligns ends.
- Margins that taffy 0.13 collapses through a plain block: the
  drawer's pull and snap, the stagger and transition rise, the dialog
  and palette rises, the fullscreen dialog's slide.
- An Input keeps room to type in a squeezed row; the field rows drop
  w_full, whose reason was taffy 0.9's.

AGENTS.md's rules that named gpui 0.2.2 or taffy 0.9 were re-tested
against both sources and rewritten, and new ones say what the port
changed. TASKS.md's tags follow: what the new gpui makes possible is
prove. Captions, doc comments and README say what holds now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-29 10:39:55 -04:00
1 parent 41368430f5
commit d7272e88bd
44 files changed
+535 -138

No files matched your search

+1 -1
View File
@@ -60,7 +60,7 @@ impl RenderOnce for DescriptionList {
.flex()
.when(stacked, |row| row.flex_col().gap_0p5())
.when(!stacked, |row| {
row.flex_wrap().items_baseline().gap_x_4().gap_y_0p5()
row.flex_wrap().items_end().gap_x_4().gap_y_0p5()
})
.py_2()
.when(lined && ix < last, |row| {
+6 -4
View File
@@ -126,8 +126,10 @@ impl Statistic {
impl RenderOnce for Statistic {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let unit = |text: SharedString| {
let id = &self.id;
let unit = |part: &str, text: SharedString| {
div()
.debug_selector(|| format!("statistic-{part} {id}"))
.text_size(theme.text_size(TextSize::Md))
.text_color(theme.colors.fg_muted)
.child(text)
@@ -147,13 +149,13 @@ impl RenderOnce for Statistic {
.flex()
.items_baseline()
.gap_1()
.children(self.prefix.map(unit))
.children(self.prefix.map(|text| unit("prefix", text)))
.child(
AnimatedNumber::new(self.id, self.value)
AnimatedNumber::new(self.id.clone(), self.value)
.decimals(self.decimals)
.size(self.size),
)
.children(self.suffix.map(unit)),
.children(self.suffix.map(|text| unit("suffix", text))),
)
.children(self.trend)
}
+91 -8
View File
@@ -5,12 +5,16 @@ use std::{
};
use gpui::{
Bounds, Context, IntoElement, Modifiers, ParentElement, Pixels, Render, Styled, TestAppContext,
VisualTestContext, Window, div, point, px,
AnyElement, Bounds, Context, IntoElement, Modifiers, ParentElement, Pixels, Render, Styled,
TestAppContext, VisualTestContext, Window, div, point, px,
};
use super::{Carousel, DescriptionList, PropertyGrid, PropertyGroup};
use crate::{primitives::Measure, theme::Theme};
use super::{Badge, Carousel, DescriptionList, PropertyGrid, PropertyGroup, Statistic, Tone};
use crate::{
primitives::Measure,
theme::{TextSize, Theme},
typography::AnimatedNumber,
};
/// A property grid whose drawn height the test reads.
struct Inspector(Rc<Cell<Pixels>>);
@@ -69,15 +73,19 @@ fn a_property_group_folds_from_its_header_and_stays_folded(cx: &mut TestAppConte
}
/// A one-row description list, this wide, that reports where the list and its value sit.
struct Facts(Pixels, Rc<Cell<[Bounds<Pixels>; 2]>>);
struct Facts(Pixels, fn() -> AnyElement, Rc<Cell<[Bounds<Pixels>; 2]>>);
fn license() -> AnyElement {
"Apache-2.0".into_any_element()
}
impl Render for Facts {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let (list, value) = (self.1.clone(), self.1.clone());
let (list, value) = (self.2.clone(), self.2.clone());
let value = Measure::new("value", move |bounds, _, _| {
value.set([value.get()[0], bounds])
})
.child("Apache-2.0");
.child((self.1)());
div().w(self.0).child(
Measure::new("list", move |bounds, _, _| {
list.set([bounds, list.get()[1]])
@@ -93,7 +101,7 @@ fn a_value_drops_below_its_label_when_the_row_runs_short(cx: &mut TestAppContext
let mut place = |width: f32| {
let seen = Rc::new(Cell::new([Bounds::default(); 2]));
let shared = seen.clone();
let (_, cx) = cx.add_window_view(move |_, _| Facts(px(width), shared));
let (_, cx) = cx.add_window_view(move |_, _| Facts(px(width), license, shared));
settle(cx);
let [list, value] = seen.get();
(value.left() - list.left(), value.top() - list.top())
@@ -116,6 +124,81 @@ fn a_value_drops_below_its_label_when_the_row_runs_short(cx: &mut TestAppContext
);
}
#[gpui::test]
fn a_badge_value_rests_on_its_labels_line(cx: &mut TestAppContext) {
cx.update(Theme::init);
let mut foot = |value: fn() -> AnyElement| {
let seen = Rc::new(Cell::new([Bounds::default(); 2]));
let shared = seen.clone();
let (_, cx) = cx.add_window_view(move |_, _| Facts(px(400.0), value, shared));
settle(cx);
let [list, value] = seen.get();
value.bottom() - list.top()
};
let text = foot(license);
let badge = foot(|| {
Badge::new("Paid")
.tone(Tone::Success)
.dot()
.into_any_element()
});
assert_eq!(badge, text, "a badge ends where its label's line does");
}
/// A statistic with units on both sides.
struct Revenue;
impl Render for Revenue {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
Statistic::new("revenue", "Revenue", 48_210.0)
.prefix("$")
.suffix("ms")
}
}
#[gpui::test]
fn units_rest_on_the_bottom_of_a_statistics_number(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
Theme::update(cx, |theme| theme.reduced_motion = true);
});
let (_, cx) = cx.add_window_view(|_, _| Revenue);
settle(cx);
let number = cx
.debug_bounds("animated-number revenue")
.expect("the number");
for unit in ["statistic-prefix revenue", "statistic-suffix revenue"] {
let bounds = cx.debug_bounds(unit).expect("a unit");
assert_eq!(
bounds.bottom(),
number.bottom(),
"{unit} rests on the number's bottom"
);
}
}
/// A lone digit in extra-small text, whose line is no whole number of device pixels.
struct Nine;
impl Render for Nine {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
AnimatedNumber::new("nine", 9.0).size(TextSize::Xs)
}
}
#[gpui::test]
fn a_rolled_digit_sits_square_in_its_cell(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
Theme::update(cx, |theme| theme.reduced_motion = true);
});
let (_, cx) = cx.add_window_view(|_, _| Nine);
settle(cx);
let cell = cx.debug_bounds("rolling-cell").expect("the cell");
let nine = cx.debug_bounds("rolling-9").expect("the nine");
assert_eq!(nine.top(), cell.top(), "the nine fills its cell");
}
/// A three-slide carousel, 200 wide, that turns every five seconds; each slide reports where it sits.
struct Deck(Rc<RefCell<[Pixels; 3]>>);
+29 -2
View File
@@ -7,8 +7,8 @@ use gpui::{
use jiff::Timestamp;
use crate::finance::{
CandlestickChart, ChartSync, DepthChart, Leg, MarketHeatmap, PayoffDiagram, PointFigureChart,
RenkoChart, candles::Candle, stage::Visible,
CandlestickChart, ChartSync, CurrencyConverter, DepthChart, Leg, MarketHeatmap, PayoffDiagram,
PointFigureChart, RenkoChart, candles::Candle, stage::Visible,
};
use crate::{layout::tests::narrow_width, theme::Theme};
@@ -147,3 +147,30 @@ fn every_chart_fills_a_column_its_block_measures_by_content(cx: &mut TestAppCont
"each chart spans the card inside its padding"
);
}
/// A converter in a 460px box beside a card 260 wide, in a 280px row.
struct Squeezed;
impl Render for Squeezed {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let converter = CurrencyConverter::new("converter", 1000.0, ("USD", "EUR"), 0.9174);
div().w(px(280.)).child(
div()
.flex()
.gap_8()
.child(div().w(px(460.)).child(converter))
.child(div().flex_1().child(div().w(px(260.)).h(px(100.)))),
)
}
}
#[gpui::test]
fn a_squeezed_converter_keeps_room_for_its_currencies(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (_, cx) = cx.add_window_view(|_, _| Squeezed);
cx.run_until_parked();
let text = cx
.debug_bounds("input-text")
.expect("the second currency's text");
assert_eq!(text.size.width, px(26.), "the frame's inset for its name");
}
+1 -1
View File
@@ -95,9 +95,9 @@ impl RenderOnce for InputGroup {
.child(addon)
};
div()
.debug_selector(|| "group-root".into())
.flex()
.items_center()
.w_full()
.h(theme.control_height(self.size))
.overflow_hidden()
.rounded(theme.radius(Radius::Md))
+10 -2
View File
@@ -90,9 +90,9 @@ impl RenderOnce for Input {
};
let (state, clear_id) = (self.state.clone(), self.state.entity_id());
div()
.debug_selector(|| "input-root".into())
.flex()
.gap_2()
.w_full()
.px(theme.control_padding(self.size))
.map(|frame| {
if multi {
@@ -119,7 +119,15 @@ impl RenderOnce for Input {
window.focus(&focus, cx)
})
.children(self.prefix)
.child(div().flex_1().min_w_0().child(self.state))
.child(
div()
.debug_selector(|| "input-text".into())
.flex_1()
.min_w_0()
.child(self.state)
// An empty strut keeps the frame's inset to type in; squeezed, it spills unseen.
.child(div().px(theme.control_padding(self.size)).border_x_1()),
)
.when(self.clearable && !empty && !disabled, |frame| {
frame.child(
IconButton::new(("input-clear", clear_id), IconName::CircleX)
-1
View File
@@ -215,7 +215,6 @@ impl RenderOnce for TagInput {
.flex_wrap()
.items_center()
.gap_1()
.w_full()
.min_h(theme.control_height(ControlSize::Md))
.px_1()
.py_0p5()
+12 -3
View File
@@ -7,8 +7,8 @@ use super::setup;
use crate::{
buttons::Button,
forms::{
Choice, DatePicker, EmailInput, FieldArray, Form, FormField, Input, InputEvent,
MentionInput, NumberInput, Select, Slider, TextInput,
Choice, DatePicker, EmailInput, FieldArray, Form, FormField, Input, InputEvent, InputGroup,
MentionInput, NumberInput, Select, Slider, TagInput, TextInput,
},
layout::tests::narrow_width,
};
@@ -224,10 +224,19 @@ fn fields_fill_a_column_their_block_measures_by_content(cx: &mut TestAppContext)
narrow_width(cx, "number-root", |_, _| {
NumberInput::new("number", 42.0).into_any_element()
}),
narrow_width(cx, "input-root", move |window, cx| {
Input::new(&field(window, cx)).into_any_element()
}),
narrow_width(cx, "group-root", move |window, cx| {
InputGroup::new(&field(window, cx)).into_any_element()
}),
narrow_width(cx, "tag-input tags", |_, _| {
TagInput::new("tags", ["a"]).into_any_element()
}),
];
assert_eq!(
widths,
[px(240.0); 6],
[px(240.0); 9],
"each field spans the card inside its padding"
);
}
+1 -1
View File
@@ -28,7 +28,7 @@ fn angle_at(bounds: Bounds<Pixels>, at: Point<Pixels>, snap: bool) -> f32 {
type OnTurn = Rc<dyn Fn(f32, &mut Window, &mut App)>;
/// An svg turned to its angle, in degrees clockwise from straight up, inside a ring with a knob at that angle. A drag on the knob turns it, Shift snapping to 15°; with focus, Left and Right turn it by 15°. gpui 0.2.2 turns svgs alone, so the picture is an svg path.
/// An svg turned to its angle, in degrees clockwise from straight up, inside a ring with a knob at that angle. A drag on the knob turns it, Shift snapping to 15°; with focus, Left and Right turn it by 15°. gpui turns svgs alone, so the picture is an svg path.
#[derive(IntoElement)]
pub struct Rotatable {
id: ElementId,
+3 -2
View File
@@ -187,6 +187,7 @@ impl RenderOnce for Drawer {
let (close_up, close_out) = (close.clone(), close);
let panel = div()
.id("drawer-panel")
.debug_selector(|| "drawer-panel".into())
.relative()
.w_full()
.max_h(relative(0.85))
@@ -198,7 +199,7 @@ impl RenderOnce for Drawer {
.rounded_t(theme.radius(Radius::Xl))
.bg(theme.colors.overlay)
.shadow(theme.elevation(Elevation::Modal))
.mb(-offset)
.top(offset)
.on_drag_move(move |event: &DragMoveEvent<DrawerPull>, _, cx| {
let pull = event.drag(cx);
if pull.owner != owner {
@@ -282,7 +283,7 @@ impl RenderOnce for Drawer {
.with_animation(
("drawer-snap", snaps),
Animation::new(quick).with_easing(motion::ease_out_cubic),
move |wrap, t| wrap.mb(-(released * (1.0 - t))),
move |wrap, t| wrap.top(released * (1.0 - t)),
)
.into_any_element()
} else {
+96 -2
View File
@@ -1,8 +1,14 @@
use std::time::Duration;
use gpui::{
AnyElement, App, Context, IntoElement, ParentElement, Pixels, Render, Styled, TestAppContext,
Window, div, px,
AnyElement, App, Bounds, Context, InteractiveElement, IntoElement, Modifiers, MouseButton,
ParentElement, Pixels, Point, Render, Styled, TestAppContext, VisualTestContext, Window, div,
point, px,
};
use super::Drawer;
use crate::theme::Theme;
type Build = Box<dyn Fn(&mut Window, &mut App) -> AnyElement>;
/// One element in a column that a padded block measures by content.
@@ -32,3 +38,91 @@ pub(crate) fn narrow_width(
cx.run_until_parked();
cx.debug_bounds(selector).expect(selector).size.width
}
/// A drawer holding a body the test can find.
struct Pulled;
impl Render for Pulled {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(
Drawer::new("drawer", |_, _| {})
.child(div().debug_selector(|| "drawer-body".into()).h(px(120.0))),
)
}
}
fn body(cx: &mut VisualTestContext) -> Bounds<Pixels> {
cx.debug_bounds("drawer-body").expect("the body")
}
/// Opens a drawer and waits out its entrance.
fn opened(cx: &mut TestAppContext, reduced: bool) -> &mut VisualTestContext {
cx.update(|cx| {
Theme::init(cx);
Theme::update(cx, |theme| theme.reduced_motion = reduced);
});
let (_, cx) = cx.add_window_view(|_, _| Pulled);
cx.run_until_parked();
std::thread::sleep(Duration::from_millis(if reduced { 10 } else { 250 }));
cx.update(|window, _| window.refresh());
cx
}
/// Presses on the grab and drags it down by `dy`, past gpui's drag threshold.
fn pull(cx: &mut VisualTestContext, dy: f32) -> Point<Pixels> {
let rest = body(cx).origin;
let grab = point(rest.x + px(10.0), rest.y - px(28.0));
cx.simulate_mouse_down(grab, MouseButton::Left, Modifiers::none());
for step in [4.0, 8.0, dy] {
let at = point(grab.x, grab.y + px(step));
cx.simulate_mouse_move(at, MouseButton::Left, Modifiers::none());
}
point(grab.x, grab.y + px(dy))
}
#[gpui::test]
fn a_resting_drawer_reaches_the_bottom(cx: &mut TestAppContext) {
let cx = opened(cx, true);
let panel = cx.debug_bounds("drawer-panel").expect("the panel");
let bottom = cx.update(|window, _| window.viewport_size().height);
assert_eq!(
body(cx).size.height,
px(120.0),
"the panel's 0.85 cap leaves a resting body whole"
);
assert_eq!(
panel.bottom(),
bottom,
"the panel reaches the window's bottom"
);
}
#[gpui::test]
fn a_pulled_drawer_follows_the_pointer(cx: &mut TestAppContext) {
let cx = opened(cx, true);
let at = pull(cx, 8.0);
let held = body(cx).origin.y;
cx.simulate_mouse_move(
at + point(px(0.0), px(40.0)),
MouseButton::Left,
Modifiers::none(),
);
assert_eq!(
body(cx).origin.y - held,
px(40.0),
"the panel moves with the pull"
);
}
#[gpui::test]
fn a_released_drawer_glides_back(cx: &mut TestAppContext) {
let cx = opened(cx, false);
let rest = body(cx).origin.y;
let at = pull(cx, 40.0);
cx.simulate_mouse_up(at, MouseButton::Left, Modifiers::none());
let lift = body(cx).origin.y - rest;
assert!(
lift > px(20.0),
"the panel glides back from {lift:?} below rest"
);
}
+36
View File
@@ -402,3 +402,39 @@ fn effects_keep_their_content_and_let_presses_through(cx: &mut TestAppContext) {
settle(cx);
assert_eq!(view.read_with(cx, |effects, _| effects.presses), 1);
}
/// Rows rising in, in a sized box down a column that fills a row.
struct Rising;
impl Render for Rising {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let rows = ["Inbox", "Drafts", "Sent"].map(|name| {
div()
.debug_selector(move || name.into())
.px_4()
.py_2()
.child(name)
});
let column = div().flex().flex_col().child(
div()
.debug_selector(|| "risen".into())
.w(px(300.0))
.child(super::Stagger::new("rise").children(rows)),
);
div().size_full().flex().child(div().flex_1().child(column))
}
}
#[gpui::test]
fn a_stagger_box_takes_in_its_first_row_rise(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (_, cx) = cx.add_window_view(|_, _| Rising);
cx.run_until_parked();
let bounds = |cx: &mut VisualTestContext, name| cx.debug_bounds(name).expect(name);
let (held, first, last) = (bounds(cx, "risen"), bounds(cx, "Inbox"), bounds(cx, "Sent"));
assert!(
first.top() - held.top() > super::NUDGE,
"the first row starts risen"
);
assert_eq!(last.bottom(), held.bottom(), "the box ends with its rows");
}
+2 -1
View File
@@ -63,7 +63,8 @@ pub(super) fn arrive<E: Styled>(element: E, entrance: Entrance, t: f32) -> E {
let t = ease_out_cubic(t);
match entrance {
Entrance::Fade => element.opacity(t),
Entrance::Rise => element.opacity(t).mt(NUDGE * 2.0 * (1.0 - t)),
// Padding, as taffy's size cache drops a margin that collapses through a block.
Entrance::Rise => element.opacity(t).pt(NUDGE * 2.0 * (1.0 - t)),
Entrance::Slide => element.opacity(t).ml(NUDGE * -4.0 * (1.0 - t)),
}
}
+9 -8
View File
@@ -306,8 +306,9 @@ impl Palette {
}
let (up, down, escape) = (go.clone(), go, self.on_close);
let (enter, chosen) = (pick, cursor.clone());
div()
let card = div()
.id(self.id.clone())
.debug_selector(|| "palette-card".into())
.flex()
.flex_col()
.w(theme.palette_size().width)
@@ -381,12 +382,12 @@ impl Palette {
.child(self.empty),
)
}),
)
.with_animation(
(self.id, "in"),
Animation::new(motion::duration(motion::BASE, cx))
.with_easing(motion::ease_out_cubic),
|card, t| card.opacity(t).mt(motion::NUDGE * (1.0 - t)),
)
);
// Padding grows the box a scrim centers, as a margin taffy kept would.
div().child(card).with_animation(
(self.id, "in"),
Animation::new(motion::duration(motion::BASE, cx)).with_easing(motion::ease_out_cubic),
|card, t| card.opacity(t).pt(motion::NUDGE * (1.0 - t)),
)
}
}
+14
View File
@@ -269,3 +269,17 @@ fn enter_picks_on_release_so_the_opener_stays_shut(cx: &mut TestAppContext) {
press("enter", cx);
assert_eq!(got(&view, cx), (false, Some("zoom".into())));
}
#[gpui::test]
fn an_opening_palette_rises_in_the_box_its_scrim_centers(cx: &mut TestAppContext) {
let (_, cx) = host(Kind::Commands, cx);
let top = |cx: &mut VisualTestContext| cx.debug_bounds("palette-card").expect("the card").top();
let first = top(cx);
std::thread::sleep(std::time::Duration::from_millis(250));
settle(cx);
let lift = first - top(cx);
assert!(
lift > gpui::px(0.0) && lift < crate::motion::NUDGE * 0.75,
"the box grows with the rise, so the card starts half a nudge low: {lift:?}"
);
}
+14 -8
View File
@@ -229,16 +229,22 @@ impl RenderOnce for Dialog {
.children(body)
.children(actions)
};
let rise = if fullscreen {
motion::NUDGE * 4.0
let arrive =
Animation::new(motion::duration(motion::BASE, cx)).with_easing(motion::ease_out_cubic);
let card = if fullscreen {
card.with_animation((self.id.clone(), "in"), arrive, |card, t| {
card.opacity(t).top(motion::NUDGE * 4.0 * (1.0 - t))
})
.into_any_element()
} else {
motion::NUDGE
// Padding grows the box the scrim centers, as a margin taffy kept would.
div()
.child(card)
.with_animation((self.id.clone(), "in"), arrive, |card, t| {
card.opacity(t).pt(motion::NUDGE * (1.0 - t))
})
.into_any_element()
};
let card = card.with_animation(
(self.id.clone(), "in"),
Animation::new(motion::duration(motion::BASE, cx)).with_easing(motion::ease_out_cubic),
move |card, t| card.opacity(t).mt(rise * (1.0 - t)),
);
let layer = div()
.when(fullscreen, |layer| layer.size_full())
.on_key_down(move |event, window, cx| {
+28
View File
@@ -432,3 +432,31 @@ fn a_tall_dialog_body_keeps_its_height_and_scrolls(cx: &mut TestAppContext) {
fn a_popover_opened_by_its_owner_takes_no_icon() {
let _ = Popover::with_opener("own", |_| div(), |_, _, _| div()).icon(IconName::Clock);
}
/// A dialog open from the first frame, with a body the test can find.
struct Rising;
impl Render for Rising {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(
Dialog::new("rising", "Rising", |_, _| {})
.child(div().debug_selector(|| "rising-body".into()).h(px(40.0))),
)
}
}
#[gpui::test]
fn an_opening_dialog_rises_in_the_box_its_scrim_centers(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (_, cx) = cx.add_window_view(|_, _| Rising);
cx.run_until_parked();
let top = |cx: &mut VisualTestContext| cx.debug_bounds("rising-body").expect("the body").top();
let first = top(cx);
std::thread::sleep(Duration::from_millis(250));
settle(cx);
let lift = first - top(cx);
assert!(
lift > px(0.0) && lift < crate::motion::NUDGE * 0.75,
"the box grows with the rise, so the card starts half a nudge low: {lift:?}"
);
}
+1 -1
View File
@@ -33,7 +33,7 @@ pub(crate) fn framed(ratio: f32, cx: &App) -> Div {
frame
}
/// Themed `img()` that fills the box it is given; a `Cover` crop keeps square corners.
/// Themed `img()` that fills the box it is given; a `Cover` crop keeps its rounded corners.
#[derive(IntoElement)]
pub struct Image {
id: ElementId,
+1 -1
View File
@@ -16,7 +16,7 @@ fn nested() -> bool {
LAYERS.with_borrow(|layers| !layers.is_empty())
}
/// Draws `child` over the page, as gpui's `deferred` does. gpui cannot defer a draw inside a deferred one, so what is raised inside something raised is laid out and drawn last within it, under `id`: a list opened in a dialog lies over the dialog and keeps its state.
/// Draws `child` over the page, as gpui's `deferred` does. gpui paints deferred draws by priority alone, so what is raised inside something raised is laid out and drawn last within it, under `id`: a list opened in a dialog lies over the dialog and keeps its state.
pub fn raise(id: impl Into<ElementId>, child: impl IntoElement) -> Raised {
let layer = Layer {
child: child.into_any_element(),
+25
View File
@@ -314,6 +314,31 @@ fn removing_the_rule_being_typed_leaves_the_next_rule_its_own_value(cx: &mut Tes
);
}
/// One rule, Change is above, in a 280px column.
struct NarrowRule;
impl Render for NarrowRule {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let rule = FilterRule {
column: "change".into(),
test: super::Test::Above,
value: "".into(),
};
div()
.w(px(280.0))
.child(FilterBuilder::new("narrow", [("change", "Change")]).rules([rule], false))
}
}
#[gpui::test]
fn a_rule_keeps_room_for_its_value_in_a_narrow_column(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (_, cx) = cx.add_window_view(|_, _| NarrowRule);
settle(cx);
let text = cx.debug_bounds("input-text").expect("the value's text");
assert_eq!(text.size.width, px(18.0), "the frame's inset to type in");
}
/// A two-column grid whose row count the test sets.
struct Shrinking(Rc<Count<usize>>);
+1 -1
View File
@@ -111,7 +111,7 @@ fn offset(align: TextAlign, free: Pixels) -> Pixels {
}
}
/// One line in the text style around it, cut by `fit` to its box and set by its alignment. gpui 0.2.2 keeps a line's first measure, so its own `truncate` clips instead in flex boxes.
/// One line in the text style around it, cut by `fit` to its box and set by its alignment.
fn cut_line(text: SharedString, fit: fn(&str, Pixels, &mut Window) -> String) -> Div {
let line = text.clone();
div()
+12 -5
View File
@@ -124,8 +124,11 @@ impl RenderOnce for AnimatedNumber {
let duration = motion::duration(motion::SLOW * 2, cx);
let theme = cx.theme();
let size = theme.text_size(self.size);
let line = size.to_pixels(window.rem_size()) * LEADING;
let scale = window.scale_factor();
// gpui snaps rows and margin apart; keep whole device pixels.
let line = (size.to_pixels(window.rem_size()) * LEADING * scale).round() / scale;
let row = tabular(div())
.debug_selector(|| format!("animated-number {}", self.id))
.id(self.id)
.flex()
.h(line)
@@ -157,11 +160,15 @@ impl RenderOnce for AnimatedNumber {
.into_any_element();
};
let start = old.to_digit(10).unwrap_or(0);
let stack =
div().flex().flex_col().children((0..10).map(|digit| {
div().h(line).child(SharedString::from(digit.to_string()))
}));
// A block column lends the cell no baseline.
let stack = div().children((0..10).map(|digit| {
div()
.debug_selector(move || format!("rolling-{digit}"))
.h(line)
.child(SharedString::from(digit.to_string()))
}));
div()
.debug_selector(|| "rolling-cell".into())
.h(line)
.overflow_hidden()
.child(stack.with_animation(