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.map_or(0.0, |padding| padding.vertical as f32);
232 let border = own.border.map_or(0.0, |border| border.width.max(0.0));
233 (height.get() as f32 - 2.0 * (padding + border)).max(0.0)
234 })
235 };
236 let all = cx.signal(Vec::<T>::new());
237 cx.effect(move || all.set(items()));
238 let count = cx.memo(move || all.with(Vec::len));
239 let shown = move || {
241 let total = count.get() as f32 * row_height.get().max(1) as f32;
242 let most = (total - inner.get()).max(0.0);
243 let offset = offset.get();
244 if offset.is_nan() {
245 0.0
246 } else {
247 offset.clamp(0.0, most)
248 }
249 };
250 let range = cx.memo(move || {
253 let row = row_height.get().max(1) as f32;
254 let top = shown();
255 let first = ((top / row).floor() as usize).saturating_sub(1);
256 let last = (((top + inner.get()) / row).ceil() as usize + 1).min(count.get());
257 first..last.max(first)
258 });
259 if let Some(current) = current {
260 let current = current.shared();
261 cx.effect(move || {
262 let Some(index) = current() else {
263 return;
264 };
265 let row = row_height.get() as f32;
266 let (top, bottom) = (index as f32 * row, (index + 1) as f32 * row);
267 let viewport = inner.get();
268 let now = untrack(shown);
269 let next = if top < now {
270 top
271 } else if bottom > now + viewport {
272 bottom - viewport
273 } else {
274 now
275 };
276 if next != now {
277 offset.set(next);
278 }
279 });
280 }
281 let rows = Column::new()
282 .style(move || Style::new().height(count.get() as f32 * row_height.get() as f32))
283 .child(
284 Stack::new()
285 .style(move || Style::new().height(range.get().start as f32 * row_height.get() as f32)),
286 )
287 .children_keyed(
288 move || {
289 let range = range.get();
290 all.with(|all| {
291 let end = range.end.min(all.len());
292 all[range.start.min(end)..end].to_vec()
293 })
294 },
295 move |item| key(item),
296 move |cx, item| {
297 Stack::new()
298 .style(move || Style::new().height(row_height.get() as f32))
299 .child(view(cx, item))
300 },
301 );
302 let mut scroll = ScrollView::new(rows).offset(offset);
303 scroll = scroll.style(move || {
304 let own = style.as_ref().map(|style| style()).unwrap_or_default();
305 own.height(height.get() as f32)
306 });
307 Box::new(scroll).into_element()
308}
309
310fn prop_fn<T: Clone + 'static>(prop: Option<Prop<T>>, default: T) -> impl FnMut() -> T + 'static {
312 let prop = prop.map(Prop::shared);
313 move || match prop.as_ref() {
314 Some(prop) => prop(),
315 None => default.clone(),
316 }
317}