use std::{
ops::Range,
time::Duration,
};
use freya_core::prelude::*;
use freya_sdk::timeout::use_timeout;
use torin::{
geometry::CursorPoint,
node::Node,
prelude::Direction,
size::Size,
};
use crate::scrollviews::{
ScrollBar,
ScrollConfig,
ScrollController,
ScrollThumb,
shared::{
Axis,
get_container_sizes,
get_corrected_scroll_position,
get_scroll_position_from_cursor,
get_scroll_position_from_wheel,
get_scrollbar_pos_and_size,
handle_key_event,
is_scrollbar_visible,
},
use_scroll_controller,
};
#[cfg_attr(feature = "docs",
doc = embed_doc_image::embed_image!("virtual_scrollview", "images/gallery_virtual_scrollview.png")
)]
#[derive(Clone)]
pub struct VirtualScrollView<D, B: Fn(usize, &D) -> Element> {
builder: B,
builder_data: D,
item_size: f32,
length: usize,
layout: LayoutData,
show_scrollbar: bool,
scroll_with_arrows: bool,
scroll_controller: Option<ScrollController>,
invert_scroll_wheel: bool,
drag_scrolling: bool,
key: DiffKey,
}
impl<D: PartialEq, B: Fn(usize, &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(usize, &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> {
fn eq(&self, other: &Self) -> bool {
self.builder_data == other.builder_data
&& self.item_size == other.item_size
&& self.length == other.length
&& self.layout == other.layout
&& self.show_scrollbar == other.show_scrollbar
&& self.scroll_with_arrows == other.scroll_with_arrows
&& self.scroll_controller == other.scroll_controller
&& self.invert_scroll_wheel == other.invert_scroll_wheel
}
}
impl<B: Fn(usize, &()) -> Element> VirtualScrollView<(), B> {
pub fn new(builder: B) -> Self {
Self {
builder,
builder_data: (),
item_size: 0.,
length: 0,
layout: {
let mut l = LayoutData::default();
l.layout.width = Size::fill();
l.layout.height = Size::fill();
l
},
show_scrollbar: true,
scroll_with_arrows: true,
scroll_controller: None,
invert_scroll_wheel: false,
drag_scrolling: cfg!(target_os = "android"),
key: DiffKey::None,
}
}
pub fn new_controlled(builder: B, scroll_controller: ScrollController) -> Self {
Self {
builder,
builder_data: (),
item_size: 0.,
length: 0,
layout: {
let mut l = LayoutData::default();
l.layout.width = Size::fill();
l.layout.height = Size::fill();
l
},
show_scrollbar: true,
scroll_with_arrows: true,
scroll_controller: Some(scroll_controller),
invert_scroll_wheel: false,
drag_scrolling: cfg!(target_os = "android"),
key: DiffKey::None,
}
}
}
impl<D, B: Fn(usize, &D) -> Element> VirtualScrollView<D, B> {
pub fn new_with_data(builder_data: D, builder: B) -> Self {
Self {
builder,
builder_data,
item_size: 0.,
length: 0,
layout: Node {
width: Size::fill(),
height: Size::fill(),
..Default::default()
}
.into(),
show_scrollbar: true,
scroll_with_arrows: true,
scroll_controller: None,
invert_scroll_wheel: false,
drag_scrolling: cfg!(target_os = "android"),
key: DiffKey::None,
}
}
pub fn new_with_data_controlled(
builder_data: D,
builder: B,
scroll_controller: ScrollController,
) -> Self {
Self {
builder,
builder_data,
item_size: 0.,
length: 0,
layout: Node {
width: Size::fill(),
height: Size::fill(),
..Default::default()
}
.into(),
show_scrollbar: true,
scroll_with_arrows: true,
scroll_controller: Some(scroll_controller),
invert_scroll_wheel: false,
drag_scrolling: cfg!(target_os = "android"),
key: DiffKey::None,
}
}
pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
self.show_scrollbar = show_scrollbar;
self
}
pub fn direction(mut self, direction: Direction) -> Self {
self.layout.direction = direction;
self
}
pub fn scroll_with_arrows(mut self, scroll_with_arrows: impl Into<bool>) -> Self {
self.scroll_with_arrows = scroll_with_arrows.into();
self
}
pub fn item_size(mut self, item_size: impl Into<f32>) -> Self {
self.item_size = item_size.into();
self
}
pub fn length(mut self, length: impl Into<usize>) -> Self {
self.length = length.into();
self
}
pub fn invert_scroll_wheel(mut self, invert_scroll_wheel: impl Into<bool>) -> Self {
self.invert_scroll_wheel = invert_scroll_wheel.into();
self
}
pub fn drag_scrolling(mut self, drag_scrolling: bool) -> Self {
self.drag_scrolling = drag_scrolling;
self
}
pub fn scroll_controller(
mut self,
scroll_controller: impl Into<Option<ScrollController>>,
) -> Self {
self.scroll_controller = scroll_controller.into();
self
}
pub fn max_width(mut self, max_width: impl Into<Size>) -> Self {
self.layout.maximum_width = max_width.into();
self
}
pub fn max_height(mut self, max_height: impl Into<Size>) -> Self {
self.layout.maximum_height = max_height.into();
self
}
}
impl<D: PartialEq + 'static, B: Fn(usize, &D) -> Element + 'static> Component
for VirtualScrollView<D, B>
{
fn render(self: &VirtualScrollView<D, B>) -> impl IntoElement {
let focus = use_focus();
let mut timeout = use_timeout(|| Duration::from_millis(800));
let mut pressing_shift = use_state(|| false);
let mut clicking_scrollbar = use_state::<Option<(Axis, f64)>>(|| None);
let mut size = use_state(SizedEventData::default);
let mut scroll_controller = self
.scroll_controller
.unwrap_or_else(|| use_scroll_controller(ScrollConfig::default));
let mut dragging_content = use_state::<Option<CursorPoint>>(|| None);
let mut drag_origin = use_state::<Option<CursorPoint>>(|| None);
let (scrolled_x, scrolled_y) = scroll_controller.into();
let layout = &self.layout.layout;
let direction = layout.direction;
let drag_scrolling = self.drag_scrolling;
let (inner_width, inner_height) = match direction {
Direction::Vertical => (
size.read().inner_sizes.width,
self.item_size * self.length as f32,
),
Direction::Horizontal => (
self.item_size * self.length as f32,
size.read().inner_sizes.height,
),
};
scroll_controller.use_apply(inner_width, inner_height);
let corrected_scrolled_x =
get_corrected_scroll_position(inner_width, size.read().area.width(), scrolled_x as f32);
let corrected_scrolled_y = get_corrected_scroll_position(
inner_height,
size.read().area.height(),
scrolled_y as f32,
);
let horizontal_scrollbar_is_visible = !timeout.elapsed()
&& is_scrollbar_visible(self.show_scrollbar, inner_width, size.read().area.width());
let vertical_scrollbar_is_visible = !timeout.elapsed()
&& is_scrollbar_visible(self.show_scrollbar, inner_height, size.read().area.height());
let (scrollbar_x, scrollbar_width) =
get_scrollbar_pos_and_size(inner_width, size.read().area.width(), corrected_scrolled_x);
let (scrollbar_y, scrollbar_height) = get_scrollbar_pos_and_size(
inner_height,
size.read().area.height(),
corrected_scrolled_y,
);
let (container_width, content_width) = get_container_sizes(self.layout.width.clone());
let (container_height, content_height) = get_container_sizes(self.layout.height.clone());
let scroll_with_arrows = self.scroll_with_arrows;
let invert_scroll_wheel = self.invert_scroll_wheel;
let on_capture_global_pointer_press = move |e: Event<PointerEventData>| {
if clicking_scrollbar.read().is_some() {
e.prevent_default();
clicking_scrollbar.set(None);
}
if drag_scrolling && (dragging_content().is_some() || drag_origin().is_some()) {
dragging_content.set(None);
drag_origin.set(None);
}
};
let on_wheel = move |e: Event<WheelEventData>| {
let invert_direction = e.source == WheelSource::Device
&& (*pressing_shift.read() || invert_scroll_wheel)
&& (!*pressing_shift.read() || !invert_scroll_wheel);
let (x_movement, y_movement) = if invert_direction {
(e.delta_y as f32, e.delta_x as f32)
} else {
(e.delta_x as f32, e.delta_y as f32)
};
let scroll_position_y = get_scroll_position_from_wheel(
y_movement,
inner_height,
size.read().area.height(),
corrected_scrolled_y,
);
scroll_controller.scroll_to_y(scroll_position_y).then(|| {
e.stop_propagation();
});
let scroll_position_x = get_scroll_position_from_wheel(
x_movement,
inner_width,
size.read().area.width(),
corrected_scrolled_x,
);
scroll_controller.scroll_to_x(scroll_position_x).then(|| {
e.stop_propagation();
});
timeout.reset();
};
let on_mouse_move = move |_| {
timeout.reset();
};
let on_capture_global_pointer_move = move |e: Event<PointerEventData>| {
if drag_scrolling {
if let Some(prev) = dragging_content() {
let coords = e.global_location();
let delta = prev - coords;
scroll_controller.scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
scroll_controller.scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
dragging_content.set(Some(coords));
e.prevent_default();
timeout.reset();
return;
} else if let Some(origin) = drag_origin() {
let coords = e.global_location();
let distance = (origin - coords).abs();
const DRAG_THRESHOLD: f64 = 2.0;
if distance.x > DRAG_THRESHOLD || distance.y > DRAG_THRESHOLD {
let delta = origin - coords;
scroll_controller
.scroll_to_y((corrected_scrolled_y - delta.y as f32) as i32);
scroll_controller
.scroll_to_x((corrected_scrolled_x - delta.x as f32) as i32);
dragging_content.set(Some(coords));
e.prevent_default();
timeout.reset();
}
return;
}
}
let clicking_scrollbar = clicking_scrollbar.peek();
if let Some((Axis::Y, y)) = *clicking_scrollbar {
let coordinates = e.element_location();
let cursor_y = coordinates.y - y - size.read().area.min_y() as f64;
let scroll_position = get_scroll_position_from_cursor(
cursor_y as f32,
inner_height,
size.read().area.height(),
);
scroll_controller.scroll_to_y(scroll_position);
} else if let Some((Axis::X, x)) = *clicking_scrollbar {
let coordinates = e.element_location();
let cursor_x = coordinates.x - x - size.read().area.min_x() as f64;
let scroll_position = get_scroll_position_from_cursor(
cursor_x as f32,
inner_width,
size.read().area.width(),
);
scroll_controller.scroll_to_x(scroll_position);
}
if clicking_scrollbar.is_some() {
e.prevent_default();
timeout.reset();
if !focus.is_focused() {
focus.request_focus();
}
}
};
let on_key_down = move |e: Event<KeyboardEventData>| {
if !scroll_with_arrows
&& (e.key == Key::Named(NamedKey::ArrowUp)
|| e.key == Key::Named(NamedKey::ArrowRight)
|| e.key == Key::Named(NamedKey::ArrowDown)
|| e.key == Key::Named(NamedKey::ArrowLeft))
{
return;
}
let x = corrected_scrolled_x;
let y = corrected_scrolled_y;
let inner_height = inner_height;
let inner_width = inner_width;
let viewport_height = size.read().area.height();
let viewport_width = size.read().area.width();
if let Some((x, y)) = handle_key_event(
&e.key,
(x, y),
inner_height,
inner_width,
viewport_height,
viewport_width,
direction,
) {
scroll_controller.scroll_to_x(x as i32);
scroll_controller.scroll_to_y(y as i32);
e.stop_propagation();
timeout.reset();
}
};
let on_global_key_down = move |e: Event<KeyboardEventData>| {
let data = e;
if data.key == Key::Named(NamedKey::Shift) {
pressing_shift.set(true);
}
};
let on_global_key_up = move |e: Event<KeyboardEventData>| {
let data = e;
if data.key == Key::Named(NamedKey::Shift) {
pressing_shift.set(false);
}
};
let (viewport_size, scroll_position) = if direction == Direction::vertical() {
(size.read().area.height(), corrected_scrolled_y)
} else {
(size.read().area.width(), corrected_scrolled_x)
};
let render_range = get_render_range(
viewport_size,
scroll_position,
self.item_size,
self.length as f32,
);
let children = render_range
.map(|i| (self.builder)(i, &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)
}
};
let on_pointer_down = move |e: Event<PointerEventData>| {
if drag_scrolling {
drag_origin.set(Some(e.global_location()));
focus.request_focus();
timeout.reset();
}
};
rect()
.width(layout.width.clone())
.height(layout.height.clone())
.a11y_id(focus.a11y_id())
.a11y_focusable(false)
.a11y_role(AccessibilityRole::ScrollView)
.a11y_builder(move |node| {
node.set_scroll_x(corrected_scrolled_x as f64);
node.set_scroll_y(corrected_scrolled_y as f64)
})
.scrollable(true)
.on_wheel(on_wheel)
.on_capture_global_pointer_press(on_capture_global_pointer_press)
.on_mouse_move(on_mouse_move)
.on_capture_global_pointer_move(on_capture_global_pointer_move)
.on_key_down(on_key_down)
.on_global_key_up(on_global_key_up)
.on_global_key_down(on_global_key_down)
.on_pointer_down(on_pointer_down)
.child(
rect()
.width(container_width)
.height(container_height)
.horizontal()
.child(
rect()
.direction(direction)
.width(content_width)
.height(content_height)
.offset_x(offset_x)
.offset_y(offset_y)
.overflow(Overflow::Clip)
.on_sized(move |e: Event<SizedEventData>| {
size.set_if_modified(e.clone())
})
.children(children),
)
.maybe_child(vertical_scrollbar_is_visible.then_some({
rect().child(ScrollBar {
theme: None,
clicking_scrollbar,
axis: Axis::Y,
offset: scrollbar_y,
size: Size::px(size.read().area.height()),
thumb: ScrollThumb {
theme: None,
clicking_scrollbar,
axis: Axis::Y,
size: scrollbar_height,
},
})
})),
)
.maybe_child(horizontal_scrollbar_is_visible.then_some({
rect().child(ScrollBar {
theme: None,
clicking_scrollbar,
axis: Axis::X,
offset: scrollbar_x,
size: Size::px(size.read().area.width()),
thumb: ScrollThumb {
theme: None,
clicking_scrollbar,
axis: Axis::X,
size: scrollbar_width,
},
})
}))
}
fn render_key(&self) -> DiffKey {
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)
}