first attempt at virtual list

This commit is contained in:
Daniel Bulant 2024-10-25 16:03:36 +02:00
parent 4d8a702380
commit e51e057d85
No known key found for this signature in database
5 changed files with 282 additions and 171 deletions

335
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,10 +4,10 @@ use api::SpotifyContext;
use auth::get_token;
use clap::Parser;
use cli::Args;
use cushy::{value::Dynamic, widget::MakeWidget, window::MakeWindow, Application, Open, PendingApp, Run, TokioRuntime};
use cushy::{figures::units::Lp, 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, ActivePage};
use widgets::{library::playlist::playlists_widget, virtual_list::{virtual_list, VirtualList}, ActivePage};
mod vibrancy;
mod theme;
@ -17,6 +17,21 @@ mod widgets;
mod rt;
mod api;
struct TestVirtualList;
impl VirtualList for TestVirtualList {
fn item_count(&self) -> impl cushy::value::IntoDynamic<usize> {
Dynamic::new(100)
}
fn item_height(&self) -> impl cushy::value::IntoDynamic<cushy::styles::Dimension> {
Dynamic::new(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 main() -> cushy::Result {
let args = Args::parse();
let mut app = PendingApp::new(TokioRuntime::default());
@ -70,6 +85,10 @@ fn main() -> cushy::Result {
let selected_page = Dynamic::new(ActivePage::default());
playlists_widget(playlists.items, selected_page)
.and(
virtual_list(TestVirtualList)
)
.into_columns()
.make_window()
.open(&mut app)
.unwrap();

View file

@ -3,6 +3,8 @@ use rspotify::model::{SimplifiedAlbum, SimplifiedPlaylist};
pub mod image;
pub mod library;
pub mod virtual_list;
pub mod probe;
#[derive(PartialEq, Debug, Default)]
pub enum ActivePage {

35
src/widgets/probe.rs Normal file
View file

@ -0,0 +1,35 @@
use cushy::{figures::Fraction, value::{Destination, Dynamic, DynamicReader, IntoReadOnly, ReadOnly}, widget::{MakeWidget, Widget, WidgetRef}};
#[derive(Debug)]
pub struct ScalingProbe {
child: WidgetRef,
scale: Dynamic<Fraction>
}
impl ScalingProbe {
pub fn new(child: impl MakeWidget) -> Self {
Self {
child: WidgetRef::new(child),
scale: Dynamic::new(Fraction::new_whole(1))
}
}
pub fn scale(&self) -> DynamicReader<Fraction> {
self.scale.create_reader()
}
}
impl Widget for ScalingProbe {
fn redraw(&mut self, context: &mut cushy::context::GraphicsContext<'_, '_, '_, '_>) {
self.scale.set(context.gfx.scale());
context.for_other(&self.child).expect("A child").redraw();
}
fn layout(
&mut self,
available_space: cushy::figures::Size<cushy::ConstraintLimit>,
context: &mut cushy::context::LayoutContext<'_, '_, '_, '_>,
) -> cushy::figures::Size<cushy::figures::units::UPx> {
let child = self.child.mounted(context);
context.for_other(&child).layout(available_space)
}
}

View file

@ -0,0 +1,58 @@
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;
pub trait VirtualList {
fn item_height(&self) -> impl IntoDynamic<Dimension>;
// fn width(&self) -> impl IntoDynamic<DimensionRange>;
fn item_count(&self) -> impl IntoDynamic<usize>;
fn widget_at(&self, index: usize) -> impl MakeWidget;
}
pub fn virtual_list<T>(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);
// 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);
let item_height = list.item_height().into_dynamic();
let item_count = list.item_count().into_dynamic();
let probe = ScalingProbe::new(scroll);
let scale = probe.scale();
// let width = list.width().into_dynamic();
// (&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 handle = (&current_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);
let list = (start as usize..end as usize).map(|index| list.widget_at(index));
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;
padding.set(Edges::ZERO.with_top(padding_start).with_bottom(padding_end));
contents.set(WidgetList::from_iter(list));
});
handle.persist();
probe
}