🏡 index : ~doyle/shalom.git

author Jordan Doyle <jordan@doyle.la> 2024-01-13 4:02:16.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2024-01-13 4:02:16.0 +00:00:00
commit
784d6f99d29a428ff5ff308d91f4cf38b465f526 [patch]
tree
ded684534f49035277484edf42b9265523d554b1
parent
84251aa7f8f772ce81b6f7f260c07cc0dd43750d
download
784d6f99d29a428ff5ff308d91f4cf38b465f526.tar.gz

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 | 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<M, Renderer> 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<String>,
    pub background: Option<MaybePendingImage>,
    artist_logo: Option<MaybePendingImage>,
    search: SearchState,
    pub search: SearchState,
    config: Arc<Config>,
}

@@ -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<M: Clone + 'static>(theme: Theme, results: SearchState<'_>) -> Search<'_, M> {
    Search {
@@ -34,67 +37,90 @@ impl<M> Search<'_, M> {
}

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 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<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![
@@ -106,42 +132,53 @@ fn result_card<M: 'static>(result: &SearchResult, style: &Theme) -> Element<'sta
    .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
            }
        }
    }
}