use gpui::{
App, Bounds, Context, FocusHandle, KeyBinding, MouseButton, Window, WindowBounds,
WindowOptions, actions, div, prelude::*, px, rgb, size,
};
use gpui_ce_motion::{
Animation, Easing, Keyframe, NumberEffect, NumberMotionState, Repeat, RepeatMode, Scramble,
StaggerFrom, Target, TextMatch, TextMotionState, TextPreset, TextTarget, TextUnit, Transition,
};
use std::time::Duration;
actions!(
ce_text,
[
NextEffect,
ChangeText,
Replay,
TogglePause,
ToggleReduced,
Increase,
Decrease,
Quit
]
);
const MODES: [&str; 12] = [
"Fade",
"Words",
"Graphemes",
"Match",
"Lines",
"Wipe",
"Typewriter",
"Scramble",
"Alternate",
"Wave",
"Scrub 50%",
"Numbers",
];
const TEXT: [&str; 4] = [
"Made to move.",
"Words find their place.",
"A little more room to breathe.",
"Café 👩🏽💻 — مرحباً بالعالم",
];
struct Palette;
impl Palette {
fn background() -> gpui::Rgba {
rgb(0x18181b)
}
fn foreground() -> gpui::Rgba {
rgb(0xf4f4f5)
}
fn surface() -> gpui::Rgba {
rgb(0x27272a)
}
}
struct Demo {
motion: TextMotionState,
number: NumberMotionState,
sample: usize,
mode: usize,
paused: bool,
count: i64,
focus: FocusHandle,
wave: Animation,
scramble: Scramble,
}
impl Demo {
fn switch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.mode = (self.mode + 1) % MODES.len();
self.motion.restart(window, cx);
cx.notify();
}
fn replay(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.motion.restart(window, cx);
self.number.text_state().restart(window, cx);
cx.notify();
}
}
impl Render for Demo {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let text = self
.motion
.element("ce-text", TEXT[self.sample])
.when(cfg!(target_os = "macos"), |text| {
text.font_family("Helvetica Neue")
})
.text_size(px(48.));
let content = match self.mode {
0 => text.preset(TextPreset::Fade).into_any_element(),
1 => text
.units(TextUnit::Words)
.preset(TextPreset::FadeUp)
.stagger(Duration::from_millis(40), StaggerFrom::Start)
.into_any_element(),
2 => text
.units(TextUnit::Graphemes)
.preset(TextPreset::FadeUp)
.stagger(Duration::from_millis(15), StaggerFrom::Center)
.into_any_element(),
3 => text
.units(TextUnit::Graphemes)
.matching(TextMatch::Content)
.reflow(Transition::default())
.into_any_element(),
4 => text
.w(px(380.))
.units(TextUnit::Lines)
.preset(TextPreset::SlideUp)
.into_any_element(),
5 => text
.units(TextUnit::Words)
.preset(TextPreset::WipeLeft)
.transition(Transition::Tween(
Duration::from_millis(300),
Easing::EaseOut,
))
.into_any_element(),
6 => text
.units(TextUnit::Graphemes)
.preset(TextPreset::Typewriter)
.stagger(Duration::from_millis(30), StaggerFrom::Start)
.into_any_element(),
7 => text
.units(TextUnit::Graphemes)
.scramble(self.scramble.clone())
.into_any_element(),
8 => text
.units(TextUnit::Words)
.enter_with(|unit| {
TextTarget::default()
.y_em(if unit.index % 2 == 0 { -0.7 } else { 0.7 })
.opacity(0.)
})
.into_any_element(),
9 => text
.units(TextUnit::Graphemes)
.animation(&self.wave)
.stagger(Duration::from_millis(35), StaggerFrom::Start)
.into_any_element(),
10 => text
.units(TextUnit::Words)
.preset(TextPreset::FadeUp)
.transition(Transition::Tween(
Duration::from_millis(300),
Easing::Linear,
))
.progress(0.5)
.into_any_element(),
_ => self
.number
.element("ce-count", self.count)
.effect(NumberEffect::Roll)
.min_digits(3)
.when(cfg!(target_os = "macos"), |number| {
number.font_family("Helvetica Neue")
})
.text_size(px(72.))
.into_any_element(),
};
div()
.track_focus(&self.focus)
.size_full()
.flex()
.flex_col()
.gap_6()
.p_8()
.bg(Palette::background())
.text_color(Palette::foreground())
.on_action(cx.listener(|this, _: &NextEffect, window, cx| this.switch(window, cx)))
.on_action(cx.listener(|this, _: &ChangeText, _, cx| {
this.sample = (this.sample + 1) % TEXT.len();
cx.notify();
}))
.on_action(cx.listener(|this, _: &Replay, window, cx| this.replay(window, cx)))
.on_action(cx.listener(|this, _: &TogglePause, window, cx| {
this.paused = !this.paused;
if this.paused {
this.motion.pause(window, cx);
} else {
this.motion.play(window, cx);
}
cx.notify();
}))
.on_action(cx.listener(|_, _: &ToggleReduced, _, cx| {
let reduced = !cx.reduce_motion();
cx.set_reduce_motion(reduced);
cx.notify();
}))
.on_action(cx.listener(|this, _: &Increase, _, cx| {
this.count = this.count.saturating_add(101);
cx.notify();
}))
.on_action(cx.listener(|this, _: &Decrease, _, cx| {
this.count = this.count.saturating_sub(101);
cx.notify();
}))
.child(div().text_2xl().child("Native text motion"))
.child(format!(
"{:02} / 12 · {} · Reduced motion {}",
self.mode + 1,
MODES[self.mode],
cx.reduce_motion()
))
.child(
div()
.flex_1()
.min_h_0()
.flex()
.items_center()
.justify_center()
.rounded_lg()
.bg(Palette::surface())
.overflow_hidden()
.p_6()
.child(content),
)
.child(
div()
.flex()
.gap_4()
.child(
div()
.id("next-effect")
.aria_label("Next text effect")
.px_4()
.py_2()
.rounded_md()
.bg(Palette::surface())
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, window, cx| this.switch(window, cx)),
)
.child("Next effect · E"),
)
.child(
div()
.id("change-text")
.aria_label("Change text")
.px_4()
.py_2()
.rounded_md()
.bg(Palette::surface())
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, _, cx| {
this.sample = (this.sample + 1) % TEXT.len();
cx.notify();
}),
)
.child("Change text · Space"),
)
.child(
div()
.id("replay")
.aria_label("Replay text motion")
.px_4()
.py_2()
.rounded_md()
.bg(Palette::surface())
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, window, cx| this.replay(window, cx)),
)
.child("Replay · R"),
),
)
.child(
div()
.text_sm()
.child("P pause/resume · M reduced motion · ↑ ↓ count · Command+Q quit"),
)
}
}
fn main() {
gpui_platform::application().run(|cx: &mut App| {
cx.bind_keys([
KeyBinding::new("e", NextEffect, None),
KeyBinding::new("space", ChangeText, None),
KeyBinding::new("r", Replay, None),
KeyBinding::new("p", TogglePause, None),
KeyBinding::new("m", ToggleReduced, None),
KeyBinding::new("up", Increase, None),
KeyBinding::new("down", Decrease, None),
KeyBinding::new("cmd-q", Quit, None),
KeyBinding::new("ctrl-q", Quit, None),
]);
cx.on_action(|_: &Quit, cx| cx.quit());
cx.on_window_closed(|cx, _| {
if cx.windows().is_empty() {
cx.quit();
}
})
.detach();
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(px(1080.), px(680.)),
cx,
))),
titlebar: Some(gpui::TitlebarOptions {
title: Some("CE Text Motion".into()),
..Default::default()
}),
..Default::default()
},
|window, cx| {
cx.new(|cx| {
let focus = cx.focus_handle();
focus.focus(window, cx);
let wave = Animation::keyframes(
Duration::from_millis(700),
[
Keyframe::new(0., Target::default()),
Keyframe::new(0.5, Target::default().y(-8.)),
Keyframe::new(1., Target::default()),
],
)
.expect("valid keyframes")
.repeat(Repeat::Forever, RepeatMode::Loop, Duration::ZERO)
.expect("positive wave period");
Demo {
motion: TextMotionState::default(),
number: NumberMotionState::default(),
sample: 0,
mode: 0,
paused: false,
count: 199,
focus,
wave,
scramble: Scramble::new("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789").seed(42),
}
})
},
)
.expect("open CE text playground");
});
}