Store states of the sidebar and menu, hide and recover together on single center tap events.

This commit is contained in:
jess 2026-05-09 13:25:24 -07:00
parent 6b11beff44
commit 97d2d6bd75
3 changed files with 127 additions and 14 deletions

View File

@ -2,6 +2,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use iced_wgpu::core::{Element, Theme};
use iced_widget::scrollable::AbsoluteOffset;
use crate::analyzer::FrameData;
use crate::analyzer_worker::AnalyzerWorker;
@ -39,6 +40,21 @@ pub struct App {
/// modal status message shown over the UI while waiting on iOS file-coordinator caching.
pub coordinating_message: Option<String>,
/// sidebar scroll offset mirrored from the on_scroll callback.
pub sidebar_scroll: AbsoluteOffset,
/// settings panel scroll offset mirrored from the on_scroll callback.
pub settings_scroll: AbsoluteOffset,
/// pending sidebar scroll restore against the live scrollable widget.
pub restore_sidebar_scroll: bool,
/// pending settings scroll restore against the live scrollable widget.
pub restore_settings_scroll: bool,
/// show_settings copy bridging the middle-tap collapse cycle.
pub saved_show_settings: bool,
}
/// every visualizer toggle, slider value, and DSP parameter the settings panel exposes.
@ -106,7 +122,10 @@ pub enum Message {
Seek(f32),
ToggleImmersive,
ToggleChrome,
ToggleSettings,
SidebarScrolled(AbsoluteOffset),
SettingsScrolled(AbsoluteOffset),
SetGlass(bool),
SetEntropy(bool),
SetAlbumColors(bool),
@ -158,6 +177,11 @@ impl App {
track_loading: false,
library_progress: None,
coordinating_message: None,
sidebar_scroll: AbsoluteOffset::default(),
settings_scroll: AbsoluteOffset::default(),
restore_sidebar_scroll: false,
restore_settings_scroll: false,
saved_show_settings: false,
}
}
@ -552,7 +576,28 @@ impl App {
}
}
Message::ToggleImmersive => self.immersive = !self.immersive,
Message::ToggleSettings => self.show_settings = !self.show_settings,
Message::ToggleChrome => {
if self.immersive {
self.immersive = false;
self.restore_sidebar_scroll = true;
if self.saved_show_settings {
self.show_settings = true;
self.restore_settings_scroll = true;
}
} else {
self.saved_show_settings = self.show_settings;
self.show_settings = false;
self.immersive = true;
}
}
Message::ToggleSettings => {
self.show_settings = !self.show_settings;
if self.show_settings {
self.restore_settings_scroll = true;
}
}
Message::SidebarScrolled(off) => self.sidebar_scroll = off,
Message::SettingsScrolled(off) => self.settings_scroll = off,
Message::SetGlass(on) => self.settings.glass = on,
Message::SetEntropy(on) => self.settings.entropy_on = on,
Message::SetAlbumColors(on) => self.settings.album_colors = on,

View File

