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:
30 files changed
+3125
-58
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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…")),
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
Reference in new issue
Block a user