Persistent search box on track search
Diff
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 | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
4 files changed, 181 insertions(+), 80 deletions(-)
@@ -22,7 +22,8 @@
use crate::theme::Icon;
const INITIAL_SEARCH_BOX_SIZE: Size = Size::new(54., 54.);
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 @@
open: bool,
search_query: &str,
mut header: Text<'a, Renderer>,
dy_mult: f32,
) -> HeaderSearch<'a, M>
where
M: Clone + 'a,
@@ -55,6 +57,7 @@
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 @@
input: Element<'a, M, Renderer>,
search_icon: Element<'a, M, Renderer>,
close_icon: Element<'a, M, Renderer>,
dy_mult: f32,
}
impl<'a, M> Widget<M, Renderer> for HeaderSearch<'a, M>
@@ -160,10 +164,16 @@
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(),
},
@@ -7,7 +7,11 @@
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,
};
@@ -20,6 +24,9 @@
room_navigation::{Page, RoomNavigation},
},
};
const PADDING: u16 = 40;
const SPACE_TOP: u16 = 51;
#[derive(Debug)]
pub struct Room {
@@ -28,6 +35,7 @@
lights: lights::Lights,
listen: listen::Listen,
current_page: Page,
dy: f32,
}
impl Room {
@@ -40,6 +48,7 @@
lights: lights::Lights::new(oracle, &room),
room,
current_page: Page::Listen,
dy: 0.0,
}
}
@@ -56,6 +65,10 @@
None
}
Message::Exit => Some(Event::Exit),
Message::OnContentScroll(viewport) => {
self.dy = viewport.absolute_offset().y;
None
}
}
}
@@ -69,25 +82,60 @@
})
.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);
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 @@
.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 @@
Lights(lights::Message),
Listen(listen::Message),
ChangePage(Page),
OnContentScroll(Viewport),
Exit,
}
@@ -35,7 +35,7 @@
musicbrainz_artist_id: Option<String>,
pub background: Option<MaybePendingImage>,
artist_logo: Option<MaybePendingImage>,
search: SearchState,
pub search: SearchState,
config: Arc<Config>,
}
@@ -56,22 +56,27 @@
}
}
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()
}
@@ -1,16 +1,19 @@
use std::fmt::{Display, Formatter};
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<M: Clone + 'static>(theme: Theme, results: SearchState<'_>) -> Search<'_, M> {
Search {
@@ -34,67 +37,90 @@
}
impl<M: Clone + 'static> Component<M, Renderer> for Search<'_, M> {
type State = ();
type State = State;
type Event = Event;
fn update(&mut self, _state: &mut Self::State, event: Self::Event) -> Option<M> {
fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<M> {
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 track = mouse_area(search_item_container(result_card(result, &self.theme)))
.on_press(Event::OnTrackPress(result.uri.to_string()));
let pressing = state.pressing == Some(i);
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<usize>,
}
impl<'a, M: 'static + Clone> From<Search<'a, M>> 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<M: 'static>(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![
@@ -104,44 +130,55 @@
.align_items(Alignment::Center)
.spacing(10)
.into()
}
fn hr<M: 'static>() -> 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>>,
) -> Element<'a, M, Renderer> {
container(elem).padding([0, 20, 0, 20]).into()
}
fn search_container<'a, M: 'a>(
elem: impl Into<Element<'a, M, Renderer>>,
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
}
}
}
}