@ -1,6 +1,7 @@
use std::ops::RangeInclusive;
use std::path::PathBuf;
use iced_wgpu::core::widget::Id as WidgetId;
use iced_wgpu::core::{Background, Border, Color, Element, Length, Padding, Theme};
use iced_widget::{
button::{self, Status as ButtonStatus},
@ -30,6 +31,16 @@ pub const TRANSPORT_H: f32 = 72.0;
const ROW_H: f32 = 56.0;
const THUMB: f32 = 40.0;
/// stable id of the sidebar scrollable.
pub fn sidebar_scroll_id() -> WidgetId {
WidgetId::new("yrx-sidebar-scroll")
}
/// stable id of the settings-panel scrollable.
pub fn settings_scroll_id() -> WidgetId {
WidgetId::new("yrx-settings-scroll")
}
/// assembles the top bar, sidebar, transport, and visualizer into the active layout.
pub fn view(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
let body: Element<'_, Message, Theme, iced_wgpu::Renderer> = if app.immersive {
@ -173,7 +184,10 @@ fn sidebar(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
for (i, t) in tracks.iter().enumerate() {
col = col.push(track_row_owned(i, t.clone(), selected == Some(i)));
}
scrollable(col).height(Length::Fill)
scrollable(col)
.id(sidebar_scroll_id())
.on_scroll(|vp| Message::SidebarScrolled(vp.absolute_offset()))
.height(Length::Fill)
});
container(inner)
@ -303,7 +317,7 @@ fn visualiser(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
..Default::default()
});
mouse_area(bordered).on_press(Message::ToggleImmersive).into()
mouse_area(bordered).on_press(Message::ToggleChrome).into()
}
/// animated cog and label overlay shown during track decode.
@ -392,15 +406,9 @@ pub const SETTINGS_W: f32 = 340.0;
fn settings_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
let s = &app.settings;
let header = row![
text("Settings").size(15).color(palette::text()),
Space::new().width(Length::Fill),
chip_button("Close", Message::ToggleSettings),
]
.align_y(iced_wgpu::core::Alignment::Center);
let body = column![
header,
Space::new().height(Length::Fixed(TOP_BAR_H + 4.0)),
text("Settings").size(15).color(palette::text()),
Space::new().height(Length::Fixed(10.0)),
section_label("style"),
toggle_row("glass", s.glass, Message::SetGlass),
@ -504,9 +512,19 @@ fn settings_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Rendere
.padding(Padding::from(16))
.width(Length::Fixed(SETTINGS_W));
let scroll = scrollable(body).height(Length::Fill);
let scroll = scrollable(body)
.id(settings_scroll_id())
.on_scroll(|vp| Message::SettingsScrolled(vp.absolute_offset()))
.height(Length::Fill);
let panel = container(scroll)
let close = container(icon_chip_button(SETTINGS_SVG, Message::ToggleSettings))
.width(Length::Fill)
.height(Length::Fixed(TOP_BAR_H))
.padding(Padding::from([0, 16]))
.align_x(iced_wgpu::core::alignment::Horizontal::Right)
.align_y(iced_wgpu::core::alignment::Vertical::Center);
let panel = container(stack![scroll, close])
.width(Length::Fixed(SETTINGS_W))
.height(Length::Fill)
.style(settings_panel_style);

View File

@ -4,14 +4,16 @@ use iced_graphics::{Shell as GShell, Viewport};
use iced_runtime::user_interface::{self, UserInterface};
use iced_wgpu::core::renderer::Style;
use iced_wgpu::core::time::Instant;
use iced_wgpu::core::widget::operation::scrollable as scrollable_op;
use iced_wgpu::core::{
clipboard, keyboard, mouse, window, Color, Event, Font, Pixels, Point, Size, SmolStr,
Theme,
};
use iced_wgpu::Engine;
use iced_widget::scrollable::AbsoluteOffset;
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
use crate::ui::{theme, App, Message};
use crate::ui::{player, theme, App, Message};
/// per-window bundle of the wgpu surface, iced renderer, App state, and pending input event queue.
pub struct ViewportHandle {
@ -534,6 +536,8 @@ fn render(handle: &mut ViewportHandle) {
.events
.push(Event::Window(window::Event::RedrawRequested(Instant::now())));
let pre_restore = take_pending_scroll_restores(&mut handle.state);
let cache = std::mem::take(&mut handle.cache);
let mut ui = UserInterface::build(
handle.state.view(),
@ -559,6 +563,7 @@ fn render(handle: &mut ViewportHandle) {
};
if messages.is_empty() {
apply_scroll_restores(&mut ui, &handle.renderer, pre_restore);
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
handle.cache = ui.into_cache();
} else {
@ -566,12 +571,18 @@ fn render(handle: &mut ViewportHandle) {
for msg in messages.drain(..) {
handle.state.update(msg);
}
let post_restore = take_pending_scroll_restores(&mut handle.state);
let combined = ScrollRestore {
sidebar: post_restore.sidebar.or(pre_restore.sidebar),
settings: post_restore.settings.or(pre_restore.settings),
};
let mut ui = UserInterface::build(
handle.state.view(),
Size::new(logical_size.width, logical_size.height),
cache,
&mut handle.renderer,
);
apply_scroll_restores(&mut ui, &handle.renderer, combined);
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
handle.cache = ui.into_cache();
}
@ -583,3 +594,42 @@ fn render(handle: &mut ViewportHandle) {
frame.present();
handle.needs_redraw = false;
}
/// drains scroll-restore flags from App state into a side-channel struct.
fn take_pending_scroll_restores(state: &mut App) -> ScrollRestore {
let r = ScrollRestore {
sidebar: state.restore_sidebar_scroll.then_some(state.sidebar_scroll),
settings: state.restore_settings_scroll.then_some(state.settings_scroll),
};
state.restore_sidebar_scroll = false;
state.restore_settings_scroll = false;
r
}
/// pushes the stored sidebar and settings scroll offsets onto the UserInterface.
fn apply_scroll_restores(
ui: &mut UserInterface<'_, Message, Theme, iced_wgpu::Renderer>,
renderer: &iced_wgpu::Renderer,
restore: ScrollRestore,
) {
if let Some(off) = restore.sidebar {
let mut op = scrollable_op::scroll_to::<()>(
player::sidebar_scroll_id(),
AbsoluteOffset { x: Some(off.x), y: Some(off.y) },
);
ui.operate(renderer, &mut op);
}
if let Some(off) = restore.settings {
let mut op = scrollable_op::scroll_to::<()>(
player::settings_scroll_id(),
AbsoluteOffset { x: Some(off.x), y: Some(off.y) },
);
ui.operate(renderer, &mut op);
}
}
/// scroll offsets staged for the UserInterface::operate pass.
struct ScrollRestore {
sidebar: Option<AbsoluteOffset>,
settings: Option<AbsoluteOffset>,
}