1use std::cell::RefCell;
4use std::fmt;
5use std::rc::Rc;
6
7use ravenrs_core::{Color, CursorIcon, Insets, Key, Role};
8use ravenrs_layout::{Align, Axis, LayoutStyle};
9use ravenrs_reactive::State;
10use ravenrs_style::Semantic;
11
12use super::reorder::{self, Extents, Reorder};
13use crate::IconName;
14use crate::context::{BuildCx, NodeId, Paint, QuadPaint, Semantics};
15use crate::util::{self, FocusTarget, Roving};
16use crate::view::{AnyView, View};
17
18#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
39pub struct SortableList {
40 count: usize,
41 item: Rc<dyn Fn(usize) -> AnyView>,
42 key: Option<Rc<dyn Fn(usize) -> u64>>,
43 row_name: Option<Rc<dyn Fn(usize) -> String>>,
44 on_reorder: Option<Rc<dyn Fn(usize, usize)>>,
45 spacing: f32,
46 label: Option<String>,
47 grip: bool,
48}
49
50impl fmt::Debug for SortableList {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 f.debug_struct("SortableList").field("count", &self.count).finish_non_exhaustive()
53 }
54}
55
56#[cfg_attr(feature = "live", ravenrs_live::expose)]
57impl SortableList {
58 pub fn new<V: View + 'static>(count: usize, item: impl Fn(usize) -> V + 'static) -> Self {
60 Self {
61 count,
62 item: Rc::new(move |index| Box::new(item(index))),
63 key: None,
64 row_name: None,
65 on_reorder: None,
66 spacing: 2.0,
67 label: None,
68 grip: true,
69 }
70 }
71
72 pub fn key(mut self, key: impl Fn(usize) -> u64 + 'static) -> Self {
75 self.key = Some(Rc::new(key));
76 self
77 }
78
79 pub fn on_reorder(mut self, handler: impl Fn(usize, usize) + 'static) -> Self {
81 self.on_reorder = Some(Rc::new(handler));
82 self
83 }
84
85 pub fn row_name(mut self, name: impl Fn(usize) -> String + 'static) -> Self {
87 self.row_name = Some(Rc::new(name));
88 self
89 }
90
91 pub fn spacing(mut self, spacing: f32) -> Self {
93 self.spacing = spacing.max(0.0);
94 self
95 }
96
97 pub fn label(mut self, label: impl Into<String>) -> Self {
99 self.label = Some(label.into());
100 self
101 }
102
103 pub fn grip(mut self, grip: bool) -> Self {
105 self.grip = grip;
106 self
107 }
108}
109
110struct SortableRow<'a> {
111 list: &'a SortableList,
112 index: usize,
113 extents: Extents,
114 drag: State<Option<Reorder>>,
115 roving: Roving,
116 apply: Rc<dyn Fn(usize, usize)>,
117}
118
119impl View for SortableRow<'_> {
120 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
121 let theme = *cx.theme();
122 let labels = cx.labels();
123 let list = self.list;
124 let index = self.index;
125 let frame = cx.scroll_viewport();
126 if let (Some(size), Some(slot)) = (frame, self.extents.borrow_mut().get_mut(index)) {
127 *slot = size.height;
128 }
129 let state = self.drag.get();
130 let dragged = state.is_some_and(|d| d.moving && d.from == index);
131 let mut parts = Vec::with_capacity(2);
132 if list.grip {
133 parts.push(util::icon(cx, IconName::EllipsisVertical, 14.0, theme.color(Semantic::InkMuted)));
134 }
135 let item = (list.item)(index);
136 let content = cx.child(&*item);
137 let content = cx.container(
138 LayoutStyle {
139 grow: 1.0,
140 min_width: Some(0.0),
141 align: Align::Stretch,
142 ..LayoutStyle::stack(Axis::Vertical)
143 },
144 &[content],
145 Paint::None,
146 );
147 parts.push(content);
148 if let (Some(gap), Some(size)) = (state.and_then(|d| d.marked_gap()), frame) {
149 let half = list.spacing / 2.0;
150 if gap == index {
151 parts.push(reorder::marker(cx, true, size.width, -half));
152 } else if gap == index + 1 && gap == list.count {
153 parts.push(reorder::marker(cx, true, size.width, size.height + half));
154 }
155 }
156 let ink = theme.color(Semantic::Ink);
157 let fill = if dragged {
158 theme.color(Semantic::Accent).with_alpha(0.14)
159 } else if cx.is_hovered() {
160 ink.with_alpha(0.05)
161 } else {
162 Color::TRANSPARENT
163 };
164 let node = cx.container(
165 LayoutStyle {
166 spacing: theme.spacing.s2,
167 padding: Insets::symmetric(theme.spacing.s1, theme.spacing.s2),
168 align: Align::Center,
169 ..LayoutStyle::stack(Axis::Horizontal)
170 },
171 &parts,
172 Paint::Quad(QuadPaint { corner_radius: theme.radii.sm, ..QuadPaint::new(fill) }),
173 );
174 cx.report_frame(node);
175 cx.cursor(node, CursorIcon::Pointer);
176 let mut semantics =
177 Semantics::new(Role::ListItem).description(labels.sortable_hint.clone()).active(dragged);
178 if let Some(name) = &list.row_name {
179 semantics = semantics.name(name(index));
180 }
181 cx.semantics(node, semantics);
182 cx.focusable(node, theme.radii.sm);
183 cx.on_drag(
184 node,
185 reorder::drag_handler(
186 self.drag.clone(),
187 self.extents.clone(),
188 index,
189 list.spacing,
190 true,
191 self.apply.clone(),
192 ),
193 );
194 let (apply, count, called_off) = (self.apply.clone(), list.count, self.drag.clone());
195 let before: crate::KeyHandler = Rc::new(move |key, modifiers| {
196 if *key == Key::Escape && called_off.peek().is_some() {
197 called_off.set(None);
198 return true;
199 }
200 if let Some(to) = reorder::key_target(key, modifiers, index, count, true) {
201 apply(index, to);
202 }
203 reorder::is_move_key(key, modifiers)
204 });
205 self.roving.wire_with(cx, node, None, Some(before));
206 node
207 }
208}
209
210impl View for SortableList {
211 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
212 let labels = cx.labels();
213 let drag: State<Option<Reorder>> = cx.use_state(|| None);
214 let extents: Extents = cx.use_state(|| Rc::new(RefCell::new(Vec::new()))).peek().clone();
215 extents.borrow_mut().resize(self.count, 0.0);
216 let spoken: State<String> = cx.use_state(String::new);
217 let focus_to: FocusTarget = util::roving_cell(cx, None);
218 let count = self.count;
219 let apply: Rc<dyn Fn(usize, usize)> = {
220 let (handler, spoken, focus_to) = (self.on_reorder.clone(), spoken.clone(), focus_to.clone());
221 let words = labels.clone();
222 Rc::new(move |from, to| {
223 if let Some(handler) = &handler {
224 handler(from, to);
225 spoken.set(words.moved_to_position(to + 1, count));
226 focus_to.request(to);
227 }
228 })
229 };
230 let mut nodes = Vec::with_capacity(count + 1);
231 for index in 0..count {
232 let row = SortableRow {
233 list: self,
234 index,
235 extents: extents.clone(),
236 drag: drag.clone(),
237 roving: Roving { index, count, focus_to: focus_to.clone() },
238 apply: apply.clone(),
239 };
240 let key = self.key.as_ref().map_or(index as u64, |key| key(index));
241 nodes.push(cx.keyed_child(key, &row));
242 }
243 let announcement = spoken.get();
244 nodes.push(reorder::announcer(cx, announcement));
245 let node = cx.container(
246 LayoutStyle {
247 spacing: self.spacing,
248 align: Align::Stretch,
249 align_self: Some(Align::Stretch),
250 ..LayoutStyle::stack(Axis::Vertical)
251 },
252 &nodes,
253 Paint::None,
254 );
255 let name = self.label.clone().unwrap_or_else(|| labels.sortable_name.clone());
256 cx.semantics(node, Semantics::new(Role::List).name(name).description(count.to_string()));
257 node
258 }
259}