Add spinner to track search
Diff
shalom/src/widgets/mod.rs | 1 +
shalom/src/widgets/spinner.rs | 223 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
shalom/src/pages/room/listen.rs | 10 +++++++---
shalom/src/pages/room/listen/search.rs | 39 ++++++++++++++++++++++++++++-----------
4 files changed, 256 insertions(+), 17 deletions(-)
@@ -8,5 +8,6 @@
pub mod media_player;
pub mod mouse_area;
pub mod room_navigation;
pub mod spinner;
pub mod toggle_card;
pub mod track_card;
@@ -1,0 +1,223 @@
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];
#[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 {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
#[must_use]
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
#[must_use]
pub fn radius(mut self, radius: f32) -> Self {
self.radius = radius;
self
}
}
impl<Message, Theme> Widget<Message, Renderer<Theme>> for CupertinoSpinner {
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(&self, _renderer: &Renderer<Theme>, 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: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: Cursor,
_viewport: &Rectangle,
) {
let state: &SpinnerState = state.state.downcast_ref::<SpinnerState>();
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 {
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::<SpinnerState>()
}
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<Theme>,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
let state: &mut SpinnerState = state.state.downcast_mut::<SpinnerState>();
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<CupertinoSpinner> for Element<'a, Message, Renderer<Theme>> {
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
}
@@ -356,10 +356,14 @@
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,
}
}
@@ -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 @@
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<M: Clone + 'static>(theme: Theme, results: &[SearchResult]) -> Search<'_, M> {
pub fn search<M: Clone + 'static>(theme: Theme, results: Option<&[SearchResult]>) -> Search<'_, M> {
Search {
on_track_press: None,
theme,
@@ -22,7 +23,7 @@
pub struct Search<'a, M> {
on_track_press: Option<fn(String) -> M>,
theme: Theme,
results: &'a [SearchResult],
results: Option<&'a [SearchResult]>,
}
impl<M> Search<'_, M> {
@@ -43,20 +44,30 @@
}
fn view(&self, _state: &Self::State) -> Element<'_, Self::Event, Renderer> {
let mut col = Column::new();
for (i, result) in self.results.iter().enumerate() {
if i != 0 {
col = col.push(hr());
}
let col = if let Some(results) = self.results {
let mut col = Column::new();
let track = mouse_area(search_item_container(result_card(result, &self.theme)))
.on_press(Event::OnTrackPress(result.uri.to_string()));
for (i, result) in results.iter().enumerate() {
if i != 0 {
col = col.push(hr());
}
col = col.push(track);
}
let track = mouse_area(search_item_container(result_card(result, &self.theme)))
.on_press(Event::OnTrackPress(result.uri.to_string()));
col = col.push(track);
}
search_container(scrollable(col.spacing(10)))
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(col)
}
}