T02 typography: text styles, keys, formatters, fit, links, math, motion text

Headings to overlines, Code, Kbd and KbdCombo on gpui keystrokes,
Highlight, MiddleEllipsis and EllipsisTooltip, links, eight formatted
values on jiff with a typographic minus, CopyableText, SelectableText,
Emoji, a TeX subset, rolling AnimatedNumber, Typewriter, tonal
GradientText, ShimmerText. Gallery probes now hug their targets.
Codex gpt-6-astra (max): PASS in round 2.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 01:12:52 -04:00
1 parent a13343c2a5
commit b35530c80d
30 files changed
+3125 -58

No files matched your search

+14 -5
View File
@@ -15,11 +15,11 @@ use crate::{
const SETTLE: Duration = Duration::from_millis(700);
const SCROLL_SETTLE: Duration = Duration::from_millis(250);
/// Shoots every page in both modes, top to bottom, then quits. Exits 1 on failure.
pub fn run(window: WindowHandle<Gallery>, dir: PathBuf, cx: &mut App) {
/// Shoots one page, or all, in both modes, then quits. Exits 1 on failure.
pub fn run(window: WindowHandle<Gallery>, dir: PathBuf, only: Option<usize>, cx: &mut App) {
Theme::update(cx, |theme| theme.reduced_motion = true);
cx.spawn(async move |cx| {
let outcome = shoot_all(window, &dir, cx).await;
let outcome = shoot_all(window, &dir, only, cx).await;
if let Err(error) = outcome.and_then(|()| cx.update(|cx| cx.quit())) {
log::error!("capture failed: {error:#}");
std::process::exit(1);
@@ -28,9 +28,18 @@ pub fn run(window: WindowHandle<Gallery>, dir: PathBuf, cx: &mut App) {
.detach();
}
async fn shoot_all(window: WindowHandle<Gallery>, dir: &Path, cx: &mut AsyncApp) -> Result<()> {
async fn shoot_all(
window: WindowHandle<Gallery>,
dir: &Path,
only: Option<usize>,
cx: &mut AsyncApp,
) -> Result<()> {
std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
for (ix, page) in pages::ALL.iter().enumerate() {
let chosen = pages::ALL
.iter()
.enumerate()
.filter(|(ix, _)| only.is_none_or(|only| only == *ix));
for (ix, page) in chosen {
for (choice, name) in [(Choice::Light, "light"), (Choice::Dark, "dark")] {
window.update(cx, |gallery, window, cx| {
gallery.select(ix, cx);
+5 -5
View File
@@ -17,13 +17,13 @@ use gpui::{
actions!(gallery, [Quit]);
struct Args {
page: usize,
page: Option<usize>,
capture: Option<PathBuf>,
}
fn parse_args() -> Result<Args> {
let mut args = Args {
page: 0,
page: None,
capture: None,
};
let mut iter = std::env::args().skip(1);
@@ -33,7 +33,7 @@ fn parse_args() -> Result<Args> {
.with_context(|| format!("{flag} needs a value"))?;
match flag.as_str() {
"--page" => {
args.page = pages::find(&value).with_context(|| format!("no page {value}"))?
args.page = Some(pages::find(&value).with_context(|| format!("no page {value}"))?)
}
"--capture" => args.capture = Some(PathBuf::from(value)),
other => bail!("unknown flag {other}"),
@@ -64,12 +64,12 @@ fn main() -> Result<()> {
};
let window = cx
.open_window(options, |window, cx| {
cx.new(|cx| shell::Gallery::new(args.page, window, cx))
cx.new(|cx| shell::Gallery::new(args.page.unwrap_or(0), window, cx))
})
.expect("gallery window failed to open");
cx.activate(true);
if let Some(dir) = args.capture {
capture::run(window, dir, cx);
capture::run(window, dir, args.page, cx);
}
});
Ok(())
+7 -1
View File
@@ -1,6 +1,7 @@
mod buttons;
mod primitives;
mod theme;
mod typography;
use gpui::{AnyElement, App, Window};
@@ -16,7 +17,12 @@ pub struct Page {
pub script: &'static [Step],
}
pub const ALL: &[Page] = &[primitives::PAGE, buttons::PAGE, theme::PAGE];
pub const ALL: &[Page] = &[
primitives::PAGE,
typography::PAGE,
buttons::PAGE,
theme::PAGE,
];
pub fn find(slug: &str) -> Option<usize> {
ALL.iter().position(|page| page.slug == slug)
+69
View File
@@ -0,0 +1,69 @@
mod motion;
mod text;
mod values;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 2,
slug: "typography",
title: "Typography",
summary: "Inter for words, tabular figures for numbers, one ink for everything.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::Rest,
Step::Hover("fit-tooltip"),
Step::Wait(700),
Step::Shot("ellipsis-tooltip"),
Step::Rest,
Step::ClickEnd("copy-token"),
Step::Shot("copied"),
Step::Click("select-text"),
Step::Key("cmd-a"),
Step::Shot("select-all"),
Step::Rest,
];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(text::headings(cx))
.child(text::title(cx))
.child(text::paragraph(cx))
.child(text::label(cx))
.child(text::caption(cx))
.child(text::overline(cx))
.child(text::inline_code(cx))
.child(text::kbd(cx))
.child(text::kbd_combo(cx))
.child(text::blockquote(cx))
.child(text::highlight(cx))
.child(text::truncate(cx))
.child(text::line_clamp(cx))
.child(text::middle(cx))
.child(text::ellipsis_tooltip(cx))
.child(text::link(cx))
.child(text::external_link(cx))
.child(values::relative_time(cx))
.child(values::date_time(cx))
.child(values::numbers(cx))
.child(values::currency(cx))
.child(values::percent(cx))
.child(values::file_size(cx))
.child(values::duration(cx))
.child(values::plural(cx))
.child(values::copyable(cx))
.child(values::selectable(cx))
.child(values::emoji(cx))
.child(values::latex(cx))
.child(motion::animated_number(window, cx))
.child(motion::typewriter(window, cx))
.child(motion::gradient(cx))
.child(motion::shimmer(cx))
.into_any_element()
}
+120
View File
@@ -0,0 +1,120 @@
use std::time::Duration;
use ely_gpui_component::{
buttons::{Button, ButtonVariant},
theme::{ActiveTheme, TextSize},
typography::{AnimatedNumber, GradientText, ShimmerText, Typewriter},
};
use gpui::{App, IntoElement, ParentElement, Styled, Task, Window, div};
use crate::ui::{row, section, specimen, specimens};
const STEP: Duration = Duration::from_millis(1600);
const VALUES: [f64; 5] = [1_284.0, 1_917.0, 12_406.0, 9_998.0, 10_031.0];
struct Ticker {
index: usize,
_task: Task<()>,
}
pub fn animated_number(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let ticker = window.use_keyed_state("type-ticker", cx, |window, cx| Ticker {
index: 0,
_task: cx.spawn_in(window, async move |ticker, cx| {
loop {
cx.background_executor().timer(STEP).await;
let advanced = ticker.update(cx, |ticker: &mut Ticker, cx| {
ticker.index = (ticker.index + 1) % VALUES.len();
cx.notify();
});
if advanced.is_err() {
return;
}
}
}),
});
let value = VALUES[ticker.read(cx).index];
section(
"AnimatedNumber",
"Digits roll by place; or the value counts up.",
cx,
)
.child(
specimens()
.gap_12()
.child(specimen("roll", AnimatedNumber::new("roll", value), cx))
.child(specimen(
"count up",
AnimatedNumber::new("count", value * 3.7)
.decimals(1)
.count_up(),
cx,
)),
)
}
pub fn typewriter(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let take = window.use_keyed_state("type-take", cx, |_, _| 0usize);
let lines = [
"Writing, one grapheme at a time.",
"中文与 emoji ✨ 也按字形逐个出现。",
];
let line = lines[*take.read(cx) % lines.len()];
section(
"Typewriter",
"Reveals by grapheme, so emoji and CJK stay whole.",
cx,
)
.child(
row()
.gap_6()
.child(div().min_w_80().child(Typewriter::new("typewriter", line)))
.child(
Button::new("type-again", "Next line")
.variant(ButtonVariant::Ghost)
.on_click(move |_, _, cx| {
take.update(cx, |take, cx| {
*take += 1;
cx.notify();
})
}),
),
)
}
pub fn gradient(cx: &App) -> impl IntoElement + use<> {
let colors = &cx.theme().colors;
let (ink, focus) = (colors.fg, colors.focus);
let display = cx.theme().text_size(TextSize::Display);
section("GradientText", "A tonal ramp, glyph by glyph. No neon.", cx).child(
specimens()
.gap_10()
.child(specimen(
"ink to muted",
div()
.text_size(display)
.child(GradientText::new("Quiet type")),
cx,
))
.child(specimen(
"ink to focus",
div()
.text_size(display)
.child(GradientText::new("Quiet type").colors(ink, focus)),
cx,
)),
)
}
pub fn shimmer(cx: &App) -> impl IntoElement + use<> {
section(
"ShimmerText",
"A slow band of light while something loads.",
cx,
)
.child(
div()
.text_size(cx.theme().text_size(TextSize::Lg))
.child(ShimmerText::new("shimmer", "Thinking through the request…")),
)
}
+220
View File
@@ -0,0 +1,220 @@
use ely_gpui_component::{
theme::{ActiveTheme, Radius},
typography::{
Blockquote, Caption, Code, EllipsisTooltip, ExternalLink, Heading, Highlight, Kbd,
KbdCombo, Label, Link, MiddleEllipsis, Overline, Paragraph, Subtitle, Title,
},
};
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
use crate::{
probe::probe,
ui::{code, row, section, specimen, specimens},
};
const PROSE: &str = "Good interfaces are quiet. They leave room around each idea, \
use one voice for text, and save color for the moments that need it. The rest is spacing.";
pub fn headings(cx: &App) -> impl IntoElement + use<> {
section(
"Heading (H1–H6)",
"Six steps, one weight. Size carries the order.",
cx,
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(Heading::h1("Quiet interfaces"))
.child(Heading::h2("Quiet interfaces"))
.child(Heading::h3("Quiet interfaces"))
.child(Heading::h4("Quiet interfaces"))
.child(Heading::h5("Quiet interfaces"))
.child(Heading::h6("Quiet interfaces")),
)
}
pub fn title(cx: &App) -> impl IntoElement + use<> {
section("Title / Subtitle", "A pair for page and panel headers.", cx).child(
div()
.flex()
.flex_col()
.gap_1()
.child(Title::new("Quarterly review"))
.child(Subtitle::new("Numbers, notes and the next three moves.")),
)
}
pub fn paragraph(cx: &App) -> impl IntoElement + use<> {
section("Paragraph", "Reading text at 14 with loose leading.", cx)
.child(Paragraph::new(PROSE).max_w(px(560.0)))
}
pub fn label(cx: &App) -> impl IntoElement + use<> {
section("Label", "Names a control or a value.", cx).child(
row()
.child(Label::new("Display name"))
.child(Label::new("Workspace")),
)
}
pub fn caption(cx: &App) -> impl IntoElement + use<> {
section("Caption", "The quiet line beneath.", cx)
.child(Caption::new("Updated a moment ago · visible to your team"))
}
pub fn overline(cx: &App) -> impl IntoElement + use<> {
section("Overline", "An eyebrow in small capitals.", cx).child(
div()
.flex()
.flex_col()
.gap_1()
.child(Overline::new("Release notes"))
.child(Title::new("Version 0.1")),
)
}
pub fn inline_code(cx: &App) -> impl IntoElement + use<> {
section("Code", "Monospace on a quiet fill.", cx).child(
row()
.child("Run")
.child(Code::new("cargo run --example gallery"))
.child("to open this window."),
)
}
pub fn kbd(cx: &App) -> impl IntoElement + use<> {
section(
"Kbd",
"gpui key syntax. secondary means ⌘ on macOS and Ctrl elsewhere.",
cx,
)
.child(
specimens()
.child(specimen("secondary-s", Kbd::new("secondary-s"), cx))
.child(specimen("shift-tab", Kbd::new("shift-tab"), cx))
.child(specimen("escape", Kbd::new("escape"), cx))
.child(specimen("enter", Kbd::new("enter"), cx)),
)
}
pub fn kbd_combo(cx: &App) -> impl IntoElement + use<> {
section("KbdCombo", "Sequences read left to right.", cx).child(
specimens()
.child(specimen(
"secondary-shift-p",
KbdCombo::new("secondary-shift-p"),
cx,
))
.child(specimen(
"secondary-k secondary-s",
KbdCombo::new("secondary-k secondary-s"),
cx,
)),
)
}
pub fn blockquote(cx: &App) -> impl IntoElement + use<> {
section("Blockquote", "A rule and a softer ink.", cx).child(div().max_w(px(520.0)).child(
Blockquote::new().child(
"Simplicity is not the absence of clutter. It is the right thing, in the right place.",
),
))
}
pub fn highlight(cx: &App) -> impl IntoElement + use<> {
section(
"Highlight / Mark",
"Every match of a query, case-insensitive.",
cx,
)
.child(Highlight::matching(
"Quiet interfaces age well. Loud ones AGE fast.",
"age",
))
.child(code("Highlight::matching(text, \"age\")", cx))
}
fn narrow(cx: &App) -> gpui::Div {
let theme = cx.theme();
div()
.w(px(240.0))
.px_3()
.py_2()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(theme.colors.border)
}
pub fn truncate(cx: &App) -> impl IntoElement + use<> {
section(
"Truncate",
"gpui's .truncate(): one line, an ellipsis at the end.",
cx,
)
.child(narrow(cx).child(div().truncate().child(PROSE)))
}
pub fn line_clamp(cx: &App) -> impl IntoElement + use<> {
section(
"LineClamp",
"gpui's .line_clamp(n): n lines, then a hard cut.",
cx,
)
.child(
narrow(cx)
.w(px(360.0))
.child(div().line_clamp(2).child(PROSE)),
)
}
pub fn middle(cx: &App) -> impl IntoElement + use<> {
section(
"MiddleEllipsis",
"Cuts the middle so both ends stay legible.",
cx,
)
.child(narrow(cx).w(px(280.0)).child(MiddleEllipsis::new(
"~/Documents/GitHub/Ely-GPUI-Components/src/typography/fit.rs",
)))
}
pub fn ellipsis_tooltip(cx: &App) -> impl IntoElement + use<> {
section(
"EllipsisTooltip",
"Hover shows the whole line, only when it was cut.",
cx,
)
.child(probe(
"fit-tooltip",
narrow(cx).child(EllipsisTooltip::new(
"fit-tooltip-text",
"Design review · Tuesday, with the full product team and guests",
)),
))
}
pub fn link(cx: &App) -> impl IntoElement + use<> {
section(
"Link",
"Blue marks where a click goes. Underline on hover.",
cx,
)
.child(
row()
.child("Read the")
.child(
Link::new("guide", "style guide").on_click(|_, _, _| log::info!("gallery: link")),
)
.child("before you start."),
)
}
pub fn external_link(cx: &App) -> impl IntoElement + use<> {
section("ExternalLink", "Leaves the app. The arrow says so.", cx).child(ExternalLink::new(
"gpui",
"gpui.rs",
"https://www.gpui.rs",
))
}
+226
View File
@@ -0,0 +1,226 @@
use ely_gpui_component::{
theme::TextSize,
typography::{
CopyableText, CurrencyText, DateTimeText, DurationStyle, DurationText, Emoji, FileSizeText,
Latex, NumberText, PercentText, PluralText, RelativeTime, SelectableText,
},
};
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
use jiff::{SignedDuration, Timestamp};
use crate::{
probe::probe,
ui::{code, section, specimen, specimens},
};
/// Fixed moment, so shots repeat: 24 Sep 2026, 09:41 UTC.
fn moment() -> Timestamp {
Timestamp::from_second(1_790_242_860).expect("valid timestamp")
}
pub fn relative_time(cx: &App) -> impl IntoElement + use<> {
let ago = |seconds: i64| {
Timestamp::now()
.checked_sub(SignedDuration::from_secs(seconds))
.expect("in range")
};
section(
"RelativeTime",
"Refreshes every thirty seconds while on screen.",
cx,
)
.child(
specimens()
.child(specimen("10 s", RelativeTime::new("rel-a", ago(10)), cx))
.child(specimen("3 min", RelativeTime::new("rel-b", ago(180)), cx))
.child(specimen("2 h", RelativeTime::new("rel-c", ago(7_200)), cx))
.child(specimen(
"3 d",
RelativeTime::new("rel-d", ago(259_200)),
cx,
)),
)
}
pub fn date_time(cx: &App) -> impl IntoElement + use<> {
section("DateTimeText", "strftime patterns in the system zone.", cx).child(
specimens()
.child(specimen("default", DateTimeText::new(moment()), cx))
.child(specimen(
"%Y-%m-%d",
DateTimeText::new(moment()).pattern("%Y-%m-%d"),
cx,
))
.child(specimen(
"%A %H:%M",
DateTimeText::new(moment()).pattern("%A %H:%M"),
cx,
)),
)
}
pub fn numbers(cx: &App) -> impl IntoElement + use<> {
section(
"NumberText",
"Grouped, fixed precision, tabular figures.",
cx,
)
.child(
specimens()
.child(specimen(
"2 decimals",
NumberText::new(1_234_567.891).decimals(2),
cx,
))
.child(specimen("integer", NumberText::new(42_000.0), cx))
.child(specimen(
"negative",
NumberText::new(-1_234.5).decimals(1),
cx,
)),
)
}
pub fn currency(cx: &App) -> impl IntoElement + use<> {
section(
"CurrencyText",
"ISO 4217 codes. Yen and won have no minor units.",
cx,
)
.child(
specimens()
.child(specimen("USD", CurrencyText::new(1_234.5, "USD"), cx))
.child(specimen("EUR", CurrencyText::new(-42.0, "EUR"), cx))
.child(specimen("JPY", CurrencyText::new(1_500.4, "JPY"), cx))
.child(specimen("CHF", CurrencyText::new(9.99, "CHF"), cx)),
)
}
pub fn percent(cx: &App) -> impl IntoElement + use<> {
section(
"PercentText",
"Signed values carry direction in word and color.",
cx,
)
.child(
specimens()
.child(specimen("plain", PercentText::new(0.1234).decimals(2), cx))
.child(specimen("gain", PercentText::new(0.052).signed(), cx))
.child(specimen("loss", PercentText::new(-0.031).signed(), cx)),
)
}
pub fn file_size(cx: &App) -> impl IntoElement + use<> {
section(
"FileSizeText",
"Decimal units by default, binary on request.",
cx,
)
.child(
specimens()
.child(specimen("1536 B", FileSizeText::new(1_536), cx))
.child(specimen("3.2 GB", FileSizeText::new(3_200_000_000), cx))
.child(specimen("binary", FileSizeText::new(3 << 30).binary(), cx)),
)
}
pub fn duration(cx: &App) -> impl IntoElement + use<> {
section(
"DurationText",
"A clock for media, a compact span for logs.",
cx,
)
.child(
specimens()
.child(specimen(
"clock",
DurationText::new(5_025, DurationStyle::Clock),
cx,
))
.child(specimen(
"clock",
DurationText::new(1_425, DurationStyle::Clock),
cx,
))
.child(specimen(
"compact",
DurationText::new(245, DurationStyle::Compact),
cx,
)),
)
}
pub fn plural(cx: &App) -> impl IntoElement + use<> {
section("PluralText", "The noun follows the count.", cx).child(
specimens()
.child(specimen("1", PluralText::new(1, "file", "files"), cx))
.child(specimen(
"12345",
PluralText::new(12_345, "file", "files"),
cx,
)),
)
}
pub fn copyable(cx: &App) -> impl IntoElement + use<> {
section(
"CopyableText",
"One click copies. A check confirms, then leaves.",
cx,
)
.child(probe(
"copy-token",
CopyableText::new("token", "ely_live_4f9a2c81d7").mono(),
))
}
pub fn selectable(cx: &App) -> impl IntoElement + use<> {
section(
"SelectableText",
"Drag to select. ⌘C copies, ⌘A takes it all.",
cx,
)
.child(probe(
"select-text",
div().max_w(px(520.0)).child(SelectableText::new(
"selectable",
"Selection works on read-only text too: logs, messages, receipts.",
)),
))
}
pub fn emoji(cx: &App) -> impl IntoElement + use<> {
let glyph = |code: &str| Emoji::shortcode(code).expect("known shortcode");
section("Emoji", "Shortcodes resolve to the system color font.", cx).child(
specimens()
.child(specimen("sparkles", glyph("sparkles"), cx))
.child(specimen("herb", glyph("herb"), cx))
.child(specimen("coffee", glyph("coffee"), cx))
.child(specimen("xl", glyph("sparkles").size(TextSize::Xxl), cx)),
)
}
pub fn latex(cx: &App) -> impl IntoElement + use<> {
let math = |source: &str| Latex::parse(source).expect("gallery formula parses");
section(
"Latex / MathInline",
"A TeX subset: scripts, fractions, roots, Greek.",
cx,
)
.child(
specimens()
.gap_10()
.child(specimen("euler", math(r"e^{i\pi} + 1 = 0"), cx))
.child(specimen(
"sum",
math(r"\sum_{k=1}^{n} k = \frac{n(n+1)}{2}"),
cx,
))
.child(specimen(
"root",
math(r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"),
cx,
)),
)
.child(code("Latex::parse(r\"e^{i\\pi} + 1 = 0\")?", cx))
}
+9 -7
View File
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use ely_gpui_component::primitives::Measure;
use gpui::{App, Bounds, Global, IntoElement, ParentElement, Pixels};
use gpui::{App, Bounds, Div, Global, IntoElement, ParentElement, Pixels, Styled, div};
/// Window bounds of named demo elements, for scripted capture.
#[derive(Default)]
@@ -16,10 +16,12 @@ impl Probes {
}
}
/// Wraps `element` so scripts can find it by `key`.
pub fn probe(key: &'static str, element: impl IntoElement) -> Measure {
Measure::new(key, move |bounds, _, cx| {
cx.default_global::<Probes>().0.insert(key, bounds);
})
.child(element)
/// Wraps `element`, sized to it, so scripts can find it by `key`.
pub fn probe(key: &'static str, element: impl IntoElement) -> Div {
div().flex().child(
Measure::new(key, move |bounds, _, cx| {
cx.default_global::<Probes>().0.insert(key, bounds);
})
.child(element),
)
}
+20 -5
View File
@@ -1,7 +1,7 @@
use std::{path::PathBuf, time::Duration};
use anyhow::{Context as _, Result};
use gpui::{AsyncApp, Keystroke, Pixels, Point, WindowHandle, point, px};
use gpui::{AsyncApp, Bounds, Keystroke, Pixels, Point, WindowHandle, point, px};
use crate::{capture::snapshot, probe::Probes, shell::Gallery};
@@ -12,6 +12,8 @@ pub enum Step {
Down(&'static str),
Up(&'static str),
Click(&'static str),
/// Clicks just inside the probe's right edge.
ClickEnd(&'static str),
Key(&'static str),
Wait(u64),
Shot(&'static str),
@@ -59,6 +61,13 @@ pub async fn play(
send(window, Mouse::Down, at, cx)?;
send(window, Mouse::Up, at, cx)?;
}
Step::ClickEnd(key) => {
let bounds = target_bounds(window, key, cx).await?;
let at = point(bounds.right() - px(12.0), bounds.center().y);
send(window, Mouse::Move, at, cx)?;
send(window, Mouse::Down, at, cx)?;
send(window, Mouse::Up, at, cx)?;
}
Step::Key(stroke) => {
let stroke =
Keystroke::parse(stroke).with_context(|| format!("bad keystroke {stroke}"))?;
@@ -135,14 +144,20 @@ async fn target(
key: &str,
cx: &mut AsyncApp,
) -> Result<Point<Pixels>> {
Ok(target_bounds(window, key, cx).await?.center())
}
async fn target_bounds(
window: WindowHandle<Gallery>,
key: &str,
cx: &mut AsyncApp,
) -> Result<Bounds<Pixels>> {
let bounds = cx
.update(|cx| Probes::get(key, cx))?
.with_context(|| format!("probe {key} never rendered"))?;
if window.update(cx, |gallery, _, cx| gallery.reveal(bounds, cx))? {
cx.background_executor().timer(FRAME).await;
}
let bounds = cx
.update(|cx| Probes::get(key, cx))?
.with_context(|| format!("probe {key} vanished after scrolling"))?;
Ok(bounds.center())
cx.update(|cx| Probes::get(key, cx))?
.with_context(|| format!("probe {key} vanished after scrolling"))
}