Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/freya-code-editor/src/editor_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,12 +276,12 @@ impl Component for CodeEditor {
})
.on_global_pointer_press(on_global_pointer_press)
.child(
VirtualScrollView::new(move |line_index, _| {
VirtualScrollView::new(move |item, _| {
EditorLineUI {
editor: editor.clone(),
font_size,
line_height,
line_index,
line_index: item.index,
read_only,
gutter,
show_whitespace,
Expand Down
231 changes: 169 additions & 62 deletions crates/freya-components/src/scrollviews/virtual_scrollview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,128 @@ use crate::scrollviews::{
use_scroll_controller,
};

/// Defines how each item of a [`VirtualScrollView`] is sized along the scroll axis.
///
/// Build one from a fixed value or from a closure that resolves the size of each
/// item by its index:
///
/// ```rust
/// # use freya::prelude::*;
/// let fixed: ItemSize = 25.0f32.into();
/// let dynamic: ItemSize =
/// (|index: usize| if index.is_multiple_of(2) { 25.0 } else { 50.0 }).into();
/// ```
#[derive(Clone, PartialEq)]
pub enum ItemSize {
/// Every item shares the same size in pixels.
Fixed(f32),
/// Each item is sized individually through a callback that receives its index.
Dynamic(Callback<usize, f32>),
}

impl ItemSize {
/// Size in pixels of the item at `index`.
fn at(&self, index: usize) -> f32 {
match self {
Self::Fixed(size) => *size,
Self::Dynamic(callback) => callback.call(index),
}
}

/// Range of items that fall inside the viewport for the given scroll position,
/// together with the offset that positions the first visible item correctly.
fn visible_range(
&self,
viewport_size: f32,
scroll_position: f32,
length: usize,
) -> (Range<usize>, f32) {
let scroll_distance = (-scroll_position).max(0.0);
match self {
Self::Fixed(size) => {
if *size <= 0.0 {
return (0..0, 0.0);
}
let start = scroll_distance / size;
let potentially_visible = (viewport_size / size) + 1.0;
let start_index = (start as usize).min(length);
let end_index = ((start + potentially_visible) as usize).min(length);
(
start_index..end_index,
start.floor() * size - scroll_distance,
)
}
Self::Dynamic(callback) => {
let mut start = 0;
let mut cumulative = 0.0;
while start < length {
let item = callback.call(start);
if cumulative + item > scroll_distance {
break;
}
cumulative += item;
start += 1;
}
let offset = cumulative - scroll_distance;
let mut end = start;
while end < length && cumulative < scroll_distance + viewport_size {
cumulative += callback.call(end);
end += 1;
}
(start..end, offset)
}
}
}

/// Total size of the content along the scroll axis.
///
/// [`Self::Fixed`] is exact. [`Self::Dynamic`] extrapolates from the average size of
/// the items down to the viewport bottom, keeping the scrollbar stable as it scrolls.
fn total_size(&self, viewport_size: f32, scroll_position: f32, length: usize) -> f32 {
match self {
Self::Fixed(size) => size * length as f32,
Self::Dynamic(callback) => {
if length == 0 {
return 0.0;
}
let viewport_bottom = (-scroll_position).max(0.0) + viewport_size;
let mut measured = callback.call(0);
let mut count = 1;
while count < length && measured < viewport_bottom {
measured += callback.call(count);
count += 1;
}
(measured / count as f32) * length as f32
}
}
}
}

impl Default for ItemSize {
fn default() -> Self {
Self::Fixed(0.)
}
}

impl From<f32> for ItemSize {
fn from(size: f32) -> Self {
Self::Fixed(size)
}
}

impl<F: Fn(usize) -> f32 + 'static> From<F> for ItemSize {
fn from(callback: F) -> Self {
Self::Dynamic(Callback::new(callback))
}
}

/// Data passed to a [`VirtualScrollView`] builder for each rendered item.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VirtualItem {
pub index: usize,
pub size: f32,
}

/// One-direction scrollable area that dynamically builds and renders items based in their size and current available size,
/// this is intended for apps using large sets of data that need good performance.
///
Expand All @@ -39,12 +161,12 @@ use crate::scrollviews::{
/// # use freya::prelude::*;
/// fn app() -> impl IntoElement {
/// rect().child(
/// VirtualScrollView::new(|i, _| {
/// VirtualScrollView::new(|item, _| {
/// rect()
/// .key(i)
/// .height(Size::px(25.))
/// .key(item.index)
/// .height(Size::px(item.size))
/// .padding(4.)
/// .child(format!("Item {i}"))
/// .child(format!("Item {}", item.index))
/// .into()
/// })
/// .length(300usize)
Expand All @@ -67,10 +189,10 @@ use crate::scrollviews::{
doc = embed_doc_image::embed_image!("virtual_scrollview", "images/gallery_virtual_scrollview.png")
)]
#[derive(Clone)]
pub struct VirtualScrollView<D, B: Fn(usize, &D) -> Element> {
pub struct VirtualScrollView<D, B: Fn(VirtualItem, &D) -> Element> {
builder: B,
builder_data: D,
item_size: f32,
item_size: ItemSize,
length: usize,
layout: LayoutData,
show_scrollbar: bool,
Expand All @@ -81,21 +203,21 @@ pub struct VirtualScrollView<D, B: Fn(usize, &D) -> Element> {
key: DiffKey,
}

impl<D: PartialEq, B: Fn(usize, &D) -> Element> LayoutExt for VirtualScrollView<D, B> {
impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> LayoutExt for VirtualScrollView<D, B> {
fn get_layout(&mut self) -> &mut LayoutData {
&mut self.layout
}
}

impl<D: PartialEq, B: Fn(usize, &D) -> Element> ContainerSizeExt for VirtualScrollView<D, B> {}
impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> ContainerSizeExt for VirtualScrollView<D, B> {}

impl<D: PartialEq, B: Fn(usize, &D) -> Element> KeyExt for VirtualScrollView<D, B> {
impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> KeyExt for VirtualScrollView<D, B> {
fn write_key(&mut self) -> &mut DiffKey {
&mut self.key
}
}

impl<D: PartialEq, B: Fn(usize, &D) -> Element> PartialEq for VirtualScrollView<D, B> {
impl<D: PartialEq, B: Fn(VirtualItem, &D) -> Element> PartialEq for VirtualScrollView<D, B> {
fn eq(&self, other: &Self) -> bool {
self.builder_data == other.builder_data
&& self.item_size == other.item_size
Expand All @@ -108,12 +230,13 @@ impl<D: PartialEq, B: Fn(usize, &D) -> Element> PartialEq for VirtualScrollView<
}
}

impl<B: Fn(usize, &()) -> Element> VirtualScrollView<(), B> {
impl<B: Fn(VirtualItem, &()) -> Element> VirtualScrollView<(), B> {
/// Creates a [`VirtualScrollView`] that builds each item from its [`VirtualItem`].
pub fn new(builder: B) -> Self {
Self {
builder,
builder_data: (),
item_size: 0.,
item_size: ItemSize::default(),
length: 0,
layout: {
let mut l = LayoutData::default();
Expand All @@ -130,11 +253,12 @@ impl<B: Fn(usize, &()) -> Element> VirtualScrollView<(), B> {
}
}

/// Same as [`Self::new`] but driven by an external [`ScrollController`].
pub fn new_controlled(builder: B, scroll_controller: ScrollController) -> Self {
Self {
builder,
builder_data: (),
item_size: 0.,
item_size: ItemSize::default(),
length: 0,
layout: {
let mut l = LayoutData::default();
Expand All @@ -152,12 +276,17 @@ impl<B: Fn(usize, &()) -> Element> VirtualScrollView<(), B> {
}
}

impl<D, B: Fn(usize, &D) -> Element> VirtualScrollView<D, B> {
impl<D, B: Fn(VirtualItem, &D) -> Element> VirtualScrollView<D, B> {
/// Same as [`Self::new`] but passes `builder_data` to the builder for every item.
///
/// `builder_data` is owned by the scroll view and handed to the builder by reference
/// instead of being captured in the closure. It is part of the view's equality check,
/// so changing it rebuilds the visible items.
pub fn new_with_data(builder_data: D, builder: B) -> Self {
Self {
builder,
builder_data,
item_size: 0.,
item_size: ItemSize::default(),
length: 0,
layout: Node {
width: Size::fill(),
Expand All @@ -174,6 +303,7 @@ impl<D, B: Fn(usize, &D) -> Element> VirtualScrollView<D, B> {
}
}

/// Same as [`Self::new_with_data`] but driven by an external [`ScrollController`].
pub fn new_with_data_controlled(
builder_data: D,
builder: B,
Expand All @@ -182,7 +312,7 @@ impl<D, B: Fn(usize, &D) -> Element> VirtualScrollView<D, B> {
Self {
builder,
builder_data,
item_size: 0.,
item_size: ItemSize::default(),
length: 0,

layout: Node {
Expand Down Expand Up @@ -215,7 +345,7 @@ impl<D, B: Fn(usize, &D) -> Element> VirtualScrollView<D, B> {
self
}

pub fn item_size(mut self, item_size: impl Into<f32>) -> Self {
pub fn item_size(mut self, item_size: impl Into<ItemSize>) -> Self {
self.item_size = item_size.into();
self
}
Expand Down Expand Up @@ -254,7 +384,7 @@ impl<D, B: Fn(usize, &D) -> Element> VirtualScrollView<D, B> {
}
}

impl<D: PartialEq + 'static, B: Fn(usize, &D) -> Element + 'static> Component
impl<D: PartialEq + 'static, B: Fn(VirtualItem, &D) -> Element + 'static> Component
for VirtualScrollView<D, B>
{
fn render(self: &VirtualScrollView<D, B>) -> impl IntoElement {
Expand All @@ -273,13 +403,18 @@ impl<D: PartialEq + 'static, B: Fn(usize, &D) -> Element + 'static> Component
let direction = layout.direction;
let drag_scrolling = self.drag_scrolling;

let viewport_width = size.read().area.width();
let viewport_height = size.read().area.height();

let (inner_width, inner_height) = match direction {
Direction::Vertical => (
size.read().inner_sizes.width,
self.item_size * self.length as f32,
self.item_size
.total_size(viewport_height, scrolled_y as f32, self.length),
),
Direction::Horizontal => (
self.item_size * self.length as f32,
self.item_size
.total_size(viewport_width, scrolled_x as f32, self.length),
size.read().inner_sizes.height,
),
};
Expand Down Expand Up @@ -483,37 +618,28 @@ impl<D: PartialEq + 'static, B: Fn(usize, &D) -> Element + 'static> Component
};

let (viewport_size, scroll_position) = if direction == Direction::vertical() {
(size.read().area.height(), corrected_scrolled_y)
(viewport_height, corrected_scrolled_y)
} else {
(size.read().area.width(), corrected_scrolled_x)
(viewport_width, corrected_scrolled_x)
};

let render_range = get_render_range(
viewport_size,
scroll_position,
self.item_size,
self.length as f32,
);
let (render_range, item_offset) =
self.item_size
.visible_range(viewport_size, scroll_position, self.length);

let children = render_range
.map(|i| (self.builder)(i, &self.builder_data))
.map(|i| {
let item = VirtualItem {
index: i,
size: self.item_size.at(i),
};
(self.builder)(item, &self.builder_data)
})
.collect::<Vec<Element>>();

let (offset_x, offset_y) = match direction {
Direction::Vertical => {
let offset_y_min =
(-corrected_scrolled_y / self.item_size).floor() * self.item_size;
let offset_y = -(-corrected_scrolled_y - offset_y_min);

(corrected_scrolled_x, offset_y)
}
Direction::Horizontal => {
let offset_x_min =
(-corrected_scrolled_x / self.item_size).floor() * self.item_size;
let offset_x = -(-corrected_scrolled_x - offset_x_min);

(offset_x, corrected_scrolled_y)
}
Direction::Vertical => (corrected_scrolled_x, item_offset),
Direction::Horizontal => (item_offset, corrected_scrolled_y),
};

let on_pointer_down = move |e: Event<PointerEventData>| {
Expand Down Expand Up @@ -596,22 +722,3 @@ impl<D: PartialEq + 'static, B: Fn(usize, &D) -> Element + 'static> Component
self.key.clone().or(self.default_key())
}
}

fn get_render_range(
viewport_size: f32,
scroll_position: f32,
item_size: f32,
item_length: f32,
) -> Range<usize> {
let render_index_start = (-scroll_position) / item_size;
let potentially_visible_length = (viewport_size / item_size) + 1.0;
let remaining_length = item_length - render_index_start;

let render_index_end = if remaining_length <= potentially_visible_length {
item_length
} else {
render_index_start + potentially_visible_length
};

render_index_start as usize..(render_index_end as usize)
}
Loading
Loading