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, SharedScroll};
use crate::view::style::{Direction, Length, Overflow, Style};
use crate::view::styled::Styled;
use std::cell::{Cell, RefCell};
use std::ops::Range;
use std::rc::Rc;
use taffy::NodeId;
const OVERDRAW: f32 = 100.0;
const INITIAL_VIEWPORT: f32 = 400.0;
struct ListInner {
heights: Vec<Option<f32>>,
estimate: f32,
scroll: SharedScroll,
anchor: (usize, f32),
applied_offset: f32,
sticky: bool,
pinned: bool,
gap_above: f32,
}
impl ListInner {
fn height(&self, index: usize) -> f32 {
self.heights[index].unwrap_or(self.estimate)
}
fn offset_of(&self, index: usize) -> f32 {
(0..index.min(self.heights.len())).map(|index| self.height(index)).sum()
}
fn locate(&self, offset: f32) -> (usize, f32) {
let mut top = 0.0;
for index in 0..self.heights.len() {
let height = self.height(index);
if top + height > offset {
return (index, offset - top);
}
top += height;
}
(self.heights.len().saturating_sub(1), 0.0)
}
}
#[derive(Clone)]
pub struct ListState(Rc<RefCell<ListInner>>);
impl ListState {
pub fn new(count: usize, estimated_height: Pixels) -> Self {
let scroll = Rc::new(Cell::new(ScrollState { viewport_y: INITIAL_VIEWPORT, ..Default::default() }));
Self(Rc::new(RefCell::new(ListInner {
heights: vec![None; count],
estimate: estimated_height.0,
scroll,
anchor: (0, 0.0),
applied_offset: 0.0,
sticky: false,
pinned: false,
gap_above: 0.0,
})))
}
pub fn item_count(&self) -> usize {
self.0.borrow().heights.len()
}
pub fn splice(&self, range: Range<usize>, inserted: usize) {
let mut inner = self.0.borrow_mut();
let range = range.start.min(inner.heights.len())..range.end.min(inner.heights.len());
inner.heights.splice(range.clone(), std::iter::repeat_n(None, inserted));
let (top, within) = inner.anchor;
if top >= range.end {
inner.anchor = (top + inserted - range.len(), within);
} else if top >= range.start {
inner.anchor = (range.start, 0.0);
}
}
pub fn scroll_to_item(&self, index: usize) {
let mut inner = self.0.borrow_mut();
inner.anchor = (index.min(inner.heights.len().saturating_sub(1)), 0.0);
}
pub fn follow_end(&self) {
let mut inner = self.0.borrow_mut();
(inner.sticky, inner.pinned) = (true, true);
}
pub fn is_following_end(&self) -> bool {
self.0.borrow().pinned
}
pub fn measured_height(&self, index: usize) -> Option<f32> {
self.0.borrow().heights.get(index).copied().flatten()
}
fn visible_range(&self) -> Range<usize> {
let mut inner = self.0.borrow_mut();
let scroll = inner.scroll.get();
if scroll.offset_y != inner.applied_offset {
inner.anchor = inner.locate(scroll.offset_y);
inner.pinned = inner.sticky && scroll.offset_y >= scroll.max_y - 1.0;
}
let count = inner.heights.len();
if inner.pinned {
let (mut first, mut covered) = (count, 0.0);
while first > 0 && covered < scroll.viewport_y + OVERDRAW {
first -= 1;
covered += inner.height(first);
}
return first..count;
}
let first = inner.anchor.0.min(inner.heights.len());
let mut covered = -inner.anchor.1;
let mut end = first;
while end < inner.heights.len() && covered < scroll.viewport_y + OVERDRAW {
covered += inner.height(end);
end += 1;
}
first..end
}
}
pub struct List {
state: ListState,
first: usize,
items: Vec<AnyElement>,
wrappers: Vec<NodeId>,
style: Style,
node: Option<NodeId>,
}
pub fn list(state: &ListState, build: impl FnMut(usize) -> AnyElement) -> List {
let range = state.visible_range();
let style = Style { direction: Some(Direction::Column), overflow_y: Some(Overflow::Hidden), ..Default::default() };
List {
state: state.clone(),
first: range.start,
items: range.map(build).collect(),
wrappers: Vec::new(),
style,
node: None,
}
}
impl Styled for List {
fn style(&mut self) -> &mut Style {
&mut self.style
}
}
impl List {
fn measure(&self, cx: &ElementContext, viewport: f32) -> bool {
let mut inner = self.state.0.borrow_mut();
let mut changed = false;
for (offset, wrapper) in self.wrappers.iter().enumerate() {
let height = Some(cx.node_bounds(*wrapper).size.height.0);
let slot = &mut inner.heights[self.first + offset];
changed |= *slot != height;
*slot = height;
}
let total = inner.offset_of(inner.heights.len());
let max_y = (total - viewport).max(0.0);
let anchored = inner.offset_of(inner.anchor.0) + inner.anchor.1;
let offset = if inner.pinned { max_y } else { anchored.clamp(0.0, max_y) };
inner.anchor = inner.locate(offset);
inner.applied_offset = offset;
inner.gap_above = if inner.sticky { (viewport - total).max(0.0) } else { 0.0 };
let grab = inner.scroll.get().grab;
inner.scroll.set(ScrollState { offset_y: offset, max_y, viewport_y: viewport, grab });
changed
}
fn visit(&mut self, cx: &mut ElementContext, visit: fn(&mut AnyElement, &mut ElementContext)) {
let Some(node) = self.node else { return };
let bounds = cx.node_bounds(node);
let shift = {
let inner = self.state.0.borrow();
inner.applied_offset - inner.offset_of(self.first) - inner.gap_above
};
let origin = bounds.origin - Point::new(Pixels::ZERO, Pixels(shift));
let wrappers = &self.wrappers;
let items = &mut self.items;
cx.with_mask(bounds, |cx| {
cx.with_origin(origin, |cx| {
for (item, wrapper) in items.iter_mut().zip(wrappers) {
let at = cx.node_bounds(*wrapper).origin;
cx.with_origin(at, |cx| visit(item, cx));
}
})
});
}
}
impl Element for List {
fn request_layout(&mut self, cx: &mut ElementContext) -> NodeId {
let wrapper_style = Style { width: Some(Length::Percent(1.0)), flex_shrink: Some(0.0), ..Default::default() };
self.wrappers = self
.items
.iter_mut()
.map(|item| {
let child = item.request_layout(cx);
cx.request_node(&wrapper_style, &[child])
})
.collect();
let node = cx.request_node(&self.style, &self.wrappers);
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);
if self.measure(cx, bounds.size.height.0) {
cx.request_animation_frame();
}
let hitbox = cx.insert_hitbox(bounds, false);
let scroll = self.state.0.borrow().scroll.clone();
cx.register_handlers(HandlerEntry { scroll: Some(scroll), ..HandlerEntry::new(hitbox) });
self.visit(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);
self.visit(cx, |item, cx| item.paint(cx));
let scroll = self.state.0.borrow().scroll.clone();
cx.scrollbar(bounds, &scroll, &[]);
}
}