use gpui::{App, IntoElement, ParentElement, SharedString, Styled, div, px};
use motion::{self, GRADIENT_SPIN, ORB, PULSE, PULSE_STAGGER, Painter};
use theme::{TextStyle, Theme, Typeset};
pub use motion::phase::{GSPIN_DIM, GSPIN_ROW_TINTS, MATRIX_SIDE, PULSE_CELLS};
pub fn pulse_loader(
_id: &'static str,
theme: &Theme,
cell_px: f32,
painter: Painter,
cx: &mut App,
) -> impl IntoElement {
let color = theme.text;
let slot = cell_px;
let delta = motion::pulse_delta(&PULSE, painter, cx);
div()
.flex()
.flex_row()
.items_center()
.gap(px(slot / 2.0))
.children((0..PULSE_CELLS).map(move |i| {
div()
.size(px(slot))
.flex()
.items_center()
.justify_center()
.child({
let phase = motion::staggered_phase(delta, i, PULSE_STAGGER);
div()
.rounded(px(slot / 4.0))
.bg(color)
.opacity(motion::pulse_opacity(phase))
.size(px(slot * motion::pulse_scale(phase)))
})
}))
}
pub fn gradient_spinner(
_id: &'static str,
_theme: &Theme,
cell_px: f32,
painter: Painter,
cx: &mut App,
) -> impl IntoElement {
let center = (MATRIX_SIDE as f32 - 1.0) / 2.0;
let max = MATRIX_SIDE as f32 - 1.0 + center;
let delta = motion::pulse_delta(&GRADIENT_SPIN, painter, cx);
div()
.flex()
.flex_col()
.gap(px(cell_px / 2.0))
.children((0..MATRIX_SIDE).map(move |row| {
let tint: gpui::Hsla = gpui::rgb(GSPIN_ROW_TINTS[row]).into();
div()
.flex()
.flex_row()
.gap(px(cell_px / 2.0))
.children((0..MATRIX_SIDE).map(move |col| {
let d = MATRIX_SIDE as f32 - 1.0 - row as f32 + (col as f32 - center).abs();
let phase = if max == 0.0 { 0.0 } else { d / (max + 1.0) };
div()
.size(px(cell_px))
.rounded(px(cell_px / 2.0))
.bg(tint)
.opacity(motion::gspin_opacity(delta + phase, GSPIN_DIM))
}))
}))
}
pub fn mini_gradient_spinner(
key: impl Into<SharedString>,
cell_px: f32,
painter: Painter,
cx: &mut App,
) -> impl IntoElement {
const COLS: usize = 2;
const ROWS: usize = 3;
const RING: [[usize; COLS]; ROWS] = [[0, 1], [5, 2], [4, 3]];
const RING_LEN: f32 = (COLS * ROWS) as f32;
let _key = key.into();
let delta = motion::pulse_delta(&GRADIENT_SPIN, painter, cx);
div()
.flex()
.flex_col()
.gap(px(cell_px / 2.0))
.children((0..ROWS).map(move |row| {
let tint: gpui::Hsla = gpui::rgb(GSPIN_ROW_TINTS[row]).into();
div()
.flex()
.flex_row()
.gap(px(cell_px / 2.0))
.children((0..COLS).map(move |col| {
let phase = RING[row][col] as f32 / RING_LEN;
div()
.size(px(cell_px))
.rounded(px(cell_px / 2.0))
.bg(tint)
.opacity(motion::gspin_opacity(delta + phase, GSPIN_DIM))
}))
}))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Orb {
Cluster,
Ring,
Converge,
Bloom,
}
pub fn orb(
shape: Orb,
key: impl Into<SharedString>,
size_px: f32,
theme: &Theme,
painter: Painter,
cx: &mut App,
) -> impl IntoElement {
let _key = key.into();
let delta = motion::pulse_delta(&ORB, painter, cx);
let accent = theme.accent;
let dot = move |cx: f32, cy: f32, size: f32, opacity: f32, glow: f32| {
div()
.absolute()
.left(px(cx - size / 2.0))
.top(px(cy - size / 2.0))
.size(px(size))
.rounded_full()
.bg(accent.opacity(opacity))
.shadow(vec![gpui::BoxShadow {
color: accent.opacity(opacity * 0.55),
offset: gpui::point(px(0.0), px(0.0)),
blur_radius: px(glow),
spread_radius: px(0.0),
inset: false,
}])
};
let cells: Vec<gpui::Div> = match shape {
Orb::Cluster => (0..motion::ORBS)
.map(|index| {
let phase = motion::staggered_phase(delta, index, 1.0 / motion::ORBS as f32);
let (seat_x, seat_y) = motion::ORB_SEATS[index];
let (drift_x, drift_y) = motion::orb_drift(phase);
dot(
size_px * (seat_x + drift_x),
size_px * (seat_y + drift_y),
size_px * motion::orb_size(phase),
motion::orb_opacity(phase),
size_px * motion::orb_glow(phase),
)
})
.collect(),
Orb::Ring => (0..motion::ORB_RING_DOTS)
.map(|index| {
let phase =
motion::staggered_phase(delta, index, 1.0 / motion::ORB_RING_DOTS as f32);
let (seat_x, seat_y) = motion::orb_ring_seat(index, motion::ORB_RING_RADIUS);
dot(
size_px * seat_x,
size_px * seat_y,
size_px * motion::ORB_RING_DOT,
motion::orb_opacity(phase),
size_px * motion::orb_glow(phase) * 0.5,
)
})
.collect(),
Orb::Converge => {
let radius = motion::orb_converge_radius(delta);
(0..motion::ORB_RING_DOTS)
.map(|index| {
let (seat_x, seat_y) = motion::orb_ring_seat(index, radius);
dot(
size_px * seat_x,
size_px * seat_y,
size_px * motion::ORB_RING_DOT,
motion::orb_opacity(delta),
size_px * motion::orb_glow(delta) * 0.5,
)
})
.collect()
}
Orb::Bloom => (0..motion::ORB_BLOOM_RINGS)
.map(|index| {
let phase =
motion::staggered_phase(delta, index, 1.0 / motion::ORB_BLOOM_RINGS as f32);
let diameter = size_px * motion::orb_bloom_radius(phase);
let opacity = motion::orb_bloom_opacity(phase);
div()
.absolute()
.left(px((size_px - diameter) / 2.0))
.top(px((size_px - diameter) / 2.0))
.size(px(diameter))
.rounded_full()
.border(px((size_px * 0.05).max(1.0)))
.border_color(accent.opacity(opacity))
})
.collect(),
};
div().relative().size(px(size_px)).children(cells)
}
pub fn loading_word(theme: &Theme) -> impl IntoElement {
div()
.text_style(TextStyle::Subheadline)
.text_color(theme.text_muted.opacity(0.7))
.child(SharedString::from(
"L\u{2009}O\u{2009}A\u{2009}D\u{2009}I\u{2009}N\u{2009}G",
))
}
const _: () = {
assert!(PULSE.duration_ms == 2400);
assert!(GRADIENT_SPIN.duration_ms == 750);
};