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:
1 parent
41368430f5
commit
d7272e88bd
44 files changed
+535
-138
No files matched your search
@@ -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| {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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| {
|
||||
|
||||
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>>);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user