liked songs list

This commit is contained in:
Daniel Bulant 2024-11-17 20:04:54 +01:00
parent 780bfabaa5
commit edcb40559f
No known key found for this signature in database
10 changed files with 728 additions and 674 deletions

709
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
use std::future::Future;
use std::sync::Arc;
use std::time::{Duration, Instant};
use chrono::TimeDelta;
@ -6,7 +7,7 @@ use futures_util::lock::Mutex;
use librespot_core::Session;
use librespot_oauth::OAuthToken;
use reqwest::StatusCode;
use rspotify::model::{Page, PrivateUser, SimplifiedPlaylist, UserId};
use rspotify::model::{Page, PrivateUser, SavedTrack, SimplifiedPlaylist, UserId};
use rspotify::prelude::*;
use rspotify::{AuthCodeSpotify, ClientError, ClientResult, Config, Token};
use rspotify::http::HttpError;
@ -20,6 +21,8 @@ pub struct SpotifyContext {
token: Mutex<OAuthToken>
}
pub type SpotifyContextRef = Arc<SpotifyContext>;
impl SpotifyContext {
pub fn new(session: Session, token: OAuthToken) -> SpotifyContext {
let config = Config {
@ -112,6 +115,10 @@ impl SpotifyContext {
self.api_with_retry(|api| api.current_user_playlists_manual(limit, offset)).await.ok_or(())
}
pub async fn current_user_saved_tracks(&self, limit: Option<u32>, offset: Option<u32>) -> Result<Page<SavedTrack>, ()> {
self.api_with_retry(|api| api.current_user_saved_tracks_manual(None, limit, offset)).await.ok_or(())
}
}
fn librespot_token_to_rspotify(token: &OAuthToken) -> Token {

View file

@ -1,40 +1,30 @@
use std::thread;
use api::SpotifyContext;
use api::{SpotifyContext, SpotifyContextRef};
use auth::get_token;
use clap::Parser;
use cli::Args;
use cushy::{figures::units::Lp, styles::Dimension, value::Dynamic, widget::MakeWidget, window::MakeWindow, Application, Open, PendingApp, Run, TokioRuntime};
use cushy::{
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::{VirtualListContent, VirtualList}, ActivePage};
use librespot_playback::{
audio_backend,
config::{AudioFormat, PlayerConfig},
mixer::NoOpVolume,
player::Player,
};
use widgets::{library::playlist::playlists_widget, pages::liked::LikedSongsPage, ActivePage};
mod vibrancy;
mod theme;
mod cli;
mod auth;
mod widgets;
mod rt;
mod api;
#[derive(Debug)]
struct TestVirtualList;
impl VirtualListContent for TestVirtualList {
fn item_count(&self) -> impl cushy::value::IntoValue<usize> {
50
}
fn item_height(&self) -> impl cushy::value::IntoValue<cushy::styles::Dimension> {
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<cushy::styles::Dimension> {
Dimension::Lp(Lp::inches_f(10.))
}
}
mod auth;
mod cli;
mod nodebug;
mod rt;
mod theme;
mod vibrancy;
mod widgets;
fn main() -> cushy::Result {
let args = Args::parse();
@ -47,36 +37,44 @@ fn main() -> cushy::Result {
let audio_format = AudioFormat::default();
let credentials = Credentials::with_access_token(&token.access_token);
let backend = audio_backend::find(None).unwrap();
let session;
{
let guard = app.cushy().enter_runtime();
session = Session::new(session_config, None);
let player = Player::new(player_config, session.clone(), Box::new(NoOpVolume), move || {
backend(None, audio_format)
});
tokio::spawn({ let session = session.clone(); async move {
if let Err(e) = session.connect(credentials, false).await {
println!("Error connecting: {}", e);
let player = Player::new(
player_config,
session.clone(),
Box::new(NoOpVolume),
move || backend(None, audio_format),
);
tokio::spawn({
let session = session.clone();
async move {
if let Err(e) = session.connect(credentials, false).await {
println!("Error connecting: {}", e);
}
}
}});
});
thread::spawn(move || {
let mut channel = player.get_player_event_channel();
loop {
let event = channel.blocking_recv();
if let Some(event) = event {
dbg!(event);
} else { break; }
} else {
break;
}
}
});
dbg!(session.user_data());
let context = SpotifyContext::new(session, token);
let context = SpotifyContextRef::new(SpotifyContext::new(session, token));
let mut app = app.as_app();
tokio::spawn(async move {
@ -88,11 +86,10 @@ fn main() -> cushy::Result {
let selected_page = Dynamic::new(ActivePage::default());
// playlists_widget(playlists.items, selected_page)
// .and(
VirtualList::new(TestVirtualList)
// )
// .into_columns()
playlists_widget(playlists.items, selected_page)
.and(LikedSongsPage::new(context.clone()).into_widget())
.into_columns()
.expand()
.make_window()
.open(&mut app)
.unwrap();
@ -102,4 +99,4 @@ fn main() -> cushy::Result {
}
app.run()
}
}

42
src/nodebug.rs Normal file
View file

@ -0,0 +1,42 @@
use std::{fmt::Debug, ops::{Deref, DerefMut}};
pub struct NoDebug<T> {
inner: T,
}
impl<T> Debug for NoDebug<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("skipped").finish()
}
}
impl<T> From<T> for NoDebug<T> {
fn from(value: T) -> Self {
Self { inner: value }
}
}
impl<T> Deref for NoDebug<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> DerefMut for NoDebug<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl<T> Default for NoDebug<T>
where
T: Default,
{
fn default() -> Self {
Self {
inner: T::default(),
}
}
}

View file

@ -1,80 +1,99 @@
use cushy::{value::{Destination, Dynamic, IntoDynamic, IntoValue, Source, Value}, widget::{MakeWidget, WidgetList}, widgets::{button::{ButtonBackground, ButtonClick, ButtonHoverBackground}, grid::Orientation, Image, Stack}};
use rspotify::model::SimplifiedPlaylist;
use cushy::kludgine::Color;
use cushy::{
value::{Destination, Dynamic, IntoDynamic, IntoValue, Source, Value},
widget::{MakeWidget, WidgetList},
widgets::{
button::{ButtonBackground, ButtonClick, ButtonHoverBackground},
grid::Orientation,
Image, Stack,
},
};
use rspotify::model::SimplifiedPlaylist;
use crate::{theme::{LIBRARY_BG, LIBRARY_BG_HOVER, LIBRARY_BG_SELECTED, LIBRARY_BG_SELECTED_HOVER}, widgets::{image::ImageExt, ActivePage, SelectedPage}};
use crate::{
theme::{LIBRARY_BG, LIBRARY_BG_HOVER, LIBRARY_BG_SELECTED, LIBRARY_BG_SELECTED_HOVER},
widgets::{image::ImageExt, ActivePage, SelectedPage},
};
fn playlist_entry(playlist: impl IntoValue<SimplifiedPlaylist>, selected_page: SelectedPage) -> impl MakeWidget {
fn playlist_entry(
playlist: impl IntoValue<SimplifiedPlaylist>,
selected_page: SelectedPage,
) -> impl MakeWidget {
let playlist: Value<SimplifiedPlaylist> = playlist.into_value();
let id = playlist.map(|p| p.id.clone());
let is_active = selected_page.map_each(move |page|
matches!(page, ActivePage::Playlist(p) if p.id == id)
);
let is_active =
selected_page.map_each(move |page| matches!(page, ActivePage::Playlist(p) if p.id == id));
entry(
playlist
.map_each(|p| p.name.clone())
.into_dynamic(),
playlist.map_each(|p| p.name.clone()).into_dynamic(),
playlist
.map_each(|playlist| playlist.images.first().map(|image| image.url.clone()))
.into_dynamic(),
is_active,
move |_| {
selected_page.set(ActivePage::Playlist(playlist.get()));
}
},
)
}
pub fn playlists_widget(playlists: impl IntoValue<Vec<SimplifiedPlaylist>>, selected_page: SelectedPage) -> impl MakeWidget {
pub fn playlists_widget(
playlists: impl IntoValue<Vec<SimplifiedPlaylist>>,
selected_page: SelectedPage,
) -> impl MakeWidget {
let playlists: Value<Vec<SimplifiedPlaylist>> = playlists.into_value();
Stack::new(
Orientation::Row,
playlists
.map_each(move |t| {
let mut list = t.clone().into_iter().map(|playlist| playlist_entry(playlist, selected_page.clone())).collect::<WidgetList>();
list.insert(0, liked_songs_entry(selected_page.clone()));
list
})
playlists.map_each(move |t| {
let mut list = t
.clone()
.into_iter()
.map(|playlist| playlist_entry(playlist, selected_page.clone()))
.collect::<WidgetList>();
list.insert(0, liked_songs_entry(selected_page.clone()));
list
}),
)
.vertical_scroll()
.expand()
.expand_horizontally()
}
fn liked_songs_entry(selected_page: SelectedPage) -> impl MakeWidget {
let is_active = selected_page.map_each(|page| matches!(page, ActivePage::LikedSongs));
entry(
"Liked Songs",
Dynamic::new(Some("https://misc.scdn.co/liked-songs/liked-songs-300.png".to_string())),
Dynamic::new(Some(
"https://misc.scdn.co/liked-songs/liked-songs-300.png".to_string(),
)),
is_active,
move |_| {
selected_page.set(ActivePage::LikedSongs);
}
},
)
}
fn entry<F>(text: impl IntoValue<String>, url: Dynamic<Option<String>>, is_active: Dynamic<bool>, callback: F) -> impl MakeWidget
where F: FnMut(Option<ButtonClick>) + Send + 'static {
fn entry<F>(
text: impl IntoValue<String>,
url: Dynamic<Option<String>>,
is_active: Dynamic<bool>,
callback: F,
) -> impl MakeWidget
where
F: FnMut(Option<ButtonClick>) + Send + 'static,
{
let (background, background_hover) = get_colors(is_active);
Image::new_empty()
.with_url(
url
)
.and(
text
.into_value()
.align_left()
.expand()
)
.into_columns()
.into_button()
.on_click(callback)
.with(&ButtonBackground, background)
.with(&ButtonHoverBackground, background_hover)
.pad()
.with_url(url)
.and(text.into_value().align_left().expand())
.into_columns()
.into_button()
.on_click(callback)
.with(&ButtonBackground, background)
.with(&ButtonHoverBackground, background_hover)
.pad()
}
/// Returns `background` and `background_hover` colors for a library entry.
fn get_colors(is_active: impl IntoValue<bool>) -> (Value<Color>, Value<Color>) {
let is_active= is_active.into_value();
let is_active = is_active.into_value();
let background = is_active.map_each(|active| {
if *active {
LIBRARY_BG_SELECTED
@ -90,4 +109,4 @@ fn get_colors(is_active: impl IntoValue<bool>) -> (Value<Color>, Value<Color>) {
}
});
(background, background_hover)
}
}

View file

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

100
src/widgets/pages/liked.rs Normal file
View file

@ -0,0 +1,100 @@
use std::{
collections::{HashMap, HashSet},
ops::Range,
sync::{Arc, RwLock},
};
use cushy::{
figures::{units::Lp, Size},
styles::{Dimension, DimensionRange},
value::{Destination, Dynamic, Source},
widget::MakeWidget,
widgets::VirtualList,
};
use futures_util::lock::Mutex;
use rspotify::model::SavedTrack;
use crate::{api::SpotifyContextRef, nodebug::NoDebug, rt::tokio_runtime};
const PER_PAGE: usize = 50;
#[derive(Debug)]
pub struct LikedSongsPage {
tracks: Dynamic<HashMap<usize, SavedTrack>>,
total_tracks: Dynamic<usize>,
context: NoDebug<SpotifyContextRef>,
pages_loading: Arc<RwLock<HashSet<usize>>>,
}
impl LikedSongsPage {
pub fn new(context: SpotifyContextRef) -> Self {
Self {
context: context.into(),
tracks: Default::default(),
total_tracks: Default::default(),
pages_loading: Default::default(),
}
}
pub fn into_widget(self) -> impl MakeWidget {
let tracks = self.tracks;
let pages_loading = self.pages_loading;
let context = self.context;
let total_tracks = self.total_tracks.clone();
VirtualList::new(
total_tracks.clone().map_each(|total| (*total).max(1)),
move |index| {
let context = context.clone();
let pages_loading = pages_loading.clone();
let total_tracks = total_tracks.clone();
let page = index / PER_PAGE;
tracks.map_ref({
let tracks = tracks.clone();
|loaded_tracks| {
if !loaded_tracks.contains_key(&index)
&& !pages_loading.read().unwrap().contains(&page)
{
pages_loading.write().unwrap().insert(page);
tokio_runtime().spawn(async move {
println!("Loading page {} idx {}", page, index);
let saved_tracks = context
.current_user_saved_tracks(
Some(PER_PAGE as _),
Some((page * PER_PAGE) as _),
)
.await;
let Ok(saved_tracks) = saved_tracks else {
eprintln!("Failed to load page {}", page);
// pages_loading.write().unwrap().remove(&page);
return;
};
println!("Loaded page {} got tracks {}", page, saved_tracks.total);
total_tracks.set(saved_tracks.total as usize);
tracks.map_mut(|mut tracks| {
for (i, track) in saved_tracks.items.into_iter().enumerate() {
tracks.insert(i + saved_tracks.offset as usize, track);
}
});
});
}
}
});
tracks
.map_each(move |tracks| {
if let Some(track) = tracks.get(&index) {
format!("{} - {}", track.track.name, track.track.artists[0].name)
} else {
format!("Loading...")
}
})
.size(Size {
width: DimensionRange::default(),
height: Dimension::Lp(Lp::points(60)).into(),
})
},
)
.expand_horizontally()
}
}

2
src/widgets/pages/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod liked;

View file

@ -1,35 +0,0 @@
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

@ -1,308 +0,0 @@
use std::{collections::VecDeque, fmt::Debug, ops::Range};
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 {
/// Single item height
fn item_height(&self) -> impl IntoValue<Dimension>;
/// Width of the items
fn width(&self) -> impl IntoValue<Dimension>;
/// Number of items
fn item_count(&self) -> impl IntoValue<usize>;
/// Create a widget for the item at the given index.
/// This is called when the widget comes into view. The widget may be removed at any moment (by scrolling it out of view) and recreated later.
fn widget_at(&self, index: usize) -> impl MakeWidget;
}
#[derive(Debug)]
struct VirtualListItem {
index: usize,
mounted: MountedWidget,
}
#[derive(Debug)]
/// A virtual list widget.
/// Requires a [VirtualListContent] trait implementation to render the items.
/// Items are lazily recreated as they go in and out of view.
pub struct VirtualList<T: VirtualListContent + Send + 'static> {
virtual_list: T,
vertical_scroll: OwnedWidget<ScrollBar>,
items: VecDeque<VirtualListItem>,
content_size: Dynamic<Size<UPx>>,
/// Maximum scroll value - max_scroll.y + control_size.height should be the height of the content.
pub max_scroll: DynamicReader<Point<UPx>>,
/// Current scroll value. The x value is always 0. Change the value to scroll the widget programmatically.
pub scroll: Dynamic<Point<UPx>>,
control_size: Dynamic<Size<UPx>>,
/// Height of an item. Based on [VirtualListContent::item_height].
pub item_height: DynamicReader<Dimension>,
/// Width of the items. Based on [VirtualListContent::width].
pub width: DynamicReader<Dimension>,
/// Number of items. Based on [VirtualListContent::item_count].
pub item_count: DynamicReader<usize>,
visible_range: Dynamic<Range<usize>>
}
impl<T: VirtualListContent + Send + 'static> VirtualList<T> {
pub fn new(virtual_list: T) -> Self {
let scroll = Dynamic::<Point<UPx>>::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());
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();
Self {
virtual_list,
vertical_scroll: OwnedWidget::new(vertical),
items: VecDeque::new(),
control_size: Dynamic::new(Size::default()),
content_size,
max_scroll,
scroll,
item_height,
width,
item_count,
visible_range: Default::default()
}
}
/// 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<Point<UPx>> {
&self.max_scroll
}
/// Returns a reader for the size of the scrollable area.
#[must_use]
pub fn content_size(&self) -> DynamicReader<Size<UPx>> {
self.content_size.create_reader()
}
/// Returns a reader for the size of this Scroll widget.
#[must_use]
pub fn control_size(&self) -> DynamicReader<Size<UPx>> {
self.control_size.create_reader()
}
/// Returns a reader for number of visible items. 0 indexed.
#[must_use]
pub fn visible_range(&self) -> DynamicReader<Range<usize>> {
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::<ScrollBar>()
.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::<ScrollBar>()
.expect("a ScrollBar")
.hide(context);
}
}
impl<T: VirtualListContent + Send + 'static> Widget for VirtualList<T> {
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_>) -> bool {
true
}
fn hover(
&mut self,
_location: Point<Px>,
context: &mut EventContext<'_>,
) -> Option<CursorIcon> {
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<cushy::ConstraintLimit>,
context: &mut cushy::context::LayoutContext<'_, '_, '_, '_>,
) -> Size<UPx> {
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::<ScrollBar>()
.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),
}
}