use omp_core::Str;
use crate::{
component::{Component, PaintCtx, Slot, next_slot},
context::UiContext,
frame::Rect,
props::{Prop, PropValue, Props},
rich::cell_width,
};
pub struct Spinner {
props: Props,
slot: Slot,
label: Str,
}
impl Spinner {
pub fn new() -> Self {
Self { props: Props::new(), slot: next_slot(), label: Str::default() }
}
pub fn label(mut self, label: impl Into<Str>) -> Self {
self.label = label.into();
self
}
pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
self.props.set(prop, value);
self
}
}
impl Default for Spinner {
fn default() -> Self {
Self::new()
}
}
impl Component for Spinner {
fn props(&self) -> &Props {
&self.props
}
fn props_mut(&mut self) -> &mut Props {
&mut self.props
}
fn slot(&self) -> Slot {
self.slot
}
fn measure(&mut self, _ctx: &UiContext) -> (u16, u16) {
let natural = if self.label.is_empty() {
1
} else {
cell_width(&self.label).saturating_add(2)
};
(1, natural)
}
fn height(&mut self, _ctx: &UiContext, _width: u16) -> u16 {
1
}
fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
if rect.y >= pc.clip || rect.width == 0 {
return;
}
let frames = pc.ctx.charset.spinner();
let style = self.props.style(&pc.ctx.theme);
let mut column = pc.frame.put(rect.x, rect.y, frames.at(pc.now), style);
if !self.label.is_empty() {
column = pc.frame.put(column, rect.y, " ", style);
pc.frame.put(column, rect.y, &self.label, style);
}
pc.wake(self.slot, frames.next_change(pc.now));
}
fn set_text(&mut self, _ctx: &UiContext, text: Str) -> bool {
if self.label == text {
return false;
}
self.label = text;
true
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::{context::Charset, test_support::frame_row_text, ui::Ui};
#[test]
fn ticking_advances_the_glyph_and_reschedules() {
let mut ui = Ui::from_root(Spinner::new().label("busy"), 10, UiContext::default());
assert_eq!(frame_row_text(ui.frame(), 0), "⠋ busy");
assert_eq!(ui.next_wake(), Some(Duration::from_millis(80)));
assert!(!ui.tick(Duration::from_millis(79)), "no deadline is due yet");
assert!(ui.tick(Duration::from_millis(80)));
assert_eq!(frame_row_text(ui.frame(), 0), "⠙ busy");
assert_eq!(ui.next_wake(), Some(Duration::from_millis(160)));
}
#[test]
fn ascii_charset_uses_spoke_frames() {
let ctx = UiContext { charset: Charset::Ascii, ..UiContext::default() };
let mut ui = Ui::from_root(Spinner::new(), 4, ctx);
assert_eq!(frame_row_text(ui.frame(), 0), "|");
ui.tick(Duration::from_millis(120));
assert_eq!(frame_row_text(ui.frame(), 0), "/");
}
#[test]
fn set_text_replaces_the_label() {
let mut ui = Ui::from_root(
Spinner::new().label("indexing").with(Prop::Id, "spin"),
24,
UiContext::default(),
);
assert!(ui.set_text("spin", "linking"));
assert_eq!(frame_row_text(ui.frame(), 0), "⠋ linking");
assert!(!ui.set_text("spin", "linking"), "unchanged text reports no update");
}
}