Scroll to top before closing search bar
Diff
Cargo.lock | 1 +
shalom/src/magic/header_search.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
shalom/src/pages/room.rs | 30 ++++++++++++++++++++++++++++++
shalom/src/pages/room/listen.rs | 8 ++++----
4 files changed, 78 insertions(+), 36 deletions(-)
@@ -1394,6 +1394,7 @@
"iced_renderer",
"iced_runtime",
"iced_style",
"keyframe",
"num-traits",
"ouroboros",
"thiserror",
@@ -27,7 +27,7 @@
pub fn header_search<'a, M>(
on_input: fn(String) -> M,
on_state_change: fn(bool) -> M,
open_state_toggle: M,
open: bool,
search_query: &str,
mut header: Text<'a, Renderer>,
@@ -49,12 +49,13 @@
original_header: header.clone(),
header,
current_search_box_size,
open,
open_state_toggle,
input: iced::widget::text_input("Search...", search_query)
.id(Id::unique())
.on_input(on_input)
.style(iced::theme::TextInput::Custom(Box::new(InputStyle)))
.into(),
on_state_change,
search_icon: Element::from(Icon::Search.canvas(Color::BLACK)),
close_icon: Element::from(Icon::Close.canvas(Color::BLACK)),
dy_mult,
@@ -72,11 +73,12 @@
original_header: Text<'a, Renderer>,
header: Text<'a, Renderer>,
current_search_box_size: BoxSize,
on_state_change: fn(bool) -> M,
input: Element<'a, M, Renderer>,
search_icon: Element<'a, M, Renderer>,
close_icon: Element<'a, M, Renderer>,
dy_mult: f32,
open: bool,
open_state_toggle: M,
}
impl<'a, M> Widget<M, Renderer> for HeaderSearch<'a, M>
@@ -219,9 +221,10 @@
}
}
#[allow(clippy::too_many_lines)]
fn on_event(
&mut self,
state: &mut Tree,
tree_state: &mut Tree,
event: iced::Event,
layout: Layout<'_>,
cursor: Cursor,
@@ -230,6 +233,20 @@
shell: &mut Shell<'_, M>,
viewport: &Rectangle,
) -> Status {
let state = tree_state.state.downcast_mut::<State>();
match state {
State::Open if !self.open => {
*state = State::close();
shell.request_redraw(RedrawRequest::NextFrame);
}
State::Closed if self.open => {
*state = State::open();
shell.request_redraw(RedrawRequest::NextFrame);
}
_ => {}
}
let status = match event {
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(mouse::Button::Left))
| iced::Event::Touch(iced::touch::Event::FingerPressed { .. })
@@ -244,13 +261,13 @@
.bounds(),
) =>
{
let state = state.state.downcast_mut::<State>();
*state = state.clone().flip();
shell.request_redraw(RedrawRequest::NextFrame);
if !matches!(state, State::Animate { .. }) {
shell.publish(self.open_state_toggle.clone());
}
Status::Captured
}
iced::Event::Window(iced::window::Event::RedrawRequested(_)) => {
let state = state.state.downcast_mut::<State>();
let State::Animate {
last_draw,
next_state,
@@ -301,11 +318,9 @@
match &state {
State::Open => {
shell.publish((self.on_state_change)(true));
self.current_search_box_size = BoxSize::Fill;
}
State::Closed => {
shell.publish((self.on_state_change)(false));
self.current_search_box_size = BoxSize::Min;
}
State::Animate { .. } => {}
@@ -321,7 +336,7 @@
if status == Status::Ignored {
self.input.as_widget_mut().on_event(
&mut state.children[0],
&mut tree_state.children[0],
event,
layout.children().nth(1).unwrap().children().nth(1).unwrap(),
cursor,
@@ -396,25 +411,25 @@
}
impl State {
fn flip(self) -> Self {
match self {
State::Closed => Self::Animate {
last_draw: Instant::now(),
next_state: Box::new(State::Open),
text_opacity: keyframes![(1.0, 0.0, EaseOutQuint), (0.0, 0.5)],
search_box_size: keyframes![(0.0, 0.0, EaseOutQuint), (0.0, 0.1), (1.0, 0.5)],
search_icon: keyframes![(1.0, 0.0, EaseOutQuint), (0.0, 0.5)],
close_icon: keyframes![(0.0, 0.0, EaseOutQuint), (0.0, 0.1), (1.0, 0.5)],
},
State::Open => Self::Animate {
last_draw: Instant::now(),
next_state: Box::new(State::Closed),
text_opacity: keyframes![(0.0, 0.0, EaseOutQuint), (0.0, 0.1), (1.0, 0.5)],
search_box_size: keyframes![(1.0, 0.0, EaseOutQuint), (0.0, 0.5)],
search_icon: keyframes![(0.0, 0.0, EaseOutQuint), (0.0, 0.1), (1.0, 0.5)],
close_icon: keyframes![(1.0, 0.0, EaseOutQuint), (0.0, 0.5)],
},
v @ State::Animate { .. } => v,
fn open() -> Self {
Self::Animate {
last_draw: Instant::now(),
next_state: Box::new(State::Open),
text_opacity: keyframes![(1.0, 0.0, EaseOutQuint), (0.0, 0.5)],
search_box_size: keyframes![(0.0, 0.0, EaseOutQuint), (0.0, 0.1), (1.0, 0.5)],
search_icon: keyframes![(1.0, 0.0, EaseOutQuint), (0.0, 0.5)],
close_icon: keyframes![(0.0, 0.0, EaseOutQuint), (0.0, 0.1), (1.0, 0.5)],
}
}
fn close() -> Self {
Self::Animate {
last_draw: Instant::now(),
next_state: Box::new(State::Closed),
text_opacity: keyframes![(0.0, 0.0, EaseOutQuint), (0.0, 0.1), (1.0, 0.5)],
search_box_size: keyframes![(1.0, 0.0, EaseOutQuint), (0.0, 0.5)],
search_icon: keyframes![(0.0, 0.0, EaseOutQuint), (0.0, 0.1), (1.0, 0.5)],
close_icon: keyframes![(1.0, 0.0, EaseOutQuint), (0.0, 0.5)],
}
}
}
@@ -36,6 +36,7 @@
listen: listen::Listen,
current_page: Page,
dy: f32,
pending_visible_toggle: bool,
}
impl Room {
@@ -49,6 +50,7 @@
room,
current_page: Page::Listen,
dy: 0.0,
pending_visible_toggle: false,
}
}
@@ -59,15 +61,37 @@
pub fn update(&mut self, event: Message) -> Option<Event> {
match event {
Message::Lights(v) => self.lights.update(v).map(Event::Lights),
Message::Listen(listen::Message::OnSearchVisibleToggle)
if self.listen.search.is_open() && self.dy > 0.0 =>
{
self.pending_visible_toggle = true;
None
}
Message::Listen(v) => self.listen.update(v).map(Event::Listen),
Message::ChangePage(page) => {
self.dy = 0.0;
self.current_page = page;
None
}
Message::Exit => Some(Event::Exit),
Message::Exit => {
self.dy = 0.0;
Some(Event::Exit)
}
Message::OnContentScroll(viewport) => {
self.dy = viewport.absolute_offset().y;
None
}
Message::OnContentAnimateFinished => {
if self.pending_visible_toggle {
self.pending_visible_toggle = false;
self.listen
.update(listen::Message::OnSearchVisibleToggle)
.map(Event::Listen)
} else {
None
}
}
}
}
@@ -120,7 +144,6 @@
.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,
@@ -132,6 +155,8 @@
Properties::default().scroller_width(0).width(0),
))
.on_scroll(Message::OnContentScroll)
.on_animate_finished(Message::OnContentAnimateFinished)
.scroll_to_top(self.pending_visible_toggle)
.into();
}
@@ -183,5 +208,6 @@
Listen(listen::Message),
ChangePage(Page),
OnContentScroll(Viewport),
OnContentAnimateFinished,
Exit,
}
@@ -69,7 +69,7 @@
header_search(
Message::OnSearchTerm,
Message::OnSearchVisibleChange,
Message::OnSearchVisibleToggle,
open,
query,
text.clone(),
@@ -180,8 +180,8 @@
self.search = self.search.open(v);
None
}
Message::OnSearchVisibleChange(v) => {
self.search = if v {
Message::OnSearchVisibleToggle => {
self.search = if matches!(self.search, SearchState::Closed) {
SearchState::Open {
search: String::new(),
results_search: String::new(),
@@ -421,7 +421,7 @@
OnSpeakerNextTrack,
OnSpeakerPreviousTrack,
OnSearchTerm(String),
OnSearchVisibleChange(bool),
OnSearchVisibleToggle,
SpotifySearchResult((SearchResult, String)),
SpotifySearchResultError((String, String)),
OnPlayTrack(String),