From ca961f85356a96aee9daecf00588519692d51e19 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Fri, 12 Jan 2024 23:43:31 +0000 Subject: [PATCH] Add spinner to track search --- shalom/src/pages/room/listen.rs | 10 +++++++--- shalom/src/pages/room/listen/search.rs | 37 ++++++++++++++++++++++++------------- shalom/src/widgets/mod.rs | 1 + shalom/src/widgets/spinner.rs | 223 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 255 insertions(+), 16 deletions(-) create mode 100644 shalom/src/widgets/spinner.rs diff --git a/shalom/src/pages/room/listen.rs b/shalom/src/pages/room/listen.rs index f24ee92..ef9bac2 100644 --- a/shalom/src/pages/room/listen.rs +++ b/shalom/src/pages/room/listen.rs @@ -356,10 +356,14 @@ impl SearchState { matches!(self, Self::Open { search, .. } if !search.is_empty()) } - pub fn results(&self) -> &[SearchResult] { + pub fn results(&self) -> Option<&[SearchResult]> { match self { - Self::Open { results, .. } => results.as_slice(), - Self::Closed => &[], + Self::Open { + results, + needs_result, + .. + } => (!needs_result).then_some(results.as_slice()), + Self::Closed => None, } } diff --git a/shalom/src/pages/room/listen/search.rs b/shalom/src/pages/room/listen/search.rs index 5a41efd..827366b 100644 --- a/shalom/src/pages/room/listen/search.rs +++ b/shalom/src/pages/room/listen/search.rs @@ -1,6 +1,7 @@ use std::fmt::{Display, Formatter}; use iced::{ + alignment::Horizontal, theme, widget::{ column, component, container, container::Appearance, horizontal_rule, image, image::Handle, @@ -9,9 +10,9 @@ use iced::{ Alignment, Background, Color, Element, Length, Renderer, Theme, }; -use crate::widgets::mouse_area::mouse_area; +use crate::widgets::{mouse_area::mouse_area, spinner::CupertinoSpinner}; -pub fn search(theme: Theme, results: &[SearchResult]) -> Search<'_, M> { +pub fn search(theme: Theme, results: Option<&[SearchResult]>) -> Search<'_, M> { Search { on_track_press: None, theme, @@ -22,7 +23,7 @@ pub fn search(theme: Theme, results: &[SearchResult]) -> Sea pub struct Search<'a, M> { on_track_press: Option M>, theme: Theme, - results: &'a [SearchResult], + results: Option<&'a [SearchResult]>, } impl Search<'_, M> { @@ -43,20 +44,30 @@ impl Component for Search<'_, M> { } fn view(&self, _state: &Self::State) -> Element<'_, Self::Event, Renderer> { - let mut col = Column::new(); + let col = if let Some(results) = self.results { + let mut col = Column::new(); - for (i, result) in self.results.iter().enumerate() { - if i != 0 { - col = col.push(hr()); - } + for (i, result) in results.iter().enumerate() { + if i != 0 { + col = col.push(hr()); + } - let track = mouse_area(search_item_container(result_card(result, &self.theme))) - .on_press(Event::OnTrackPress(result.uri.to_string())); + let track = mouse_area(search_item_container(result_card(result, &self.theme))) + .on_press(Event::OnTrackPress(result.uri.to_string())); - col = col.push(track); - } + col = col.push(track); + } + + Element::from(scrollable(col.spacing(10))) + } else { + Element::from( + container(CupertinoSpinner::new().width(40.into()).height(40.into())) + .width(Length::Fill) + .align_x(Horizontal::Center), + ) + }; - search_container(scrollable(col.spacing(10))) + search_container(col) } } diff --git a/shalom/src/widgets/mod.rs b/shalom/src/widgets/mod.rs index 16d56e0..4265f44 100644 --- a/shalom/src/widgets/mod.rs +++ b/shalom/src/widgets/mod.rs @@ -8,5 +8,6 @@ pub mod image_card; pub mod media_player; pub mod mouse_area; pub mod room_navigation; +pub mod spinner; pub mod toggle_card; pub mod track_card; diff --git a/shalom/src/widgets/spinner.rs b/shalom/src/widgets/spinner.rs new file mode 100644 index 0000000..afaf917 --- /dev/null +++ b/shalom/src/widgets/spinner.rs @@ -0,0 +1,223 @@ +// Adapted from https://github.com/iced-rs/iced_aw/blob/9ea1b245041178ecf4db8e08629c7f1e985be8e0/src/native/cupertino/cupertino_spinner.rs + +use std::{f32::consts::PI, time::Instant}; + +use iced::{ + advanced::{ + layout::{Limits, Node}, + renderer, + widget::{ + tree::{State, Tag}, + Tree, + }, + Clipboard, Layout, Renderer as RendererTrait, Shell, Widget, + }, + event, + mouse::Cursor, + widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Renderer as CanvasRenderer, Stroke}, + window, Color, Element, Event, Length, Point, Rectangle, Renderer, Size, Vector, +}; + +const HAND_COUNT: usize = 8; +const ALPHAS: [u16; 8] = [47, 47, 47, 47, 72, 97, 122, 147]; + +/// `CupertinoSpinner` +/// +/// See +/// +/// 1. [Flutter Activity Indicator](https://github.com/flutter/flutter/blob/0b451b6dfd6de73ff89d89081c33d0f971db1872/packages/flutter/lib/src/cupertino/activity_indicator.dart) +/// 2. [Flutter Cupertino Widgets](https://docs.flutter.dev/development/ui/widgets/cupertino) +/// +/// for reference. The Flutter source is used for constants. The implementation for this widget +/// pulls together ideas from: +/// +/// 1. the mainline Clock example +/// 2. the existing `iced_aw` Spinner +/// 3. the Flutter Activity Indicator above +/// 4. the QR Code widget +/// +/// See the examples folder (`examples/cupertino/cupertino_spinner`) for a full example of usage. +#[allow(clippy::module_name_repetitions)] +#[derive(Debug)] +pub struct CupertinoSpinner { + width: Length, + height: Length, + radius: f32, +} + +struct SpinnerState { + now: Instant, + spinner: Cache, +} + +impl Default for CupertinoSpinner { + fn default() -> Self { + Self { + width: Length::Fixed(20.0), + height: Length::Fixed(20.0), + radius: 20.0, + } + } +} + +impl CupertinoSpinner { + /// Creates a new [`CupertinoSpinner`] widget. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the width of the [`CupertinoSpinner`](CupertinoSpinner). + #[must_use] + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`CupertinoSpinner`](CupertinoSpinner). + #[must_use] + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the radius of the [`CupertinoSpinner`](CupertinoSpinner). + /// NOTE: While you _can_ tweak the radius, the scale may be all out of whack if not using a + /// number close to the default of `20.0`. + #[must_use] + pub fn radius(mut self, radius: f32) -> Self { + self.radius = radius; + self + } +} + +impl Widget> for CupertinoSpinner { + fn width(&self) -> Length { + self.width + } + fn height(&self) -> Length { + self.height + } + + fn layout(&self, _renderer: &Renderer, limits: &Limits) -> Node { + Node::new( + limits + .width(self.width) + .height(self.height) + .resolve(Size::new(f32::INFINITY, f32::INFINITY)), + ) + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: Cursor, + _viewport: &Rectangle, + ) { + let state: &SpinnerState = state.state.downcast_ref::(); + + let spinner: Geometry = state + .spinner + .draw(renderer, layout.bounds().size(), |frame| { + let center = frame.center(); + let radius = self.radius; + let width = radius / 5.0; + + let mut hands: Vec<(Path, _)> = vec![]; + + for alpha in &ALPHAS { + hands.push(( + Path::line(Point::new(0.0, radius / 3.0), Point::new(0.0, radius / 1.5)), + move || -> Stroke { + // The `60.0` is to shift the original black to dark grey // + gen_stroke( + width, + Color::from_rgba(0.0, 0.0, 0.0, f32::from(*alpha) / (60.0 + 147.0)), + ) + }, + )); + } + + frame.translate(Vector::new(center.x, center.y)); + + frame.with_save(|frame| { + let new_index: usize = (state.now.elapsed().as_millis() / 125 % 8) as usize; + + for i in 0..HAND_COUNT { + frame.rotate(hand_rotation(45, 360)); + + frame.stroke( + &hands[i].0, + hands[((HAND_COUNT - i - 1) + new_index) % 8].1(), + ); + } + }); + }); + + let bounds = layout.bounds(); + let translation = Vector::new(bounds.x, bounds.y); + renderer.with_translation(translation, |renderer| { + renderer.draw(vec![spinner]); + }); + } + + fn tag(&self) -> Tag { + Tag::of::() + } + + fn state(&self) -> State { + State::new(SpinnerState { + now: Instant::now(), + spinner: Cache::default(), + }) + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + _layout: Layout<'_>, + _cursor: Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + let state: &mut SpinnerState = state.state.downcast_mut::(); + + if let Event::Window(window::Event::RedrawRequested(_now)) = &event { + state.spinner.clear(); + shell.request_redraw(window::RedrawRequest::NextFrame); + return event::Status::Captured; + } + + event::Status::Ignored + } +} + +impl<'a, Message, Theme> From for Element<'a, Message, Renderer> { + fn from(spinner: CupertinoSpinner) -> Self { + Self::new(spinner) + } +} + +fn gen_stroke(width: f32, color: Color) -> Stroke<'static> { + Stroke { + width, + style: stroke::Style::Solid(color), + line_cap: LineCap::Round, + ..Stroke::default() + } +} + +const K: f32 = PI * 2.0; + +fn hand_rotation(n: u16, total: u16) -> f32 { + let turns = f32::from(n) / f32::from(total); + + K * turns +} -- libgit2 1.7.2