git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

src/viewer_state.rs (23.4K)

  1 //! The viewer-state framework: interactive viewport tools, generalized out of
  2 //! the curve tool.
  3 //!
  4 //! A viewer state is a mode the viewport is in, bound to one node, in which
  5 //! the pointer edits that node directly instead of orbiting the camera. The
  6 //! curve tool was the first, and everything in it except "what a handle IS"
  7 //! turned out to be the same for any such tool:
  8 //!
  9 //! - **projection** — world positions to screen through the raster scene's
 10 //!   cached mvp, the same path the meta Point Numbers overlay rides;
 11 //! - **hit-testing** — the nearest handle within a radius of the cursor;
 12 //! - **the drag model** — capture the grabbed handle's NDC depth, then
 13 //!   unproject the cursor onto that plane, so orbiting between edits never
 14 //!   makes a drag jump;
 15 //! - **per-gesture undo** — a whole drag is one step, recorded on the first
 16 //!   motion after a grab so a click that never moves records nothing;
 17 //! - **binding by node ID, not slot** — renames and graph edits do not detach
 18 //!   the tool, and a node that disappears makes every handler resolve nothing
 19 //!   so the state drops out lazily;
 20 //! - **write-back** — the same resync sequence `McpAction::SetParam` runs, so
 21 //!   the params pane, the spreadsheet and the scene all follow an edit live.
 22 //!
 23 //! What differs per tool is [`HandleSource`]: which node types it accepts,
 24 //! where the handles are, how to write them back, and whether the pointer may
 25 //! add and remove them. Two implementations ship, deliberately different in
 26 //! shape — a curve's open-ended list of control points, and a soft transform's
 27 //! fixed pair where one handle's position is DERIVED from two parameters. An
 28 //! abstraction with a single implementation has not been shown to be one.
 29 //!
 30 //! The two things the framework adds over what the curve tool had are snapping
 31 //! and a HUD, both of which every tool wants and neither of which a tool
 32 //! should implement itself.
 33 
 34 use crate::app::{FsNode, State};
 35 use cce_ui::history::History;
 36 use glam::{Mat4, Vec3, Vec4};
 37 
 38 /// How close (logical px) a press must land to a projected handle to grab it.
 39 pub const HANDLE_HIT_RADIUS: f32 = 10.0;
 40 
 41 /// What a viewer state edits.
 42 ///
 43 /// Handles are WORLD positions, always. A source whose parameters are not
 44 /// world positions — a soft transform's translation is an offset — converts in
 45 /// [`read`](Self::read) and [`write`](Self::write), so the framework never has
 46 /// to know the difference and the drag maths stays one implementation.
 47 /// What a source may need to know beyond its own node.
 48 ///
 49 /// A curve's points are world positions and need nothing. A shape on an
 50 /// image is written in the image's unit from the image's corner, so where it
 51 /// stands in the scene depends on a node further up its chain and on what a
 52 /// world unit is — neither of which `write` can look up, holding the node
 53 /// mutably as it does. The framework gathers both before it calls.
 54 #[derive(Clone, Copy, Debug, Default)]
 55 pub struct HandleCtx {
 56     /// The page the node draws on, when it draws on one.
 57     pub page: Option<crate::page::PageFrame>,
 58     /// One world unit in millimetres.
 59     pub world_unit_mm: f32,
 60 }
 61 
 62 pub trait HandleSource {
 63     /// Shown in the HUD, so it says what mode the viewport is in.
 64     fn name(&self) -> &'static str;
 65 
 66     /// Whether this source can edit a node of that type. Checked on every
 67     /// resolution, not just on entry: a project reload can put anything at an
 68     /// old id, and the tool must drop out rather than write nonsense.
 69     fn accepts(&self, node_type: &str) -> bool;
 70 
 71     /// The node's handles, in world space.
 72     fn read(&self, node: &FsNode, ctx: &HandleCtx) -> Vec<Vec3>;
 73 
 74     /// Write handles back into the node's parameters. The framework runs the
 75     /// resync afterwards.
 76     fn write(&self, node: &mut FsNode, handles: &[Vec3], ctx: &HandleCtx);
 77 
 78     /// Handle `moved` was dragged to `to`: what the whole set is now.
 79     ///
 80     /// Moving the one handle is the default, and is all a source of
 81     /// independent handles wants. A source whose handles hang off one
 82     /// another carries them here — a shape's corner goes with its centre —
 83     /// so that `write` is still handed a whole set that means one thing,
 84     /// whether it came from a drag or from an undo snapshot.
 85     fn drag(&self, handles: &mut Vec<Vec3>, moved: usize, to: Vec3, _ctx: &HandleCtx) {
 86         handles[moved] = to;
 87     }
 88 
 89     /// The plane the handles live in, as a point on it and its normal, for
 90     /// a source that has one. A drag then follows the cursor's ray to that
 91     /// plane, where without one it follows it to the camera-facing plane at
 92     /// the depth the handle was grabbed — which leaves a flat thing's plane
 93     /// as soon as the view is not square to it.
 94     fn plane(&self, _ctx: &HandleCtx) -> Option<(Vec3, Vec3)> {
 95         None
 96     }
 97 
 98     /// A closed outline to draw with the handles, in world space: what is
 99     /// being edited, where the handles alone do not show it.
100     fn outline(&self, _node: &FsNode, _ctx: &HandleCtx) -> Vec<Vec3> {
101         Vec::new()
102     }
103 
104     /// Whether the handles are joined in order by a faint line — a curve's
105     /// control cage. False for handles that are not a sequence.
106     fn cage(&self) -> bool {
107         true
108     }
109 
110     /// Whether a press on empty space appends a handle and a right press
111     /// deletes one. False for a source with a fixed set — a soft transform has
112     /// exactly a centre and a translation, and a third handle would mean
113     /// nothing.
114     fn extensible(&self) -> bool;
115 
116     /// The key hints for the HUD, without the ones the framework owns
117     /// (snapping, Escape) — those are appended.
118     fn hints(&self) -> &'static str;
119 
120     /// What to write beside handle `i`. Indices by default, which is right
121     /// for an ordered list; a source whose handles mean different things names
122     /// them instead, because "1" and "2" on a centre and a tip is a worse
123     /// label than none.
124     fn handle_label(&self, i: usize) -> String {
125         (i + 1).to_string()
126     }
127 }
128 
129 /// An in-flight drag.
130 #[derive(Clone, Copy)]
131 pub struct Drag {
132     pub handle: usize,
133     /// NDC depth captured at grab time; motion unprojects onto this plane.
134     pub ndc_z: f32,
135 }
136 
137 /// The active viewer state.
138 pub struct ViewerTool {
139     /// The edited node's id — not its slot, so renames and graph edits do not
140     /// detach the tool.
141     pub node_id: String,
142     pub source: Box<dyn HandleSource>,
143     /// The last-clicked handle — the Delete target.
144     pub selected: Option<usize>,
145     pub drag: Option<Drag>,
146     /// Handle snapshots, one per gesture.
147     pub history: History<Vec<Vec3>>,
148     /// World-space increment a dragged handle rounds to, or `None` for free
149     /// movement. Lives on the tool rather than in settings because it is a
150     /// property of the editing session, and it survives retargeting so turning
151     /// it on does not have to be repeated per node.
152     pub snap: Option<f32>,
153 }
154 
155 /// The increment snapping rounds to when it is switched on.
156 ///
157 /// A tenth of a world unit: fine enough to place a point deliberately, coarse
158 /// enough that two snapped points actually coincide. The world unit is a
159 /// DECLARATION here (see the Guides node), so this is a tenth of whatever the
160 /// project says a unit is rather than a tenth of a millimetre.
161 pub const SNAP_INCREMENT: f32 = 0.1;
162 
163 impl ViewerTool {
164     pub fn new(node_id: String, source: Box<dyn HandleSource>) -> Self {
165         ViewerTool { node_id, source, selected: None, drag: None, history: History::new(), snap: None }
166     }
167 
168     /// The HUD line: what mode this is, what the keys do, and whether snapping
169     /// is on. The snap state is on the HUD because it silently changes what a
170     /// drag does, and a mode you cannot see is a mode you forget you are in.
171     pub fn hud(&self) -> String {
172         format!(
173             "{} — {}  ·  Snap {}  ·  Esc exits",
174             self.source.name(),
175             self.source.hints(),
176             if self.snap.is_some() { "on" } else { "off" },
177         )
178     }
179 }
180 
181 /// Round a world position to `increment` on every axis.
182 fn snapped(p: Vec3, increment: Option<f32>) -> Vec3 {
183     match increment {
184         Some(i) if i > 0.0 => Vec3::new(
185             (p.x / i).round() * i,
186             (p.y / i).round() * i,
187             (p.z / i).round() * i,
188         ),
189         _ => p,
190     }
191 }
192 
193 /// World → (screen x, screen y, ndc z) through the cached scene mvp.
194 pub fn project_point(mvp: &Mat4, view: (f32, f32, f32, f32), p: Vec3) -> Option<(f32, f32, f32)> {
195     let (vx, vy, vw, vh) = view;
196     let clip = *mvp * Vec4::new(p.x, p.y, p.z, 1.0);
197     if clip.w <= 0.0 {
198         return None;
199     }
200     let ndc = clip / clip.w;
201     Some((vx + (ndc.x * 0.5 + 0.5) * vw, vy + (0.5 - ndc.y * 0.5) * vh, ndc.z))
202 }
203 
204 /// (screen x, screen y, ndc z) → world through the inverse of the cached mvp.
205 pub fn unproject_point(
206     mvp: &Mat4,
207     view: (f32, f32, f32, f32),
208     sx: f32,
209     sy: f32,
210     ndc_z: f32,
211 ) -> Option<Vec3> {
212     let (vx, vy, vw, vh) = view;
213     if vw <= 0.0 || vh <= 0.0 {
214         return None;
215     }
216     let inv = mvp.inverse();
217     if !inv.is_finite() {
218         return None;
219     }
220     let ndc_x = ((sx - vx) / vw - 0.5) * 2.0;
221     let ndc_y = (0.5 - (sy - vy) / vh) * 2.0;
222     let world = inv * Vec4::new(ndc_x, ndc_y, ndc_z, 1.0);
223     if world.w.abs() < 1e-6 {
224         return None;
225     }
226     let world = world / world.w;
227     if !world.is_finite() {
228         return None;
229     }
230     Some(Vec3::new(world.x, world.y, world.z))
231 }
232 
233 pub fn find_node_by_id<'a>(root: &'a FsNode, id: &str) -> Option<&'a FsNode> {
234     if root.id == id {
235         return Some(root);
236     }
237     root.children.iter().find_map(|c| find_node_by_id(c, id))
238 }
239 
240 pub fn find_node_by_id_mut<'a>(root: &'a mut FsNode, id: &str) -> Option<&'a mut FsNode> {
241     if root.id == id {
242         return Some(root);
243     }
244     root.children.iter_mut().find_map(|c| find_node_by_id_mut(c, id))
245 }
246 
247 /// The viewer state a node type can enter, if any.
248 ///
249 /// One place that maps node types to tools, so the node context menu, the
250 /// command and any future entry point agree about what is editable.
251 pub fn source_for(node_type: &str) -> Option<Box<dyn HandleSource>> {
252     let sources: [Box<dyn HandleSource>; 4] = [
253         Box::new(crate::curve_tool::CurveHandles),
254         Box::new(crate::soft_transform_tool::SoftTransformHandles),
255         Box::new(crate::image_handles::ShapeHandles),
256         Box::new(crate::image_handles::TextHandles),
257     ];
258     sources.into_iter().find(|s| s.accepts(node_type))
259 }
260 
261 impl State {
262     /// Enter/exit the viewer state for the node in `slot` of the current
263     /// directory. A different editable node retargets the tool.
264     pub(crate) fn toggle_viewer_state(&mut self, slot: usize) {
265         let Some(node) = self.current_dir().children.get(slot) else { return };
266         let Some(source) = source_for(&node.node_type) else { return };
267         let id = node.id.clone();
268         if self.viewer_tool.as_ref().map(|t| t.node_id == id).unwrap_or(false) {
269             self.viewer_tool = None;
270         } else {
271             self.viewer_tool = Some(ViewerTool::new(id, source));
272         }
273     }
274 
275     /// Turn snapping on or off for the active state. Returns false when no
276     /// state is active, so the command falls through to mean nothing rather
277     /// than reporting success.
278     pub(crate) fn toggle_viewer_snap(&mut self) -> bool {
279         let Some(tool) = self.viewer_tool.as_mut() else { return false };
280         tool.snap = match tool.snap {
281             Some(_) => None,
282             None => Some(SNAP_INCREMENT),
283         };
284         let on = tool.snap.is_some();
285         self.update_status_text(if on { "Snapping on" } else { "Snapping off" });
286         true
287     }
288 
289     /// What the source of the node's handles may need to know: gathered
290     /// here, once, ahead of every read and write.
291     fn viewer_handle_ctx(&self, node: &FsNode) -> HandleCtx {
292         let page = crate::page::is_page_node(&node.node_type)
293             .then(|| crate::page::resolve_frame(&self.fs_root, node))
294             .flatten();
295         HandleCtx { page, world_unit_mm: self.world_unit_mm() }
296     }
297 
298     /// The edited node's handles, or None if the node is gone or is no longer
299     /// a type this source accepts.
300     fn viewer_handles_of(&self, node_id: &str) -> Option<Vec<Vec3>> {
301         let tool = self.viewer_tool.as_ref()?;
302         let node = find_node_by_id(&self.fs_root, node_id)?;
303         if !tool.source.accepts(&node.node_type) {
304             return None;
305         }
306         Some(tool.source.read(node, &self.viewer_handle_ctx(node)))
307     }
308 
309     /// The active tool's outline, projected: screen points of a closed
310     /// loop, empty for a source that draws none.
311     pub(crate) fn viewer_tool_outline(&self) -> Vec<(f32, f32)> {
312         let Some(tool) = &self.viewer_tool else { return Vec::new() };
313         let Some(mvp) = self.last_scene_mvp else { return Vec::new() };
314         let Some(node) = find_node_by_id(&self.fs_root, &tool.node_id) else { return Vec::new() };
315         if !tool.source.accepts(&node.node_type) {
316             return Vec::new();
317         }
318         let loop_ = tool.source.outline(node, &self.viewer_handle_ctx(node));
319         let projected: Vec<(f32, f32)> = loop_
320             .iter()
321             .filter_map(|p| project_point(&mvp, self.last_scene_view_rect, *p))
322             .map(|(x, y, _)| (x, y))
323             .collect();
324         // A corner behind the camera has no place on screen, and a loop
325         // missing one is another shape.
326         if projected.len() == loop_.len() { projected } else { Vec::new() }
327     }
328 
329     /// Where a drag puts the grabbed handle: on the source's plane under
330     /// the cursor when it has one, else on the camera-facing plane at the
331     /// depth the handle was grabbed.
332     fn viewer_drag_target(&self, node_id: &str, ndc_z: f32) -> Option<Vec3> {
333         let mvp = self.last_scene_mvp?;
334         let view = self.last_scene_view_rect;
335         let (cx, cy) = (self.cursor_x, self.cursor_y);
336         let plane = self.viewer_tool.as_ref().and_then(|tool| {
337             let node = find_node_by_id(&self.fs_root, node_id)?;
338             tool.source.plane(&self.viewer_handle_ctx(node))
339         });
340         if let Some((point, normal)) = plane {
341             let near = unproject_point(&mvp, view, cx, cy, 0.0)?;
342             let far = unproject_point(&mvp, view, cx, cy, 1.0)?;
343             let ray = far - near;
344             let along = ray.dot(normal);
345             // A plane seen edge-on is met nowhere the cursor can name.
346             if along.abs() > 1e-6 * ray.length().max(1e-6) {
347                 let t = (point - near).dot(normal) / along;
348                 return Some(near + ray * t);
349             }
350         }
351         unproject_point(&mvp, view, cx, cy, ndc_z)
352     }
353 
354     /// Write handles back and run the same resync sequence as SetParam.
355     fn set_viewer_handles(&mut self, node_id: &str, handles: &[Vec3]) {
356         let ctx = match find_node_by_id(&self.fs_root, node_id) {
357             Some(node) => self.viewer_handle_ctx(node),
358             None => return,
359         };
360         let Some(tool) = self.viewer_tool.take() else { return };
361         if let Some(node) = find_node_by_id_mut(&mut self.fs_root, node_id) {
362             tool.source.write(node, handles, &ctx);
363         }
364         self.viewer_tool = Some(tool);
365         self.sync_nodes();
366         self.rebuild_scene_geometry();
367         self.sync_parameters_pane();
368     }
369 
370     /// The active tool's handles as (index, screen x, screen y, ndc z).
371     /// Empty when no tool is active, the node is gone, or no scene mvp has
372     /// been cached yet (a frame before the first scene staging).
373     pub(crate) fn viewer_tool_handles(&self) -> Vec<(usize, f32, f32, f32)> {
374         let Some(tool) = &self.viewer_tool else { return Vec::new() };
375         let Some(mvp) = self.last_scene_mvp else { return Vec::new() };
376         let Some(pts) = self.viewer_handles_of(&tool.node_id) else { return Vec::new() };
377         pts.iter()
378             .enumerate()
379             .filter_map(|(i, p)| {
380                 project_point(&mvp, self.last_scene_view_rect, *p).map(|(sx, sy, z)| (i, sx, sy, z))
381             })
382             .collect()
383     }
384 
385     /// The handle under the cursor, nearest first.
386     fn viewer_handle_at_cursor(&self) -> Option<(usize, f32)> {
387         let (cx, cy) = (self.cursor_x, self.cursor_y);
388         self.viewer_tool_handles()
389             .iter()
390             .map(|(i, sx, sy, z)| (*i, ((sx - cx).powi(2) + (sy - cy).powi(2)).sqrt(), *z))
391             .filter(|(_, d, _)| *d <= HANDLE_HIT_RADIUS)
392             .min_by(|a, b| a.1.total_cmp(&b.1))
393             .map(|(i, _, z)| (i, z))
394     }
395 
396     /// Left press in the viewport while a state is active: grab the handle
397     /// under the cursor, or — for an extensible source — append a new handle
398     /// there and start dragging it. Returns false, letting the press fall
399     /// through, only when the edited node no longer exists.
400     pub(crate) fn viewer_tool_press(&mut self) -> bool {
401         let Some(tool) = &self.viewer_tool else { return false };
402         let node_id = tool.node_id.clone();
403         let Some(mut pts) = self.viewer_handles_of(&node_id) else {
404             self.viewer_tool = None;
405             return false;
406         };
407         if let Some((idx, ndc_z)) = self.viewer_handle_at_cursor() {
408             let tool = self.viewer_tool.as_mut().expect("checked above");
409             tool.selected = Some(idx);
410             tool.history.begin_gesture(pts);
411             tool.drag = Some(Drag { handle: idx, ndc_z });
412             return true;
413         }
414         if !self.viewer_tool.as_ref().is_some_and(|t| t.source.extensible()) {
415             // A fixed source consumes the press anyway: the state owns the
416             // viewport while it is active, and falling through would open the
417             // context menu on every miss.
418             return true;
419         }
420         // Empty space: add a handle. Depth comes from the last one (or the
421         // world origin) so the new handle lands in the plane already in use.
422         let Some(mvp) = self.last_scene_mvp else { return true };
423         let view = self.last_scene_view_rect;
424         let ndc_z = pts
425             .last()
426             .and_then(|p| project_point(&mvp, view, *p))
427             .map(|(_, _, z)| z)
428             .or_else(|| project_point(&mvp, view, Vec3::ZERO).map(|(_, _, z)| z));
429         let Some(ndc_z) = ndc_z else { return true };
430         let Some(world) = unproject_point(&mvp, view, self.cursor_x, self.cursor_y, ndc_z) else {
431             return true;
432         };
433         let snap = self.viewer_tool.as_ref().and_then(|t| t.snap);
434         let before = pts.clone();
435         pts.push(snapped(world, snap));
436         let idx = pts.len() - 1;
437         self.set_viewer_handles(&node_id, &pts);
438         if let Some(tool) = self.viewer_tool.as_mut() {
439             // The add is the recorded step; the drag that follows is part of
440             // the same gesture, so no gesture is opened for it.
441             tool.history.record(before);
442             tool.selected = Some(idx);
443             tool.drag = Some(Drag { handle: idx, ndc_z });
444         }
445         true
446     }
447 
448     /// Pointer motion during a grab: the handle tracks the cursor on the
449     /// camera-facing plane at its grab depth.
450     pub(crate) fn viewer_tool_drag_motion(&mut self) -> bool {
451         let Some(drag) = self.viewer_tool.as_ref().and_then(|t| t.drag) else { return false };
452         if self.last_scene_mvp.is_none() {
453             return false;
454         }
455         let node_id = self.viewer_tool.as_ref().expect("drag implies tool").node_id.clone();
456         let Some(mut pts) = self.viewer_handles_of(&node_id) else {
457             self.viewer_tool = None;
458             return false;
459         };
460         if drag.handle >= pts.len() {
461             return false;
462         }
463         let Some(world) = self.viewer_drag_target(&node_id, drag.ndc_z) else {
464             return false;
465         };
466         let snap = self.viewer_tool.as_ref().and_then(|t| t.snap);
467         let ctx = match find_node_by_id(&self.fs_root, &node_id) {
468             Some(node) => self.viewer_handle_ctx(node),
469             None => return false,
470         };
471         if let Some(tool) = self.viewer_tool.as_ref() {
472             tool.source.drag(&mut pts, drag.handle, snapped(world, snap), &ctx);
473         }
474         if let Some(tool) = self.viewer_tool.as_mut() {
475             tool.history.commit_gesture();
476         }
477         self.set_viewer_handles(&node_id, &pts);
478         true
479     }
480 
481     /// Button release: end any in-flight grab.
482     pub(crate) fn viewer_tool_release(&mut self) -> bool {
483         match self.viewer_tool.as_mut() {
484             Some(tool) if tool.drag.is_some() => {
485                 tool.drag = None;
486                 tool.history.cancel_gesture();
487                 true
488             }
489             _ => false,
490         }
491     }
492 
493     /// Right press: delete the handle under the cursor. Consumes only on a hit
494     /// on an extensible source — otherwise the press falls through to the
495     /// viewport context menu.
496     pub(crate) fn viewer_tool_delete_at_cursor(&mut self) -> bool {
497         if !self.viewer_tool.as_ref().is_some_and(|t| t.source.extensible()) {
498             return false;
499         }
500         let Some((idx, _)) = self.viewer_handle_at_cursor() else { return false };
501         self.viewer_tool_delete_handle(idx)
502     }
503 
504     /// Delete/Backspace: remove the selected handle, if any.
505     pub(crate) fn viewer_tool_delete_selected(&mut self) -> bool {
506         if !self.viewer_tool.as_ref().is_some_and(|t| t.source.extensible()) {
507             return false;
508         }
509         let Some(idx) = self.viewer_tool.as_ref().and_then(|t| t.selected) else { return false };
510         self.viewer_tool_delete_handle(idx)
511     }
512 
513     fn viewer_tool_delete_handle(&mut self, idx: usize) -> bool {
514         let Some(tool) = &self.viewer_tool else { return false };
515         let node_id = tool.node_id.clone();
516         let Some(mut pts) = self.viewer_handles_of(&node_id) else {
517             self.viewer_tool = None;
518             return false;
519         };
520         if idx >= pts.len() {
521             return false;
522         }
523         let before = pts.clone();
524         pts.remove(idx);
525         self.set_viewer_handles(&node_id, &pts);
526         if let Some(tool) = self.viewer_tool.as_mut() {
527             tool.history.record(before);
528             tool.drag = None;
529             // Keep a neighbour selected so repeated Delete walks the handles.
530             tool.selected = if pts.is_empty() { None } else { Some(idx.min(pts.len() - 1)) };
531         }
532         true
533     }
534 
535     /// Undo: return the handles to how they were before the last recorded
536     /// gesture. Consumes only when a state is active and has history.
537     pub(crate) fn viewer_tool_undo(&mut self) -> bool {
538         self.viewer_tool_step(true)
539     }
540 
541     /// Redo: reapply the last undone gesture.
542     pub(crate) fn viewer_tool_redo(&mut self) -> bool {
543         self.viewer_tool_step(false)
544     }
545 
546     fn viewer_tool_step(&mut self, undo: bool) -> bool {
547         let Some(tool) = &self.viewer_tool else { return false };
548         let node_id = tool.node_id.clone();
549         let Some(current) = self.viewer_handles_of(&node_id) else {
550             self.viewer_tool = None;
551             return false;
552         };
553         let tool = self.viewer_tool.as_mut().expect("checked above");
554         let stepped = if undo { tool.history.undo(current) } else { tool.history.redo(current) };
555         let Some(target) = stepped else { return false };
556         // A step mid-drag abandons the drag: the grabbed index may not exist
557         // in the restored list, and the pointer no longer means anything to it.
558         tool.drag = None;
559         tool.selected = match tool.selected {
560             Some(i) if !target.is_empty() => Some(i.min(target.len() - 1)),
561             _ => None,
562         };
563         self.set_viewer_handles(&node_id, &target);
564         true
565     }
566 }