1use std::hash::Hash;
5
6use crabshell_reactive::{Memo, Signal, untrack};
7
8use crate::cx::Cx;
9use crate::prop::{IntoProp, Prop};
10use crate::style::Style;
11use crate::widgets::{Column, Element, ElementChild, ElementKind, Stack, Widget};
12
13pub(crate) const PIXELS_PER_NOTCH: f32 = 48.0;
15
16pub(crate) struct ScrollProps {
17 pub(crate) offset: Option<Signal<f32>>,
18}
19
20pub struct ScrollView {
31 scroll: ScrollProps,
32 style: Option<Prop<Style>>,
33 child: Box<dyn Widget>,
34}
35
36impl ScrollView {
37 pub fn new(child: impl Widget) -> ScrollView {
39 ScrollView {
40 scroll: ScrollProps { offset: None },
41 style: None,
42 child: Box::new(child),
43 }
44 }
45
46 pub fn offset(mut self, offset: Signal<f32>) -> ScrollView {
50 self.scroll.offset = Some(offset);
51 self
52 }
53
54 pub fn style(mut self, style: impl IntoProp<Style>) -> ScrollView {
57 self.style = Some(style.into_prop());
58 self
59 }
60}
61
62impl Widget for ScrollView {
63 fn into_element(self: Box<Self>) -> Element {
64 let ScrollView {
65 scroll,
66 style,
67 child,
68 } = *self;
69 Element {
70 name: "ScrollView",
71 style,
72 kind: ElementKind::Scroll(scroll),
73 children: vec![ElementChild::One(Box::new(child.into_element()))],
74 }
75 }
76}
77
78pub struct ListView {
97 build: Build,
99 props: ListProps,
100}
101
102type Build = Box<dyn FnOnce(&Cx, ListProps) -> Element>;
104
105struct ListProps {
106 row_height: Option<Prop<u32>>,
107 height: Option<Prop<u32>>,
108 current: Option<Prop<Option<usize>>>,
109 offset: Option<Signal<f32>>,
110 style: Option<Prop<Style>>,
111}
112
113impl ListView {
114 pub const DEFAULT_ROW_HEIGHT: u32 = 24;
116 pub const DEFAULT_HEIGHT: u32 = 240;
118
119 pub fn new<T, K, W>(
122 items: impl FnMut() -> Vec<T> + 'static,
123 key: impl FnMut(&T) -> K + 'static,
124 view: impl FnMut(&Cx, Memo<T>) -> W + 'static,
125 ) -> ListView
126 where
127 T: Clone + PartialEq + 'static,
128 K: Eq + Hash + 'static,
129 W: Widget,
130 {
131 ListView {
132 build: Box::new(move |cx, props| build(cx, props, items, key, view)),
133 props: ListProps {
134 row_height: None,
135 height: None,
136 current: None,
137 offset: None,
138 style: None,
139 },
140 }
141 }
142
143 pub fn row_height(mut self, height: impl IntoProp<u32>) -> ListView {
145 self.props.row_height = Some(height.into_prop());
146 self
147 }
148
149 pub fn height(mut self, height: impl IntoProp<u32>) -> ListView {
151 self.props.height = Some(height.into_prop());
152 self
153 }
154
155 pub fn current(mut self, index: impl IntoProp<Option<usize>>) -> ListView {
158 self.props.current = Some(index.into_prop());
159 self
160 }
161
162 pub fn offset(mut self, offset: Signal<f32>) -> ListView {
164 self.props.offset = Some(offset);
165 self
166 }
167
168 pub fn style(mut self, style: impl IntoProp<Style>) -> ListView {
170 self.props.style = Some(style.into_prop());
171 self
172 }
173}
174
175impl Widget for ListView {
176 fn into_element(self: Box<Self>) -> Element {
177 let ListView { build, props } = *self;
178 let mut once = Some((build, props));
179 let mut element = Box::new(Stack::new().children_keyed(
182 || vec![()],
183 |_| (),
184 move |cx, _| Built(once.take().map(|(build, props)| build(cx, props))),
185 ))
186 .into_element();
187 element.name = "ListView";
188 element
189 }
190}
191
192struct Built(Option<Element>);
194
195impl Widget for Built {
196 fn into_element(self: Box<Self>) -> Element {
197 self
198 .0
199 .unwrap_or_else(|| Box::new(Stack::new()).into_element())
200 }
201}
202
203fn build<T, K, W>(
204 cx: &Cx,
205 props: ListProps,
206 mut items: impl FnMut() -> Vec<T> + 'static,
207 mut key: impl FnMut(&T) -> K + 'static,
208 mut view: impl FnMut(&Cx, Memo<T>) -> W + 'static,
209) -> Element
210where
211 T: Clone + PartialEq + 'static,
212 K: Eq + Hash + 'static,
213 W: Widget,
214{
215 let ListProps {
216 row_height,
217 height,
218 current,
219 offset,
220 style,
221 } = props;
222 let offset = offset.unwrap_or_else(|| cx.signal(0.0));
223 let row_height = cx.memo(prop_fn(row_height, ListView::DEFAULT_ROW_HEIGHT));
224 let height = cx.memo(prop_fn(height, ListView::DEFAULT_HEIGHT));
225 let style = style.map(Prop::shared);
226 let inner = {
228 let style = style.clone();
229 cx.memo(move || {
230 let own = style.as_ref().map(|style| style()).unwrap_or_default();
231 let padding = own.padding.unwrap_or_default();
232 let border = own.border.map_or(0.0, |border| border.width.max(0.0));
233 let insets = padding.top as f32 + padding.bottom as f32 + 2.0 * border;
234 (height.get() as f32 - insets).max(0.0)
235 })
236 };
237 let all = cx.signal(Vec::<T>::new());
238 cx.effect(move || all.set(items()));
239 let count = cx.memo(move || all.with(Vec::len));
240 let shown = move || {
242 let total = count.get() as f32 * row_height.get().max(1) as f32;
243 let most = (total - inner.get()).max(0.0);
244 let offset = offset.get();
245 if offset.is_nan() {
246 0.0
247 } else {
248 offset.clamp(0.0, most)
249 }
250 };
251 let range = cx.memo(move || {
254 let row = row_height.get().max(1) as f32;
255 let top = shown();
256 let first = ((top / row).floor() as usize).saturating_sub(1);
257 let last = (((top + inner.get()) / row).ceil() as usize + 1).min(count.get());
258 first..last.max(first)
259 });
260 if let Some(current) = current {
261 let current = current.shared();
262 cx.effect(move || {
263 let Some(index) = current() else {
264 return;
265 };
266 let row = row_height.get() as f32;
267 let (top, bottom) = (index as f32 * row, (index + 1) as f32 * row);
268 let viewport = inner.get();
269 let now = untrack(shown);
270 let next = if top < now {
271 top
272 } else if bottom > now + viewport {
273 bottom - viewport
274 } else {
275 now
276 };
277 if next != now {
278 offset.set(next);
279 }
280 });
281 }
282 let rows = Column::new()
283 .style(move || Style::new().height(count.get() as f32 * row_height.get() as f32))
284 .child(
285 Stack::new()
286 .style(move || Style::new().height(range.get().start as f32 * row_height.get() as f32)),
287 )
288 .children_keyed(
289 move || {
290 let range = range.get();
291 all.with(|all| {
292 let end = range.end.min(all.len());
293 all[range.start.min(end)..end].to_vec()
294 })
295 },
296 move |item| key(item),
297 move |cx, item| {
298 Stack::new()
299 .style(move || Style::new().height(row_height.get() as f32))
300 .child(view(cx, item))
301 },
302 );
303 let mut scroll = ScrollView::new(rows).offset(offset);
304 scroll = scroll.style(move || {
305 let own = style.as_ref().map(|style| style()).unwrap_or_default();
306 own.height(height.get() as f32)
307 });
308 Box::new(scroll).into_element()
309}
310
311fn prop_fn<T: Clone + 'static>(prop: Option<Prop<T>>, default: T) -> impl FnMut() -> T + 'static {
313 let prop = prop.map(Prop::shared);
314 move || match prop.as_ref() {
315 Some(prop) => prop(),
316 None => default.clone(),
317 }
318}