liana/gui/ui/src/component/spinner.rs
2024-10-08 10:46:07 +01:00

186 lines
5.1 KiB
Rust

use std::time::Duration;
use iced::{
advanced::{
layout, renderer,
widget::tree::{self, Tree},
Clipboard, Layout, Shell, Widget,
},
event, mouse,
time::Instant,
window, Element, Event, Length, Rectangle, Renderer, Size,
};
/// A loading spinner widget that cycles through a collection of
/// `children` at a fixed rate.
///
/// `interval` is how long to wait before displaying the next child.
pub struct Carousel<'a, Message, Theme> {
interval: Duration,
children: Vec<Element<'a, Message, Theme>>,
}
impl<'a, Message, Theme> Carousel<'a, Message, Theme> {
pub fn new(interval: Duration, children: Vec<impl Into<Element<'a, Message, Theme>>>) -> Self {
Carousel {
interval,
children: children.into_iter().map(|child| child.into()).collect(),
}
}
}
/// The state of a `Carousel`.
///
/// `last_transition` is when the `current`th child
/// of `Carousel::children` was selected.
struct CarouselState {
last_transition: Instant,
current: usize,
}
impl CarouselState {
fn new() -> Self {
Self {
last_transition: Instant::now(),
current: 0,
}
}
}
impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for Carousel<'a, Message, Theme>
where
Message: 'a + Clone,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<CarouselState>()
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(self.children.as_slice());
}
fn layout(
&self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let state = tree.state.downcast_mut::<CarouselState>();
let child_nodes: Vec<_> = self
.children
.iter()
.enumerate()
.map(|(i, child)| {
child
.as_widget()
.layout(&mut tree.children[i], renderer, limits)
})
.collect();
layout::Node::with_children(child_nodes[state.current].size(), child_nodes)
}
fn children(&self) -> Vec<Tree> {
self.children.iter().map(|child| Tree::new(child)).collect()
}
fn state(&self) -> tree::State {
tree::State::new(CarouselState::new())
}
fn size(&self) -> Size<Length> {
// Use an arbitrary size here as the layout node size
// is determined from the current child.
Size {
width: Length::Shrink,
height: Length::Shrink,
}
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
_layout: Layout<'_>,
_cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
let state = tree.state.downcast_mut::<CarouselState>();
if let Event::Window(_, window::Event::RedrawRequested(now)) = event {
if now.duration_since(state.last_transition) > self.interval {
state.last_transition = now;
state.current = (state.current + 1) % self.children.len();
}
shell.request_redraw(window::RedrawRequest::NextFrame);
}
event::Status::Ignored
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<CarouselState>();
let current = self.children.get(state.current).expect("current");
let current_layout = layout
.children()
.nth(state.current)
.expect("current layout");
current.as_widget().draw(
&tree.children[state.current],
renderer,
theme,
style,
current_layout,
cursor,
viewport,
);
}
}
impl<'a, Message, Theme> From<Carousel<'a, Message, Theme>> for Element<'a, Message, Theme>
where
Message: 'a + Clone,
Theme: 'a,
{
fn from(carousel: Carousel<'a, Message, Theme>) -> Self {
Element::new(carousel)
}
}
/// Create a `Carousel` that types out the given `content` one character
/// at a time.
///
/// If `show_empty` is `true`, the text will begin with an empty string.
///
/// `interval` is how long to wait before the next character appears.
///
/// `text_builder` is used to build each `Text` element with the required
/// style etc.
pub fn typing_text_carousel<'a, Message, Theme>(
content: &'a str,
show_empty: bool,
interval: Duration,
text_builder: impl Fn(&'a str) -> iced::widget::Text<'a, Theme, Renderer>,
) -> Carousel<'a, Message, Theme>
where
Theme: 'a + iced::widget::text::StyleSheet,
{
let mut children = Vec::new();
if show_empty {
children.push(text_builder(""));
}
for end_char in 0..content.chars().count() {
children.push(text_builder(&content[0..=end_char]));
}
Carousel::new(interval, children)
}