From 784d6f99d29a428ff5ff308d91f4cf38b465f526 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 13 Jan 2024 04:02:16 +0000 Subject: [PATCH] Persistent search box on track search --- shalom/src/magic/header_search.rs | 14 ++++++++++++-- shalom/src/pages/room.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------- shalom/src/pages/room/listen.rs | 37 +++++++++++++++++++++---------------- shalom/src/pages/room/listen/search.rs | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------- 4 files changed, 180 insertions(+), 79 deletions(-) diff --git a/shalom/src/magic/header_search.rs b/shalom/src/magic/header_search.rs index 9dbc1cf..d0aae8d 100644 --- a/shalom/src/magic/header_search.rs +++ b/shalom/src/magic/header_search.rs @@ -22,7 +22,8 @@ use keyframe::{functions::EaseOutQuint, keyframes, AnimationSequence}; use crate::theme::Icon; -const INITIAL_SEARCH_BOX_SIZE: Size = Size::new(54., 54.); +// text height +const INITIAL_SEARCH_BOX_SIZE: Size = Size::new(78., 78.); pub fn header_search<'a, M>( on_input: fn(String) -> M, @@ -30,6 +31,7 @@ pub fn header_search<'a, M>( open: bool, search_query: &str, mut header: Text<'a, Renderer>, + dy_mult: f32, ) -> HeaderSearch<'a, M> where M: Clone + 'a, @@ -55,6 +57,7 @@ where on_state_change, search_icon: Element::from(Icon::Search.canvas(Color::BLACK)), close_icon: Element::from(Icon::Close.canvas(Color::BLACK)), + dy_mult, } } @@ -73,6 +76,7 @@ pub struct HeaderSearch<'a, M> { input: Element<'a, M, Renderer>, search_icon: Element<'a, M, Renderer>, close_icon: Element<'a, M, Renderer>, + dy_mult: f32, } impl<'a, M> Widget for HeaderSearch<'a, M> @@ -160,10 +164,16 @@ where viewport, ); + let border_radius = if matches!(self.current_search_box_size, BoxSize::Fill) { + 100.0 * (1.0 - self.dy_mult) + } else { + 100.0 + }; + renderer.fill_quad( Quad { bounds: search_layout.bounds(), - border_radius: 1000.0.into(), + border_radius: border_radius.into(), border_width: 0.0, border_color: Color::default(), }, diff --git a/shalom/src/pages/room.rs b/shalom/src/pages/room.rs index 77f3fec..71389c3 100644 --- a/shalom/src/pages/room.rs +++ b/shalom/src/pages/room.rs @@ -7,7 +7,11 @@ use iced::{ advanced::graphics::core::Element, font::{Stretch, Weight}, theme, - widget::{container, row, text, Column}, + widget::{ + container, row, scrollable, + scrollable::{Direction, Properties, Viewport}, + text, Column, + }, Color, Font, Length, Renderer, Subscription, Theme, }; @@ -21,6 +25,9 @@ use crate::{ }, }; +const PADDING: u16 = 40; +const SPACE_TOP: u16 = 51; + #[derive(Debug)] pub struct Room { id: &'static str, @@ -28,6 +35,7 @@ pub struct Room { lights: lights::Lights, listen: listen::Listen, current_page: Page, + dy: f32, } impl Room { @@ -40,6 +48,7 @@ impl Room { lights: lights::Lights::new(oracle, &room), room, current_page: Page::Listen, + dy: 0.0, } } @@ -56,6 +65,10 @@ impl Room { None } Message::Exit => Some(Event::Exit), + Message::OnContentScroll(viewport) => { + self.dy = viewport.absolute_offset().y; + None + } } } @@ -69,25 +82,60 @@ impl Room { }) .style(theme::Text::Color(Color::WHITE)); - let header = if let Page::Listen = self.current_page { - self.listen - .header_magic(header.clone()) - .map(Message::Listen) - } else { - Element::from(header) + let (mut current, needs_scrollable) = match self.current_page { + Page::Climate => (Element::from(row![]), false), + Page::Listen => ( + self.listen.view(style).map(Message::Listen), + self.listen.search.is_open(), + ), + Page::Lights => ( + container(self.lights.view().map(Message::Lights)) + .padding([0, PADDING, 0, PADDING]) + .into(), + false, + ), }; - let header = container(header).padding([40, 40, 0, 40]); + let (header, padding_mult) = if let Page::Listen = self.current_page { + let padding_mult = if needs_scrollable { + (self.dy / f32::from(SPACE_TOP)).min(1.0) + } else { + 0.0 + }; + + ( + self.listen + .header_magic(header.clone(), padding_mult) + .map(Message::Listen), + padding_mult, + ) + } else { + (Element::from(header), 0.0) + }; - let mut col = Column::new().spacing(20).push(header); + let padding = f32::from(PADDING) * (1.0 - padding_mult); + let header = container(header).padding([padding, padding, 0.0, padding]); + + let mut col = Column::new() + .spacing(20.0 * (1.0 - padding_mult)) + .push(header); + + // TODO: on close, we need to animate the scrollback by padding the container up to dy + if needs_scrollable { + current = scrollable(container(current).width(Length::Fill).padding([ + f32::from(PADDING + 30) * padding_mult, + 0.0, + 0.0, + 0.0, + ])) + .direction(Direction::Vertical( + Properties::default().scroller_width(0).width(0), + )) + .on_scroll(Message::OnContentScroll) + .into(); + } - col = col.push(match self.current_page { - Page::Climate => Element::from(row![]), - Page::Listen => self.listen.view(style).map(Message::Listen), - Page::Lights => container(self.lights.view().map(Message::Lights)) - .padding([0, 40, 0, 40]) - .into(), - }); + col = col.push(current); let background = match self.current_page { Page::Listen => self @@ -105,7 +153,7 @@ impl Room { .on_exit(Message::Exit), image_background( background.unwrap_or_else(|| crate::theme::Image::Sunset.into()), - col.width(Length::Fill).into() + col.width(Length::Fill).into(), ) .width(Length::FillPortion(15)) .height(Length::Fill), @@ -134,5 +182,6 @@ pub enum Message { Lights(lights::Message), Listen(listen::Message), ChangePage(Page), + OnContentScroll(Viewport), Exit, } diff --git a/shalom/src/pages/room/listen.rs b/shalom/src/pages/room/listen.rs index 172b5f8..44016f0 100644 --- a/shalom/src/pages/room/listen.rs +++ b/shalom/src/pages/room/listen.rs @@ -35,7 +35,7 @@ pub struct Listen { musicbrainz_artist_id: Option, pub background: Option, artist_logo: Option, - search: SearchState, + pub search: SearchState, config: Arc, } @@ -56,22 +56,27 @@ impl Listen { } } - pub fn header_magic(&self, text: Text<'static>) -> Element<'static, Message> { - lazy(self.search.clone(), move |search| { - let (open, query) = if let Some(v) = search.search() { - (true, v) - } else { - (false, "") - }; + pub fn header_magic(&self, text: Text<'static>, dy_mult: f32) -> Element<'static, Message> { + lazy( + (self.search.clone(), dy_mult.to_be_bytes()), + move |(search, dy_mult)| { + let dy_mult = f32::from_be_bytes(*dy_mult); + let (open, query) = if let Some(v) = search.search() { + (true, v) + } else { + (false, "") + }; - header_search( - Message::OnSearchTerm, - Message::OnSearchVisibleChange, - open, - query, - text.clone(), - ) - }) + header_search( + Message::OnSearchTerm, + Message::OnSearchVisibleChange, + open, + query, + text.clone(), + dy_mult, + ) + }, + ) .into() } diff --git a/shalom/src/pages/room/listen/search.rs b/shalom/src/pages/room/listen/search.rs index 1351507..6680f3b 100644 --- a/shalom/src/pages/room/listen/search.rs +++ b/shalom/src/pages/room/listen/search.rs @@ -4,13 +4,16 @@ use iced::{ alignment::Horizontal, theme, widget::{ - column, component, container, container::Appearance, horizontal_rule, image, image::Handle, - row, scrollable, text, Column, Component, + column, component, container, container::Appearance, image, image::Handle, row, text, + Column, Component, }, Alignment, Background, Color, Element, Length, Renderer, Theme, }; -use crate::widgets::{mouse_area::mouse_area, spinner::CupertinoSpinner}; +use crate::{ + theme::colours::SYSTEM_GRAY6, + widgets::{mouse_area::mouse_area, spinner::CupertinoSpinner}, +}; pub fn search(theme: Theme, results: SearchState<'_>) -> Search<'_, M> { Search { @@ -34,67 +37,90 @@ impl Search<'_, M> { } impl Component for Search<'_, M> { - type State = (); + type State = State; type Event = Event; - fn update(&mut self, _state: &mut Self::State, event: Self::Event) -> Option { + fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option { match event { - Event::OnTrackPress(id) => self.on_track_press.map(|f| (f)(id)), + Event::OnTrackPress(id) => { + state.pressing = None; + self.on_track_press.map(|f| (f)(id)) + } + Event::OnDown(i) => { + state.pressing = Some(i); + None + } + Event::OnCancel => { + state.pressing = None; + None + } } } - fn view(&self, _state: &Self::State) -> Element<'_, Self::Event, Renderer> { - let col = match self.results { + fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Renderer> { + match self.results { SearchState::Ready(results) if !results.is_empty() => { let mut col = Column::new(); for (i, result) in results.iter().enumerate() { - if i != 0 { - col = col.push(hr()); - } + let pressing = state.pressing == Some(i); - 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), + pressing, + )) + .on_press(Event::OnDown(i)) + .on_release(Event::OnTrackPress(result.uri.to_string())) + .on_cancel(Event::OnCancel); col = col.push(track); } - Element::from(scrollable(col.spacing(10))) + Element::from(col.spacing(10)) } - SearchState::Ready(_) => Element::from( + SearchState::Ready(_) => Element::from(search_item_container( container(text("No results found")) .width(Length::Fill) .align_x(Horizontal::Center), - ), - SearchState::Error(error) => Element::from( + false, + )), + SearchState::Error(error) => Element::from(search_item_container( container(text(error)) .width(Length::Fill) .align_x(Horizontal::Center), - ), - SearchState::NotReady => Element::from( + false, + )), + SearchState::NotReady => Element::from(search_item_container( container(CupertinoSpinner::new().width(40.into()).height(40.into())) .width(Length::Fill) .align_x(Horizontal::Center), - ), - }; - - search_container(col) + false, + )), + } } } +#[derive(Default, Debug, Clone, Copy)] +pub struct State { + pressing: Option, +} + impl<'a, M: 'static + Clone> From> for Element<'a, M, Renderer> { fn from(value: Search<'a, M>) -> Self { component(value) } } +#[allow(clippy::enum_variant_names)] #[derive(Clone, Debug)] pub enum Event { OnTrackPress(String), + OnDown(usize), + OnCancel, } fn result_card(result: &SearchResult, style: &Theme) -> Element<'static, M, Renderer> { - let main_text = text(&result.title).style(style.extended_palette().background.base.text); + let main_text = text(&result.title); let sub_text = text(&result.metadata).style(style.extended_palette().background.strong.color); row![ @@ -106,42 +132,53 @@ fn result_card(result: &SearchResult, style: &Theme) -> Element<'sta .into() } -fn hr() -> Element<'static, M, Renderer> { - container(horizontal_rule(1)) - .width(Length::Fill) - .padding([10, 0, 10, 0]) - .into() -} - fn search_item_container<'a, M: 'a>( elem: impl Into>, -) -> Element<'a, M, Renderer> { - container(elem).padding([0, 20, 0, 20]).into() -} - -fn search_container<'a, M: 'a>( - elem: impl Into>, + pressing: bool, ) -> Element<'a, M, Renderer> { container(elem) - .padding([20, 0, 20, 0]) + .padding([20, 20, 20, 20]) + .style(theme::Container::Custom(Box::new(SearchItemContainer( + pressing, + )))) .width(Length::Fill) - .style(theme::Container::Custom(Box::new(SearchContainer))) .into() } #[allow(clippy::module_name_repetitions)] -pub struct SearchContainer; +pub struct SearchItemContainer(bool); -impl container::StyleSheet for SearchContainer { +impl container::StyleSheet for SearchItemContainer { type Style = Theme; fn appearance(&self, _style: &Self::Style) -> Appearance { - Appearance { - text_color: Some(Color::BLACK), - background: Some(Background::Color(Color::WHITE)), + let base = Appearance { + text_color: Some(Color { + a: 0.7, + ..Color::WHITE + }), + background: None, border_radius: 20.0.into(), border_width: 0.0, border_color: Color::default(), + }; + + if self.0 { + Appearance { + background: Some(Background::Color(Color { + a: 0.9, + ..SYSTEM_GRAY6 + })), + ..base + } + } else { + Appearance { + background: Some(Background::Color(Color { + a: 0.7, + ..SYSTEM_GRAY6 + })), + ..base + } } } } -- libgit2 1.7.2