use crate::core::{Pixels, Point};
use crate::view::any_element::AnyElement;
use crate::view::context::ElementContext;
use crate::view::element::Element;
use crate::view::interactivity::{HandlerEntry, ScrollState};
use crate::view::style::{Direction, Length, Overflow, Style};
use crate::view::styled::Styled;
use std::cell::Cell;
use std::ops::Range;
use std::rc::Rc;
use taffy::NodeId;
const INITIAL_VIEWPORT: f32 = 400.0;
#[derive(Clone)]
pub struct UniformListState(Rc<Cell<ScrollState>>);
impl Default for UniformListState {
fn default() -> Self {
Self(Rc::new(Cell::new(ScrollState { viewport_y: INITIAL_VIEWPORT, ..Default::default() })))
}
}
impl UniformListState {
pub fn visible_range(&self, count: usize, item_height: f32) -> Range<usize> {
let state = self.0.get();
let first = (state.offset_y / item_height).floor().max(0.0) as usize;
let visible = (state.viewport_y / item_height).ceil() as usize + 2;
first.min(count)..(first + visible).min(count)
}
pub fn scroll_to_item(&self, index: usize, item_height: f32) {
let mut state = self.0.get();
let top = index as f32 * item_height;
if top < state.offset_y {
state.offset_y = top;
} else if top + item_height > state.offset_y + state.viewport_y {
state.offset_y = top + item_height - state.viewport_y;
}
self.0.set(state);
}
pub fn reset(&self) {
let mut state = self.0.get();
state.offset_y = 0.0;
self.0.set(state);
}
}
pub struct UniformList {
state: UniformListState,
count: usize,
item_height: f32,
first: usize,
items: Vec<AnyElement>,
style: Style,
node: Option<NodeId>,
}
pub fn uniform_list(
state: &UniformListState,
count: usize,
item_height: Pixels,
build: impl FnOnce(Range<usize>) -> Vec<AnyElement>,
) -> UniformList {
let range = state.visible_range(count, item_height.0);
let style = Style { direction: Some(Direction::Column), overflow_y: Some(Overflow::Hidden), ..Default::default() };
UniformList {
state: state.clone(),
count,
item_height: item_height.0,
first: range.start,
items: build(range),
style,
node: None,
}
}
impl Styled for UniformList {
fn style(&mut self) -> &mut Style {
&mut self.style
}
}
impl Element for UniformList {
fn request_layout(&mut self, cx: &mut ElementContext) -> NodeId {
let row_style = Style {
width: Some(Length::Percent(1.0)),
height: Some(Length::Px(Pixels(self.item_height))),
flex_shrink: Some(0.0),
..Default::default()
};
let rows: Vec<NodeId> = self
.items
.iter_mut()
.map(|item| {
let child = item.request_layout(cx);
cx.request_node(&row_style, &[child])
})
.collect();
let node = cx.request_node(&self.style, &rows);
self.node = Some(node);
node
}
fn prepaint(&mut self, cx: &mut ElementContext) {
let Some(node) = self.node else { return };
let bounds = cx.node_bounds(node);
let mut state = self.state.0.get();
state.viewport_y = bounds.size.height.0;
state.max_y = (self.count as f32 * self.item_height - state.viewport_y).max(0.0);
state.offset_y = state.offset_y.clamp(0.0, state.max_y);
self.state.0.set(state);
let hitbox = cx.insert_hitbox(bounds, false);
cx.register_handlers(HandlerEntry { scroll: Some(self.state.0.clone()), ..HandlerEntry::new(hitbox) });
let origin = self.content_origin(bounds.origin, state.offset_y);
let height = self.item_height;
cx.with_mask(bounds, |cx| for_each_row(&mut self.items, origin, height, cx, |item, cx| item.prepaint(cx)));
}
fn paint(&mut self, cx: &mut ElementContext) {
let Some(node) = self.node else { return };
let bounds = cx.node_bounds(node);
let state = self.state.0.get();
let origin = self.content_origin(bounds.origin, state.offset_y);
let height = self.item_height;
cx.with_mask(bounds, |cx| for_each_row(&mut self.items, origin, height, cx, |item, cx| item.paint(cx)));
cx.scrollbar(bounds, &self.state.0, &[]);
}
}
fn for_each_row(
items: &mut [AnyElement],
origin: Point,
height: f32,
cx: &mut ElementContext,
visit: fn(&mut AnyElement, &mut ElementContext),
) {
for (index, item) in items.iter_mut().enumerate() {
let row_origin = origin + Point::new(Pixels::ZERO, Pixels(index as f32 * height));
cx.with_origin(row_origin, |cx| visit(item, cx));
}
}
impl UniformList {
fn content_origin(&self, origin: Point, offset: f32) -> Point {
origin + Point::new(Pixels::ZERO, Pixels(self.first as f32 * self.item_height - offset))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn range_covers_only_visible_rows() {
let state = UniformListState::default();
assert_eq!(state.visible_range(10_000, 20.0), 0..22);
state.scroll_to_item(500, 20.0);
let range = state.visible_range(10_000, 20.0);
assert!(range.contains(&500) && range.len() <= 22);
}
}