use std::hash::Hash;
use crabshell_reactive::{Memo, Signal, untrack};
use crate::cx::Cx;
use crate::prop::{IntoProp, Prop};
use crate::style::Style;
use crate::widgets::{Column, Element, ElementChild, ElementKind, Stack, Widget};
pub(crate) const PIXELS_PER_NOTCH: f32 = 48.0;
pub(crate) struct ScrollProps {
pub(crate) offset: Option<Signal<f32>>,
}
pub struct ScrollView {
scroll: ScrollProps,
style: Option<Prop<Style>>,
child: Box<dyn Widget>,
}
impl ScrollView {
pub fn new(child: impl Widget) -> ScrollView {
ScrollView {
scroll: ScrollProps { offset: None },
style: None,
child: Box::new(child),
}
}
pub fn offset(mut self, offset: Signal<f32>) -> ScrollView {
self.scroll.offset = Some(offset);
self
}
pub fn style(mut self, style: impl IntoProp<Style>) -> ScrollView {
self.style = Some(style.into_prop());
self
}
}
impl Widget for ScrollView {
fn into_element(self: Box<Self>) -> Element {
let ScrollView {
scroll,
style,
child,
} = *self;
Element {
name: "ScrollView",
style,
kind: ElementKind::Scroll(scroll),
children: vec![ElementChild::One(Box::new(child.into_element()))],
}
}
}
pub struct ListView {
build: Build,
props: ListProps,
}
type Build = Box<dyn FnOnce(&Cx, ListProps) -> Element>;
struct ListProps {
row_height: Option<Prop<u32>>,
height: Option<Prop<u32>>,
current: Option<Prop<Option<usize>>>,
offset: Option<Signal<f32>>,
style: Option<Prop<Style>>,
}
impl ListView {
pub const DEFAULT_ROW_HEIGHT: u32 = 24;
pub const DEFAULT_HEIGHT: u32 = 240;
pub fn new<T, K, W>(
items: impl FnMut() -> Vec<T> + 'static,
key: impl FnMut(&T) -> K + 'static,
view: impl FnMut(&Cx, Memo<T>) -> W + 'static,
) -> ListView
where
T: Clone + PartialEq + 'static,
K: Eq + Hash + 'static,
W: Widget,
{
ListView {
build: Box::new(move |cx, props| build(cx, props, items, key, view)),
props: ListProps {
row_height: None,
height: None,
current: None,
offset: None,
style: None,
},
}
}
pub fn row_height(mut self, height: impl IntoProp<u32>) -> ListView {
self.props.row_height = Some(height.into_prop());
self
}
pub fn height(mut self, height: impl IntoProp<u32>) -> ListView {
self.props.height = Some(height.into_prop());
self
}
pub fn current(mut self, index: impl IntoProp<Option<usize>>) -> ListView {
self.props.current = Some(index.into_prop());
self
}
pub fn offset(mut self, offset: Signal<f32>) -> ListView {
self.props.offset = Some(offset);
self
}
pub fn style(mut self, style: impl IntoProp<Style>) -> ListView {
self.props.style = Some(style.into_prop());
self
}
}
impl Widget for ListView {
fn into_element(self: Box<Self>) -> Element {
let ListView { build, props } = *self;
let mut once = Some((build, props));
let mut element = Box::new(Stack::new().children_keyed(
|| vec![()],
|_| (),
move |cx, _| Built(once.take().map(|(build, props)| build(cx, props))),
))
.into_element();
element.name = "ListView";
element
}
}
struct Built(Option<Element>);
impl Widget for Built {
fn into_element(self: Box<Self>) -> Element {
self
.0
.unwrap_or_else(|| Box::new(Stack::new()).into_element())
}
}
fn build<T, K, W>(
cx: &Cx,
props: ListProps,
mut items: impl FnMut() -> Vec<T> + 'static,
mut key: impl FnMut(&T) -> K + 'static,
mut view: impl FnMut(&Cx, Memo<T>) -> W + 'static,
) -> Element
where
T: Clone + PartialEq + 'static,
K: Eq + Hash + 'static,
W: Widget,
{
let ListProps {
row_height,
height,
current,
offset,
style,
} = props;
let offset = offset.unwrap_or_else(|| cx.signal(0.0));
let row_height = cx.memo(prop_fn(row_height, ListView::DEFAULT_ROW_HEIGHT));
let height = cx.memo(prop_fn(height, ListView::DEFAULT_HEIGHT));
let style = style.map(Prop::shared);
let inner = {
let style = style.clone();
cx.memo(move || {
let own = style.as_ref().map(|style| style()).unwrap_or_default();
let padding = own.padding.map_or(0.0, |padding| padding.vertical as f32);
let border = own.border.map_or(0.0, |border| border.width.max(0.0));
(height.get() as f32 - 2.0 * (padding + border)).max(0.0)
})
};
let all = cx.signal(Vec::<T>::new());
cx.effect(move || all.set(items()));
let count = cx.memo(move || all.with(Vec::len));
let shown = move || {
let total = count.get() as f32 * row_height.get().max(1) as f32;
let most = (total - inner.get()).max(0.0);
let offset = offset.get();
if offset.is_nan() {
0.0
} else {
offset.clamp(0.0, most)
}
};
let range = cx.memo(move || {
let row = row_height.get().max(1) as f32;
let top = shown();
let first = ((top / row).floor() as usize).saturating_sub(1);
let last = (((top + inner.get()) / row).ceil() as usize + 1).min(count.get());
first..last.max(first)
});
if let Some(current) = current {
let current = current.shared();
cx.effect(move || {
let Some(index) = current() else {
return;
};
let row = row_height.get() as f32;
let (top, bottom) = (index as f32 * row, (index + 1) as f32 * row);
let viewport = inner.get();
let now = untrack(shown);
let next = if top < now {
top
} else if bottom > now + viewport {
bottom - viewport
} else {
now
};
if next != now {
offset.set(next);
}
});
}
let rows = Column::new()
.style(move || Style::new().height(count.get() as f32 * row_height.get() as f32))
.child(
Stack::new()
.style(move || Style::new().height(range.get().start as f32 * row_height.get() as f32)),
)
.children_keyed(
move || {
let range = range.get();
all.with(|all| {
let end = range.end.min(all.len());
all[range.start.min(end)..end].to_vec()
})
},
move |item| key(item),
move |cx, item| {
Stack::new()
.style(move || Style::new().height(row_height.get() as f32))
.child(view(cx, item))
},
);
let mut scroll = ScrollView::new(rows).offset(offset);
scroll = scroll.style(move || {
let own = style.as_ref().map(|style| style()).unwrap_or_default();
own.height(height.get() as f32)
});
Box::new(scroll).into_element()
}
fn prop_fn<T: Clone + 'static>(prop: Option<Prop<T>>, default: T) -> impl FnMut() -> T + 'static {
let prop = prop.map(Prop::shared);
move || match prop.as_ref() {
Some(prop) => prop(),
None => default.clone(),
}
}