use gpui::prelude::*;
use gpui::{
div, px, size, AnimationElement, App, Bounds, Context, Div, Entity, Hsla, IntoElement,
SharedString, Window, WindowBounds, WindowOptions,
};
use guise::prelude::*;
use guise::theme::{theme, ColorName, Theme};
const STEPS: [&str; 5] = [
"Run the test suite",
"Build the universal binary",
"Notarize and staple",
"Attach the DMG",
"Publish the release",
];
const PER_STEP: f32 = 900.0;
const TRACK: f32 = 280.0;
struct Checklist {
run: Entity<Animator>,
shipped: Entity<Presence>,
epoch: usize,
}
impl Checklist {
fn new(cx: &mut Context<Self>) -> Self {
let run = cx.new(|cx| {
Animator::new(
motion! {
duration: PER_STEP * STEPS.len() as f32;
ease: linear;
custom("step"): 0 => STEPS.len() as i32;
w: 0 => TRACK;
},
cx,
)
});
let shipped = cx.new(|cx| {
Presence::new(cx)
.kind(TransitionKind::SlideLeft)
.duration_ms(220)
.content(|_window, cx| {
let color = theme(cx).color(ColorName::Teal, 5).hsla();
div()
.px(px(9.0))
.py(px(3.0))
.rounded(px(999.0))
.bg(color)
.text_size(px(11.0))
.child("shipped")
.into_any_element()
})
});
cx.subscribe(&run, |this: &mut Self, _run, event: &AnimatorEvent, cx| {
let done = matches!(event, AnimatorEvent::Complete);
this
.shipped
.update(cx, |badge, cx| badge.set_open(done, cx));
})
.detach();
Checklist {
run,
shipped,
epoch: 0,
}
}
fn row(
&self,
index: usize,
reached: f32,
colors: (Hsla, Hsla, Hsla),
delay: f32,
) -> AnimationElement<Div> {
let (surface, accent, dimmed) = colors;
let done = reached >= index as f32 + 1.0;
let working = !done && reached > index as f32;
let dot = div()
.w(px(8.0))
.h(px(8.0))
.rounded(px(999.0))
.bg(if done || working { accent } else { dimmed })
.animate_when(
working,
("pulse", index),
motion! {
duration: 700;
ease: in_out sine;
repeat: forever;
alternate;
opacity: 1 => 0.3;
},
);
div()
.flex()
.items_center()
.gap(px(9.0))
.px(px(10.0))
.py(px(7.0))
.rounded(px(7.0))
.bg(surface)
.child(dot)
.child(SharedString::from(STEPS[index]))
.when(!done && !working, |el| el.text_color(dimmed))
.animate(
("row", self.epoch * STEPS.len() + index),
motion! {
enter: slide_left 18;
duration: 380;
ease: out back;
delay: delay;
},
)
}
}
impl Render for Checklist {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let t = theme(cx);
let body = t.body().hsla();
let surface = t.surface().hsla();
let raised = t.surface_hover().hsla();
let accent = t.color(ColorName::Teal, 5).hsla();
let text = t.text().hsla();
let dimmed = t.dimmed().hsla();
let frame = self.run.read(cx).frame(window);
let reached = frame.number_or(Prop::Custom("step"), 0.0);
let filled = frame.number_or(Prop::Width, 0.0);
let playing = self.run.read(cx).is_playing();
let rise = Stagger::new(70.0).start(120.0);
let rows = (0..STEPS.len())
.map(|index| {
self.row(
index,
reached,
(raised, accent, dimmed),
rise.at(index, STEPS.len()),
)
})
.collect::<Vec<_>>();
let bar = div()
.w(px(TRACK))
.h(px(6.0))
.rounded(px(999.0))
.bg(raised)
.child(div().h(px(6.0)).rounded(px(999.0)).bg(accent).w(px(filled)));
let controls = div()
.flex()
.gap(px(8.0))
.items_center()
.child(
Button::new("run", if playing { "Pause" } else { "Run" })
.variant(Variant::Filled)
.color(ColorName::Teal)
.on_click(cx.listener(|this, _ev, _window, cx| {
this.run.update(cx, |run, cx| run.toggle(cx));
})),
)
.child(
Button::new("rewind", "Rewind")
.variant(Variant::Light)
.on_click(cx.listener(|this, _ev, _window, cx| {
this.run.update(cx, |run, cx| run.stop(cx));
this
.shipped
.update(cx, |badge, cx| badge.set_open(false, cx));
})),
)
.child(
Button::new("replay", "Replay entrance")
.variant(Variant::Subtle)
.on_click(cx.listener(|this, _ev, _window, cx| {
this.epoch += 1;
cx.notify();
})),
)
.child(self.shipped.clone());
let panel = div()
.w(px(360.0))
.flex()
.flex_col()
.gap(px(10.0))
.p(px(18.0))
.rounded(px(12.0))
.bg(surface)
.text_color(text)
.child(Title::new("Release checklist").order(4))
.children(rows)
.child(bar)
.child(controls)
.animate(
("panel", self.epoch),
motion! {
enter: slide_up 20;
duration: 420;
ease: out back;
},
);
div()
.size_full()
.bg(body)
.flex()
.items_center()
.justify_center()
.child(panel)
}
}
fn main() {
gpui::Application::new().run(|cx: &mut App| {
Theme::dark().init(cx);
let bounds = Bounds::centered(None, size(px(640.0), px(560.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_window, cx| cx.new(Checklist::new),
)
.unwrap();
cx.activate(true);
});
}