From d5ec39ea531f1bae5c2af1be947ccf6814fc76f6 Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Fri, 25 Oct 2024 21:55:24 +0200 Subject: [PATCH] working virtual list --- src/main.rs | 28 ++-- src/widgets/mod.rs | 1 + src/widgets/owned.rs | 56 +++++++ src/widgets/virtual_list.rs | 320 +++++++++++++++++++++++++++++++----- 4 files changed, 351 insertions(+), 54 deletions(-) create mode 100644 src/widgets/owned.rs diff --git a/src/main.rs b/src/main.rs index ade266f..8e1f62d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,10 +4,10 @@ use api::SpotifyContext; use auth::get_token; use clap::Parser; use cli::Args; -use cushy::{figures::units::Lp, value::Dynamic, widget::MakeWidget, window::MakeWindow, Application, Open, PendingApp, Run, TokioRuntime}; +use cushy::{figures::units::Lp, styles::Dimension, value::Dynamic, widget::MakeWidget, window::MakeWindow, Application, Open, PendingApp, Run, TokioRuntime}; use librespot_core::{authentication::Credentials, Session, SessionConfig}; use librespot_playback::{audio_backend, config::{AudioFormat, PlayerConfig}, mixer::NoOpVolume, player::Player}; -use widgets::{library::playlist::playlists_widget, virtual_list::{virtual_list, VirtualList}, ActivePage}; +use widgets::{library::playlist::playlists_widget, virtual_list::{VirtualListContent, VirtualList}, ActivePage}; mod vibrancy; mod theme; @@ -17,19 +17,23 @@ mod widgets; mod rt; mod api; +#[derive(Debug)] struct TestVirtualList; -impl VirtualList for TestVirtualList { - fn item_count(&self) -> impl cushy::value::IntoDynamic { - Dynamic::new(100) +impl VirtualListContent for TestVirtualList { + fn item_count(&self) -> impl cushy::value::IntoValue { + 50 } - fn item_height(&self) -> impl cushy::value::IntoDynamic { - Dynamic::new(cushy::styles::Dimension::Lp(Lp::inches_f(0.5))) + fn item_height(&self) -> impl cushy::value::IntoValue { + cushy::styles::Dimension::Lp(Lp::inches_f(0.5)) } fn widget_at(&self, index: usize) -> impl MakeWidget { // println!("Creating item {}", index); format!("Item {}", index) } + fn width(&self) -> impl cushy::value::IntoValue { + Dimension::Lp(Lp::inches_f(10.)) + } } fn main() -> cushy::Result { @@ -84,11 +88,11 @@ fn main() -> cushy::Result { let selected_page = Dynamic::new(ActivePage::default()); - playlists_widget(playlists.items, selected_page) - .and( - virtual_list(TestVirtualList) - ) - .into_columns() + // playlists_widget(playlists.items, selected_page) + // .and( + VirtualList::new(TestVirtualList) + // ) + // .into_columns() .make_window() .open(&mut app) .unwrap(); diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index b0dae3e..037db5e 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -5,6 +5,7 @@ pub mod image; pub mod library; pub mod virtual_list; pub mod probe; +pub mod owned; #[derive(PartialEq, Debug, Default)] pub enum ActivePage { diff --git a/src/widgets/owned.rs b/src/widgets/owned.rs new file mode 100644 index 0000000..5f74683 --- /dev/null +++ b/src/widgets/owned.rs @@ -0,0 +1,56 @@ +use cushy::widget::{Widget, WidgetRef}; + + +#[derive(Debug)] +pub struct OwnedWidget(OwnedWidgetState); + +#[derive(Debug)] +enum OwnedWidgetState { + Unmade(W), + Making, + Made(WidgetRef), +} + +impl OwnedWidget +where + W: Widget, +{ + pub const fn new(widget: W) -> Self { + Self(OwnedWidgetState::Unmade(widget)) + } + + pub fn make_if_needed(&mut self) -> &mut WidgetRef { + if matches!(&self.0, OwnedWidgetState::Unmade(_)) { + let OwnedWidgetState::Unmade(widget) = + std::mem::replace(&mut self.0, OwnedWidgetState::Making) + else { + unreachable!("just matched") + }; + + self.0 = OwnedWidgetState::Made(WidgetRef::new(widget)); + } + + self.expect_made_mut() + } + + pub fn expect_made(&self) -> &WidgetRef { + let OwnedWidgetState::Made(widget) = &self.0 else { + unreachable!("widget made") + }; + widget + } + + pub fn expect_made_mut(&mut self) -> &mut WidgetRef { + let OwnedWidgetState::Made(widget) = &mut self.0 else { + unreachable!("widget made") + }; + widget + } + + pub fn expect_unmade_mut(&mut self) -> &mut W { + let OwnedWidgetState::Unmade(widget) = &mut self.0 else { + unreachable!("widget unmade") + }; + widget + } +} \ No newline at end of file diff --git a/src/widgets/virtual_list.rs b/src/widgets/virtual_list.rs index a1efa2a..def922c 100644 --- a/src/widgets/virtual_list.rs +++ b/src/widgets/virtual_list.rs @@ -1,58 +1,294 @@ -use cushy::{figures::{Round, ScreenScale, Size, Zero}, styles::{Dimension, DimensionRange, Edges}, value::{Destination, Dynamic, ForEach, IntoDynamic, MapEach, Source}, widget::{MakeWidget, WidgetList}, widgets::{Container, Scroll, Stack}}; -use crate::widgets::probe::ScalingProbe; +use std::{collections::VecDeque, fmt::Debug, ops::Range}; -pub trait VirtualList { - fn item_height(&self) -> impl IntoDynamic; - // fn width(&self) -> impl IntoDynamic; - fn item_count(&self) -> impl IntoDynamic; +use cushy::{context::{AsEventContext, EventContext}, figures::{units::{Px, UPx}, IntoSigned, Point, Rect, Round, ScreenScale, Size, Zero}, kludgine::app::winit::{event::{ MouseScrollDelta, TouchPhase}, window::CursorIcon}, styles::Dimension, value::{Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, Source}, widget::{EventHandling, MakeWidget, MountedWidget, Widget, HANDLED, IGNORED}, widgets::scroll::ScrollBar, window::DeviceId, ConstraintLimit}; + +use super::owned::OwnedWidget; + +/// A virtual list contents. +/// This simple virtual list assumes that all items have the same height, width and that the item count is known. +/// All the values are dynamic, so the list will update when the values change. +pub trait VirtualListContent: Debug { + fn item_height(&self) -> impl IntoValue; + fn width(&self) -> impl IntoValue; + fn item_count(&self) -> impl IntoValue; fn widget_at(&self, index: usize) -> impl MakeWidget; } -pub fn virtual_list(list: T) -> impl MakeWidget -where - T: VirtualList + Send + 'static -{ - let contents = Dynamic::default(); - let stack = Stack::rows(contents.clone()); - let padding = Dynamic::default(); - let container = Container::new(stack).transparent().pad_by(padding.clone()); - let scroll = Scroll::vertical(container); +#[derive(Debug)] +struct VirtualListItem { + index: usize, + mounted: MountedWidget, +} - // Current scroll position - let current_scroll = scroll.scroll.clone().map_each(|scroll| scroll.y); - // height of the scroll widget - let visible_size = scroll.control_size().map_each(|size| size.height); - // max scroll position. Height of contents is max_scroll + visible_size - // let max_scroll = scroll.max_scroll().map_each(|size| size.y); +#[derive(Debug)] +pub struct VirtualList { + virtual_list: T, + vertical_scroll: OwnedWidget, + items: VecDeque, + content_size: Dynamic>, + pub max_scroll: DynamicReader>, + pub scroll: Dynamic>, + control_size: Dynamic>, - let item_height = list.item_height().into_dynamic(); - let item_count = list.item_count().into_dynamic(); + pub item_height: DynamicReader, + pub width: DynamicReader, + pub item_count: DynamicReader, - let probe = ScalingProbe::new(scroll); - let scale = probe.scale(); + visible_range: Dynamic> +} - // let width = list.width().into_dynamic(); +impl VirtualList { + pub fn new(virtual_list: T) -> Self { + let scroll = Dynamic::>::default(); + let item_height = virtual_list.item_height().into_value().into_dynamic().create_reader(); + let width = virtual_list.width().into_value().into_dynamic().create_reader(); + let item_count = virtual_list.item_count().into_value().into_dynamic().create_reader(); + let content_size = Dynamic::new(Size::default()); - // (&width, &item_height, &item_count, &scale).map_each(|(width, item_height, item_count, scale)| { - // Size::new(*width, *item_height.into_upx(*scale) * *item_count as f32) - // }); + let y = scroll.map_each_cloned(|scroll| scroll.y); + y.for_each_cloned({ + let scroll = scroll.clone(); + move |y| { + if let Ok(mut scroll) = scroll.try_lock() { + if scroll.y != y { + scroll.y = y; + } + } + } + }) + .persist(); + let vertical = + ScrollBar::new(content_size.map_each_cloned(|size| size.height), y, true); + let max_scroll = (&vertical.max_scroll()) + .map_each_cloned(|y| Point::new(UPx::ZERO, y)) + .into_reader(); - let handle = (¤t_scroll, &item_height, &item_count, &scale, &visible_size).for_each(move |(current_scroll, item_height, item_count, scale, visible_size)| { - let start = (*current_scroll / item_height.into_upx(*scale)).floor().get(); - let end = ((*current_scroll + *visible_size) / item_height.into_upx(*scale)).ceil().get().min(*item_count as _); - println!("Start: {}, End: {}", start, end); + Self { + virtual_list, + vertical_scroll: OwnedWidget::new(vertical), + items: VecDeque::new(), + control_size: Dynamic::new(Size::default()), + content_size, + max_scroll, + scroll, - let list = (start as usize..end as usize).map(|index| list.widget_at(index)); + item_height, + width, + item_count, + visible_range: Default::default() + } + } - let padding_start = *item_height * start as i32; - let items_end = (*item_count as u32).saturating_sub(end); - let padding_end = *item_height * items_end as i32; + /// Returns a reader for the maximum scroll value. + /// + /// This represents the maximum amount that the scroll can be moved by. + #[must_use] + pub const fn max_scroll(&self) -> &DynamicReader> { + &self.max_scroll + } - padding.set(Edges::ZERO.with_top(padding_start).with_bottom(padding_end)); + /// Returns a reader for the size of the scrollable area. + #[must_use] + pub fn content_size(&self) -> DynamicReader> { + self.content_size.create_reader() + } - contents.set(WidgetList::from_iter(list)); - }); - handle.persist(); + /// Returns a reader for the size of this Scroll widget. + #[must_use] + pub fn control_size(&self) -> DynamicReader> { + self.control_size.create_reader() + } - probe + #[must_use] + pub fn visible_range(&self) -> DynamicReader> { + self.visible_range.create_reader() + } + + fn show_scrollbars(&mut self, context: &mut EventContext<'_>) { + let mut vertical = self.vertical_scroll.expect_made_mut().widget().lock(); + vertical + .downcast_mut::() + .expect("a ScrollBar") + .show(context); + } + + fn hide_scrollbars(&mut self, context: &mut EventContext<'_>) { + let mut vertical = self.vertical_scroll.expect_made_mut().widget().lock(); + vertical + .downcast_mut::() + .expect("a ScrollBar") + .hide(context); + } +} + +impl Widget for VirtualList { + fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_>) -> bool { + true + } + + fn hover( + &mut self, + _location: Point, + context: &mut EventContext<'_>, + ) -> Option { + self.show_scrollbars(context); + + None + } + + fn unhover(&mut self, context: &mut EventContext<'_>) { + self.hide_scrollbars(context); + } + + fn mounted(&mut self, context: &mut EventContext<'_>) { + for child in &mut self.items { + child.mounted.remount_if_needed(context); + } + } + + fn redraw(&mut self, context: &mut cushy::context::GraphicsContext<'_, '_, '_, '_>) { + for child in &mut self.items { + context.for_other(&child.mounted).redraw(); + } + let vertical = self + .vertical_scroll + .expect_made_mut() + .mounted(&mut context.as_event_context()); + context.for_other(&vertical).redraw(); + } + + fn layout( + &mut self, + available_space: Size, + context: &mut cushy::context::LayoutContext<'_, '_, '_, '_>, + ) -> Size { + let item_height = self.item_height.get_tracking_invalidate(context); + let item_height_upx = item_height.into_upx(context.gfx.scale()); + let item_count = self.item_count.get_tracking_invalidate(context); + let content_height = item_height * item_count as i32; + let content_height = content_height.into_upx(context.gfx.scale()); + let width = self.width.get_tracking_invalidate(context); + let width = width.into_upx(context.gfx.scale()); + + let new_control_size = Size::new( + width, + constrain_child(available_space.height, content_height), + ); + + let vertical = self + .vertical_scroll + .make_if_needed() + .mounted(&mut context.as_event_context()); + let scrollbar_layout = context.for_other(&vertical).layout(available_space); + context.set_child_layout( + &vertical, + Rect::new( + Point::new( + available_space.width + .fit_measured(new_control_size.width) + .saturating_sub(scrollbar_layout.width) + .into_signed(), + Px::ZERO, + ), + scrollbar_layout.into_signed(), + ), + ); + let scroll = self.scroll.get_tracking_invalidate(context); + + let start_item = (scroll.y / item_height_upx).floor().get() as usize; + let end_item = ((scroll.y + new_control_size.height) / item_height_upx).ceil().get() as usize; + let end_item = end_item.min(item_count-1); + + self.visible_range.set(start_item..end_item); + + let first = self.items.front().map(|t| t.index); + let last = self.items.back().map(|t| t.index); + let mut closure = |index| { + let widget = self.virtual_list.widget_at(index); + let mut widget = widget.widget_ref(); + let mounted = widget.mounted(&mut context.as_event_context()); + VirtualListItem { index, mounted } + }; + if self.items.is_empty() || first.unwrap() > end_item || last.unwrap() < start_item { + self.items.clear(); + self.items.extend((start_item..=end_item).map(closure)); + } else { + let first = first.expect("List is not empty"); + let last = last.expect("List is not empty"); + if first < start_item { + while self.items.front().is_some() && self.items.front().expect("Checked is some").index < start_item { + self.items.pop_front(); + } + } + if last > end_item { + while self.items.back().is_some() && self.items.back().expect("Checked is some").index > end_item { + self.items.pop_back(); + } + } + // no extend front :( + for item in (start_item..first).rev() { + self.items.push_front(closure(item)); + } + self.items.extend(((last+1)..=end_item).map(closure)); + } + + let item_size = Size::new(width, item_height_upx); + let constraint = item_size.map(ConstraintLimit::Fill); + + for item in &self.items { + context.for_other(&item.mounted).layout(constraint); + } + + let item_size = item_size.into_signed(); + let scroll = self.scroll.get_tracking_invalidate(context).into_signed(); + + for item in &self.items { + context.set_child_layout( + &item.mounted, + Rect::new( + Point::new(Px::ZERO, (item_height_upx * item.index as f32).into_signed() - scroll.y), + item_size, + ) + ); + } + + self.control_size.set(new_control_size); + self.content_size.set(Size::new(width, content_height)); + + new_control_size + } + + fn mouse_wheel( + &mut self, + _device_id: DeviceId, + delta: MouseScrollDelta, + _phase: TouchPhase, + context: &mut EventContext<'_>, + ) -> EventHandling { + let mut handled = false; + { + let mut vertical = self.vertical_scroll.expect_made().widget().lock(); + handled |= vertical + .downcast_mut::() + .expect("a ScrollBar") + .mouse_wheel(delta, context) + .is_break(); + } + if handled { + self.show_scrollbars(context); + context.set_needs_redraw(); + + HANDLED + } else { + IGNORED + } + } +} + +fn constrain_child(constraint: ConstraintLimit, measured: UPx) -> UPx { + match constraint { + ConstraintLimit::Fill(size) => size.min(measured), + // change from Scroll widget: returning just measured here would break the functionality (render too many items) + ConstraintLimit::SizeToFit(size) => size.min(measured), + } } \ No newline at end of file