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

+186
View File
@@ -0,0 +1,186 @@
use std::{
hash::{DefaultHasher, Hash, Hasher},
ops::Range,
time::Duration,
};
use gpui::{
Animation, AnimationExt, App, ElementId, HighlightStyle, Hsla, IntoElement, ParentElement,
RenderOnce, SharedString, Styled, StyledText, Window, div, prelude::*, pulsating_between,
};
use unicode_segmentation::UnicodeSegmentation;
use crate::{
motion,
theme::{ActiveTheme, Mix, TextSize},
};
const SHIMMER: Duration = Duration::from_millis(1800);
const BLINK: Duration = Duration::from_millis(1100);
fn graphemes(text: &str) -> Vec<Range<usize>> {
text.grapheme_indices(true)
.map(|(start, grapheme)| start..start + grapheme.len())
.collect()
}
fn tinted(text: &SharedString, color: impl Fn(usize, usize) -> Hsla) -> StyledText {
let spans = graphemes(text);
let count = spans.len();
let runs: Vec<_> = spans
.into_iter()
.enumerate()
.map(|(ix, span)| {
let style = HighlightStyle {
color: Some(color(ix, count)),
..Default::default()
};
(span, style)
})
.collect();
StyledText::new(text.clone()).with_highlights(runs)
}
/// Reveals text one grapheme at a time, with a blinking caret.
#[derive(IntoElement)]
pub struct Typewriter {
id: ElementId,
text: SharedString,
per_second: f32,
}
impl Typewriter {
pub fn new(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
text: text.into(),
per_second: 32.0,
}
}
/// Graphemes per second.
pub fn speed(mut self, per_second: f32) -> Self {
assert!(per_second > 0.0, "typewriter speed must be positive");
self.per_second = per_second;
self
}
}
impl RenderOnce for Typewriter {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let text = self.text;
let spans = graphemes(&text);
let count = spans.len();
let full = Duration::from_secs_f32(count as f32 / self.per_second);
let reveal = motion::duration(full, cx);
let mut hasher = DefaultHasher::new();
text.hash(&mut hasher);
let theme = cx.theme();
let body = theme.text_size(TextSize::Base);
let caret = div().w(body * 0.15).h(body * 1.3).bg(theme.colors.fg);
let caret = if theme.reduced_motion {
caret.into_any_element()
} else {
caret
.with_animation(
"caret",
Animation::new(BLINK)
.repeat()
.with_easing(pulsating_between(0.15, 1.0)),
|caret, t| caret.opacity(t),
)
.into_any_element()
};
div()
.id(self.id)
.flex()
.items_center()
.gap_0p5()
.child(div().with_animation(
("typing", hasher.finish()),
Animation::new(reveal),
move |line, t| {
let shown = ((count as f32 * t).ceil() as usize).min(count);
let end = if shown == 0 { 0 } else { spans[shown - 1].end };
line.child(SharedString::from(text[..end].to_string()))
},
))
.child(caret)
}
}
/// Tonal ramp across the glyphs, ink to muted by default.
#[derive(IntoElement)]
pub struct GradientText {
text: SharedString,
ends: Option<(Hsla, Hsla)>,
}
impl GradientText {
pub fn new(text: impl Into<SharedString>) -> Self {
Self {
text: text.into(),
ends: None,
}
}
pub fn colors(mut self, from: impl Into<Hsla>, to: impl Into<Hsla>) -> Self {
self.ends = Some((from.into(), to.into()));
self
}
}
impl RenderOnce for GradientText {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let colors = &cx.theme().colors;
let (from, to) = self.ends.unwrap_or((colors.fg, colors.fg_subtle));
tinted(&self.text, |ix, count| {
from.mix(&to, ix as f32 / count.saturating_sub(1).max(1) as f32)
})
}
}
/// A band of light that sweeps the text while it waits.
#[derive(IntoElement)]
pub struct ShimmerText {
id: ElementId,
text: SharedString,
}
impl ShimmerText {
pub fn new(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
text: text.into(),
}
}
}
impl RenderOnce for ShimmerText {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let (dim, lit) = (theme.colors.fg_subtle, theme.colors.fg);
let text = self.text;
if theme.reduced_motion {
return div()
.id(self.id)
.text_color(dim)
.child(text)
.into_any_element();
}
div()
.id(self.id)
.with_animation(
"shimmer",
Animation::new(SHIMMER).repeat(),
move |line, t| {
let band = motion::lerp(-0.3, 1.3, t);
line.child(tinted(&text, |ix, count| {
let x = ix as f32 / count.saturating_sub(1).max(1) as f32;
dim.mix(&lit, (1.0 - (x - band).abs() / 0.25).max(0.0))
}))
},
)
.into_any_element()
}
}