feat: add spinner component

This commit is contained in:
2026-11-03 16:39:13 +07:00
parent 978aecb651
commit 7399d92f36
11 changed files with 595 additions and 114 deletions
+104
View File
@@ -0,0 +1,104 @@
use std::{cell::RefCell, rc::Rc, time::Duration};
use gpui::prelude::FluentBuilder as _;
use gpui::{
Context, IntoElement, ParentElement as _, Render, Styled as _, TestAppContext, Window, div,
};
use gpui_native_core::spinner::{Spinner, SpinnerVariant};
use gpui_native_theme::Theme;
struct SpinnerHost {
variant: SpinnerVariant,
determinate: bool,
renders: Rc<RefCell<usize>>,
}
impl Render for SpinnerHost {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
*self.renders.borrow_mut() += 1;
let spinner = Spinner::new("test-spinner")
.variant(self.variant)
.when(self.determinate, |spinner| spinner.value(0.4));
div().size_full().child(spinner)
}
}
fn host_window(
variant: SpinnerVariant,
determinate: bool,
reduce_motion: bool,
) -> (TestAppContext, Rc<RefCell<usize>>) {
let mut cx = TestAppContext::single();
cx.update(|cx| {
Theme::init(cx);
if reduce_motion {
cx.set_reduce_motion(true);
}
});
let renders = Rc::new(RefCell::new(0));
let host_renders = renders.clone();
cx.add_window(move |_, _| SpinnerHost {
variant,
determinate,
renders: host_renders,
});
cx.run_until_parked();
(cx, renders)
}
#[test]
fn spinning_repaints_on_the_animation_clock() {
let (cx, renders) = host_window(SpinnerVariant::Ring, false, false);
assert_eq!(*renders.borrow(), 1);
// The 60fps cap drives re-renders with timers, not frame callbacks.
cx.executor().advance_clock(Duration::from_millis(100));
cx.run_until_parked();
assert!(
*renders.borrow() > 1,
"the spinner must repaint while spinning"
);
}
#[test]
fn determinate_paints_one_static_frame() {
let (cx, renders) = host_window(SpinnerVariant::Ring, true, false);
assert_eq!(*renders.borrow(), 1);
cx.executor().advance_clock(Duration::from_millis(200));
cx.run_until_parked();
assert_eq!(*renders.borrow(), 1, "a determinate ring holds still");
}
#[test]
fn reduce_motion_paints_one_static_frame() {
let (cx, renders) = host_window(SpinnerVariant::Ring, false, true);
assert_eq!(*renders.borrow(), 1);
cx.executor().advance_clock(Duration::from_millis(200));
cx.run_until_parked();
assert_eq!(*renders.borrow(), 1, "reduced motion must freeze the spin");
}
#[test]
fn the_icon_variant_repaints_while_spinning() {
let (cx, renders) = host_window(SpinnerVariant::Icon, false, false);
assert_eq!(*renders.borrow(), 1);
cx.executor().advance_clock(Duration::from_millis(100));
cx.run_until_parked();
assert!(
*renders.borrow() > 1,
"the icon must repaint while spinning"
);
}
#[test]
fn the_icon_variant_holds_still_when_determinate() {
let (cx, renders) = host_window(SpinnerVariant::Icon, true, false);
assert_eq!(*renders.borrow(), 1);
cx.executor().advance_clock(Duration::from_millis(200));
cx.run_until_parked();
assert_eq!(*renders.borrow(), 1, "a held icon must not animate");
}