Skip to main content

cosmic/widget/reorderable_flex_row/
widget.rs

1// Copyright 2026 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use crate::{Element, Renderer};
5use iced::{Alignment, Pixels, alignment};
6use iced_core::event::Event;
7use iced_core::layout::{self, Layout};
8use iced_core::mouse::{self, Cursor};
9use iced_core::widget::Operation;
10use iced_core::widget::tree::{self, Tree};
11#[cfg(feature = "wgpu")]
12use iced_core::{Background, Border, Shadow};
13use iced_core::{
14    Clipboard, Length, Padding, Point, Rectangle, Renderer as _, Shell, Size, Vector, Widget,
15    overlay, renderer, window,
16};
17use std::collections::{HashMap, HashSet};
18use std::hash::Hash;
19use std::time::{Duration, Instant};
20
21const DEFAULT_ANIMATION_DURATION: Duration = Duration::from_millis(180);
22const DEFAULT_DRAG_LIFT: f32 = 10.0;
23const DEFAULT_DRAG_THRESHOLD: f32 = 6.0;
24const SHADOW_BLUR_RADIUS: f32 = 20.0;
25const POSITION_EPSILON: f32 = 0.5;
26
27#[derive(Debug, Clone)]
28struct SlotAnimation {
29    from: Point,
30    to: Point,
31    started_at: Option<Instant>,
32}
33
34impl SlotAnimation {
35    fn new(position: Point) -> Self {
36        Self {
37            from: position,
38            to: position,
39            started_at: None,
40        }
41    }
42
43    fn current_position(&self, duration: Duration) -> Point {
44        let Some(started_at) = self.started_at else {
45            return self.to;
46        };
47
48        let duration_secs = duration.as_secs_f32();
49        if duration_secs <= f32::EPSILON {
50            return self.to;
51        }
52
53        let progress = (started_at.elapsed().as_secs_f32() / duration_secs).clamp(0.0, 1.0);
54        let eased = 1.0 - (1.0 - progress).powi(3);
55
56        Point::new(
57            self.from.x + (self.to.x - self.from.x) * eased,
58            self.from.y + (self.to.y - self.from.y) * eased,
59        )
60    }
61
62    fn retarget(&mut self, new_target: Point, duration: Duration) {
63        if approx_eq_point(self.to, new_target) {
64            if !self.is_animating(duration) {
65                self.from = new_target;
66                self.to = new_target;
67                self.started_at = None;
68            }
69            return;
70        }
71
72        self.from = self.current_position(duration);
73        self.to = new_target;
74        self.started_at = Some(Instant::now());
75    }
76
77    fn is_animating(&self, duration: Duration) -> bool {
78        self.started_at
79            .is_some_and(|started_at| started_at.elapsed() < duration)
80    }
81
82    fn finish_if_done(&mut self, duration: Duration) {
83        if self
84            .started_at
85            .is_some_and(|started_at| started_at.elapsed() >= duration)
86        {
87            self.from = self.to;
88            self.started_at = None;
89        }
90    }
91}
92
93#[derive(Debug, Clone)]
94struct PendingDragState<Key>
95where
96    Key: Clone + Eq + Hash + 'static,
97{
98    key: Key,
99    item_index: usize,
100    original_index: usize,
101    press_local: Point,
102    pointer_offset: Vector,
103}
104
105#[derive(Debug, Clone)]
106struct DragState<Key>
107where
108    Key: Clone + Eq + Hash + 'static,
109{
110    key: Key,
111    item_index: usize,
112    original_index: usize,
113    current_index: usize,
114    cursor_local: Point,
115    pointer_offset: Vector,
116}
117
118#[derive(Debug, Clone)]
119struct State<Key>
120where
121    Key: Clone + Eq + Hash + 'static,
122{
123    keys: Vec<Key>,
124    slot_positions: HashMap<Key, SlotAnimation>,
125    pending_drag: Option<PendingDragState<Key>>,
126    drag: Option<DragState<Key>>,
127    wrap_width: f32,
128    initialized: bool,
129}
130
131impl<Key> Default for State<Key>
132where
133    Key: Clone + Eq + Hash + 'static,
134{
135    fn default() -> Self {
136        Self {
137            keys: Vec::new(),
138            slot_positions: HashMap::new(),
139            pending_drag: None,
140            drag: None,
141            wrap_width: f32::INFINITY,
142            initialized: false,
143        }
144    }
145}
146
147impl<Key> State<Key>
148where
149    Key: Clone + Eq + Hash + 'static,
150{
151    fn retain_keys(&mut self, keys: &[Key]) {
152        let keep: HashSet<_> = keys.iter().cloned().collect();
153        self.slot_positions.retain(|key, _| keep.contains(key));
154
155        if self
156            .pending_drag
157            .as_ref()
158            .is_some_and(|drag| !keep.contains(&drag.key))
159        {
160            self.pending_drag = None;
161        }
162
163        if self
164            .drag
165            .as_ref()
166            .is_some_and(|drag| !keep.contains(&drag.key))
167        {
168            self.drag = None;
169        }
170    }
171
172    fn finish_animations(&mut self, duration: Duration) {
173        self.slot_positions
174            .values_mut()
175            .for_each(|slot| slot.finish_if_done(duration));
176    }
177
178    fn is_animating(&self, duration: Duration) -> bool {
179        self.slot_positions
180            .values()
181            .any(|slot| slot.is_animating(duration))
182    }
183
184    fn current_slot_position(&self, key: &Key, fallback: Point, duration: Duration) -> Point {
185        self.slot_positions
186            .get(key)
187            .map(|slot| slot.current_position(duration))
188            .unwrap_or(fallback)
189    }
190
191    fn retarget_slot(&mut self, key: &Key, target: Point, duration: Duration) {
192        self.slot_positions
193            .entry(key.clone())
194            .or_insert_with(|| SlotAnimation::new(target))
195            .retarget(target, duration);
196    }
197
198    fn snap_slot(&mut self, key: &Key, target: Point) {
199        self.slot_positions
200            .insert(key.clone(), SlotAnimation::new(target));
201    }
202
203    fn apply_layout_position(&mut self, key: &Key, target: Point, duration: Duration) {
204        if self.initialized {
205            self.retarget_slot(key, target, duration);
206        } else {
207            self.snap_slot(key, target);
208        }
209    }
210}
211
212#[derive(Debug, Clone)]
213struct LocalSlot<Key>
214where
215    Key: Clone + Eq + Hash + 'static,
216{
217    key: Key,
218    locked: bool,
219    bounds: Rectangle,
220}
221
222/// A horizontal flex row with drag-to-reorder behavior.
223#[must_use]
224pub struct ReorderableFlexRow<'a, Key, Message>
225where
226    Key: Clone + Eq + Hash + 'static,
227{
228    spacing: f32,
229    padding: Padding,
230    width: Length,
231    height: Length,
232    align: Alignment,
233    clip: bool,
234    drag_lift: f32,
235    animation_duration: Duration,
236    on_reorder: Box<dyn Fn(Vec<Key>) -> Message + 'a>,
237    keys: Vec<Key>,
238    locked: Vec<bool>,
239    children: Vec<Element<'a, Message>>,
240}
241
242impl<'a, Key, Message> ReorderableFlexRow<'a, Key, Message>
243where
244    Key: Clone + Eq + Hash + 'static,
245{
246    pub fn new(on_reorder: impl Fn(Vec<Key>) -> Message + 'a) -> Self {
247        Self {
248            spacing: 0.0,
249            padding: Padding::ZERO,
250            width: Length::Shrink,
251            height: Length::Shrink,
252            align: Alignment::Start,
253            clip: false,
254            drag_lift: DEFAULT_DRAG_LIFT,
255            animation_duration: DEFAULT_ANIMATION_DURATION,
256            on_reorder: Box::new(on_reorder),
257            keys: Vec::new(),
258            locked: Vec::new(),
259            children: Vec::new(),
260        }
261    }
262
263    pub fn with_capacity(capacity: usize, on_reorder: impl Fn(Vec<Key>) -> Message + 'a) -> Self {
264        let mut row = Self::new(on_reorder);
265        row.keys = Vec::with_capacity(capacity);
266        row.locked = Vec::with_capacity(capacity);
267        row.children = Vec::with_capacity(capacity);
268        row
269    }
270
271    pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
272        self.spacing = amount.into().0;
273        self
274    }
275
276    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
277        self.padding = padding.into();
278        self
279    }
280
281    pub fn width(mut self, width: impl Into<Length>) -> Self {
282        self.width = width.into();
283        self
284    }
285
286    pub fn height(mut self, height: impl Into<Length>) -> Self {
287        self.height = height.into();
288        self
289    }
290
291    pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self {
292        self.align = Alignment::from(align.into());
293        self
294    }
295
296    /// Leave disabled for dragged item to visibly lift above the row.
297    pub fn clip(mut self, clip: bool) -> Self {
298        self.clip = clip;
299        self
300    }
301
302    pub fn drag_lift(mut self, lift: f32) -> Self {
303        self.drag_lift = lift.max(0.0);
304        self
305    }
306
307    pub fn animation_duration(mut self, duration: Duration) -> Self {
308        self.animation_duration = duration;
309        self
310    }
311
312    pub fn push(self, key: Key, child: impl Into<Element<'a, Message>>) -> Self {
313        self.push_with_lock(key, false, child)
314    }
315
316    /// Item stays fixed, never participates in reordering.
317    pub fn push_locked(self, key: Key, child: impl Into<Element<'a, Message>>) -> Self {
318        self.push_with_lock(key, true, child)
319    }
320
321    fn push_with_lock(
322        mut self,
323        key: Key,
324        locked: bool,
325        child: impl Into<Element<'a, Message>>,
326    ) -> Self {
327        let child = child.into();
328        let child_size = child.as_widget().size_hint();
329
330        if !child_size.is_void() {
331            self.width = self.width.enclose(child_size.width);
332            self.height = self.height.enclose(child_size.height);
333            self.keys.push(key);
334            self.locked.push(locked);
335            self.children.push(child);
336        }
337
338        self
339    }
340
341    pub fn extend(self, items: impl IntoIterator<Item = (Key, Element<'a, Message>)>) -> Self {
342        items
343            .into_iter()
344            .fold(self, |row, (key, child)| row.push(key, child))
345    }
346
347    pub fn extend_locked(
348        self,
349        items: impl IntoIterator<Item = (Key, Element<'a, Message>)>,
350    ) -> Self {
351        items
352            .into_iter()
353            .fold(self, |row, (key, child)| row.push_locked(key, child))
354    }
355
356    fn item_local_slots_from_layout(
357        &self,
358        bounds: Rectangle,
359        child_layouts: &[Layout<'_>],
360    ) -> Vec<LocalSlot<Key>> {
361        self.keys
362            .iter()
363            .zip(self.locked.iter())
364            .zip(child_layouts.iter())
365            .map(|((key, locked), child_layout)| {
366                let child_bounds = child_layout.bounds();
367                LocalSlot {
368                    key: key.clone(),
369                    locked: *locked,
370                    bounds: Rectangle {
371                        x: child_bounds.x - bounds.x,
372                        y: child_bounds.y - bounds.y,
373                        width: child_bounds.width,
374                        height: child_bounds.height,
375                    },
376                }
377            })
378            .collect()
379    }
380
381    fn sync_slot_positions(&self, state: &mut State<Key>, slots: &[LocalSlot<Key>]) {
382        let ordered_keys: Vec<Key> = slots.iter().map(|slot| slot.key.clone()).collect();
383        state.retain_keys(&ordered_keys);
384
385        let size_by_key: HashMap<Key, Size> = slots
386            .iter()
387            .map(|slot| {
388                (
389                    slot.key.clone(),
390                    Size::new(slot.bounds.width, slot.bounds.height),
391                )
392            })
393            .collect();
394        let locked_by_key: HashMap<Key, bool> = slots
395            .iter()
396            .map(|slot| (slot.key.clone(), slot.locked))
397            .collect();
398
399        let Some(drag_snapshot) = state.drag.as_ref().map(|drag| {
400            (
401                drag.key.clone(),
402                drag.cursor_local,
403                drag.pointer_offset,
404                drag.item_index,
405            )
406        }) else {
407            for slot in slots {
408                state.apply_layout_position(
409                    &slot.key,
410                    Point::new(slot.bounds.x, slot.bounds.y),
411                    self.animation_duration,
412                );
413            }
414            return;
415        };
416
417        let (drag_key, cursor_local, pointer_offset, drag_item_index) = drag_snapshot;
418
419        let Some(dragged_slot) = slots.iter().find(|slot| slot.key == drag_key) else {
420            state.drag = None;
421            for slot in slots {
422                state.apply_layout_position(
423                    &slot.key,
424                    Point::new(slot.bounds.x, slot.bounds.y),
425                    self.animation_duration,
426                );
427            }
428            return;
429        };
430
431        if dragged_slot.locked {
432            state.drag = None;
433            return;
434        }
435
436        let dragged_center = Point::new(
437            cursor_local.x - pointer_offset.x + dragged_slot.bounds.width / 2.0,
438            cursor_local.y - pointer_offset.y + dragged_slot.bounds.height / 2.0,
439        );
440        let target_index = target_index_for_drag(slots, &drag_key, dragged_center);
441        let prior_index = state.drag.as_ref().map(|drag| drag.current_index);
442
443        if let Some(drag) = state.drag.as_mut() {
444            drag.current_index = target_index;
445            drag.item_index = drag_item_index;
446        }
447
448        if prior_index == Some(target_index) {
449            return;
450        }
451
452        let reordered_keys =
453            reordered_keys_for_drag(&ordered_keys, &locked_by_key, &drag_key, target_index);
454        let (target_slots, _) = compute_wrapped_slots(
455            &reordered_keys,
456            &locked_by_key,
457            &size_by_key,
458            state.wrap_width,
459            self.padding,
460            self.spacing,
461            self.align,
462        );
463        let target_positions: HashMap<Key, Point> = target_slots
464            .iter()
465            .map(|slot| (slot.key.clone(), Point::new(slot.bounds.x, slot.bounds.y)))
466            .collect();
467
468        for slot in slots {
469            let target = target_positions
470                .get(&slot.key)
471                .copied()
472                .unwrap_or(Point::new(slot.bounds.x, slot.bounds.y));
473            state.retarget_slot(&slot.key, target, self.animation_duration);
474        }
475    }
476}
477
478impl<'a, Key, Message> Widget<Message, crate::Theme, Renderer>
479    for ReorderableFlexRow<'a, Key, Message>
480where
481    Key: Clone + Eq + Hash + 'static,
482    Message: 'a,
483{
484    fn tag(&self) -> tree::Tag {
485        tree::Tag::of::<State<Key>>()
486    }
487
488    fn state(&self) -> tree::State {
489        tree::State::new(State {
490            keys: self.keys.clone(),
491            ..State::default()
492        })
493    }
494
495    fn children(&self) -> Vec<Tree> {
496        self.children.iter().map(Tree::new).collect()
497    }
498
499    fn diff(&mut self, tree: &mut Tree) {
500        let Tree {
501            state, children, ..
502        } = tree;
503        let state = state.downcast_mut::<State<Key>>();
504        let previous_keys = state.keys.clone();
505        let previous_children = std::mem::take(children);
506        let mut previous_by_key = HashMap::with_capacity(previous_keys.len());
507
508        for (key, child_tree) in previous_keys.into_iter().zip(previous_children) {
509            previous_by_key.insert(key, child_tree);
510        }
511
512        children.reserve(self.children.len());
513
514        for (key, child) in self.keys.iter().cloned().zip(self.children.iter_mut()) {
515            let mut child_tree = previous_by_key
516                .remove(&key)
517                .unwrap_or_else(|| Tree::new(child.as_widget()));
518            child.as_widget_mut().diff(&mut child_tree);
519            children.push(child_tree);
520        }
521
522        if state.keys != self.keys {
523            state.keys.clone_from(&self.keys);
524        }
525        state.retain_keys(&self.keys);
526    }
527
528    fn size(&self) -> Size<Length> {
529        Size {
530            width: self.width,
531            height: self.height,
532        }
533    }
534
535    fn layout(
536        &mut self,
537        tree: &mut Tree,
538        renderer: &Renderer,
539        limits: &layout::Limits,
540    ) -> layout::Node {
541        let limits = limits
542            .width(self.width)
543            .height(self.height)
544            .shrink(self.padding);
545        let child_limits = limits.loose();
546        let wrap_width = limits.max().width;
547
548        let mut nodes = Vec::with_capacity(self.children.len());
549        let mut size_by_key = HashMap::with_capacity(self.children.len());
550        let locked_by_key: HashMap<Key, bool> = self
551            .keys
552            .iter()
553            .cloned()
554            .zip(self.locked.iter().copied())
555            .collect();
556
557        for ((key, child), child_tree) in self
558            .keys
559            .iter()
560            .zip(self.children.iter_mut())
561            .zip(tree.children.iter_mut())
562        {
563            let node = child
564                .as_widget_mut()
565                .layout(child_tree, renderer, &child_limits);
566            size_by_key.insert(key.clone(), node.size());
567            nodes.push(node);
568        }
569
570        let (slots, intrinsic_size) = compute_wrapped_slots(
571            &self.keys,
572            &locked_by_key,
573            &size_by_key,
574            wrap_width,
575            self.padding,
576            self.spacing,
577            self.align,
578        );
579
580        for (node, slot) in nodes.iter_mut().zip(&slots) {
581            node.move_to_mut(Point::new(slot.bounds.x, slot.bounds.y));
582        }
583
584        let size = limits.resolve(self.width, self.height, intrinsic_size);
585        let node = layout::Node::with_children(size.expand(self.padding), nodes);
586        let state = tree.state.downcast_mut::<State<Key>>();
587        state.wrap_width = wrap_width;
588        self.sync_slot_positions(state, &slots);
589
590        node
591    }
592
593    fn operate(
594        &mut self,
595        tree: &mut Tree,
596        layout: Layout<'_>,
597        renderer: &Renderer,
598        operation: &mut dyn Operation,
599    ) {
600        operation.container(None, layout.bounds());
601        operation.traverse(&mut |operation| {
602            self.children
603                .iter_mut()
604                .zip(&mut tree.children)
605                .zip(layout.children())
606                .for_each(|((child, state), child_layout)| {
607                    child.as_widget_mut().operate(
608                        state,
609                        child_layout.with_virtual_offset(layout.virtual_offset()),
610                        renderer,
611                        operation,
612                    );
613                });
614        });
615    }
616
617    fn update(
618        &mut self,
619        tree: &mut Tree,
620        event: &Event,
621        layout: Layout<'_>,
622        cursor: Cursor,
623        renderer: &Renderer,
624        clipboard: &mut dyn Clipboard,
625        shell: &mut Shell<'_, Message>,
626        viewport: &Rectangle,
627    ) {
628        let bounds = layout.bounds();
629        let state = tree.state.downcast_mut::<State<Key>>();
630        let child_layouts: Vec<_> = layout.children().collect();
631
632        if let Event::Window(window::Event::RedrawRequested(_)) = event {
633            state.initialized = true;
634            state.finish_animations(self.animation_duration);
635            if state.drag.is_some() || state.is_animating(self.animation_duration) {
636                shell.request_redraw();
637            }
638        }
639
640        match event {
641            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
642                if state.drag.is_none()
643                    && state.pending_drag.is_none()
644                    && let Some(cursor_pos) = cursor.position()
645                    && let Some((index, child_layout)) = child_layouts
646                        .iter()
647                        .enumerate()
648                        .find(|(_, child_layout)| child_layout.bounds().contains(cursor_pos))
649                    && !self.locked.get(index).copied().unwrap_or(false)
650                    && let Some(reorder_index) = reorderable_index_for_child(&self.locked, index)
651                {
652                    let child_bounds = child_layout.bounds();
653                    state.pending_drag = Some(PendingDragState {
654                        key: self.keys[index].clone(),
655                        item_index: index,
656                        original_index: reorder_index,
657                        press_local: Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y),
658                        pointer_offset: Vector::new(
659                            cursor_pos.x - child_bounds.x,
660                            cursor_pos.y - child_bounds.y,
661                        ),
662                    });
663                }
664            }
665            Event::Mouse(mouse::Event::CursorMoved { .. }) => {
666                if let Some(pending) = state.pending_drag.clone()
667                    && let Some(cursor_pos) = cursor.position()
668                {
669                    let cursor_local = Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y);
670                    let delta = Vector::new(
671                        cursor_local.x - pending.press_local.x,
672                        cursor_local.y - pending.press_local.y,
673                    );
674                    let distance = (delta.x.powi(2) + delta.y.powi(2)).sqrt();
675
676                    if distance >= DEFAULT_DRAG_THRESHOLD {
677                        if let (Some(child), Some(child_tree), Some(child_layout)) = (
678                            self.children.get_mut(pending.item_index),
679                            tree.children.get_mut(pending.item_index),
680                            child_layouts.get(pending.item_index),
681                        ) {
682                            let cursor_left = Event::Mouse(mouse::Event::CursorLeft);
683                            child.as_widget_mut().update(
684                                child_tree,
685                                &cursor_left,
686                                child_layout.with_virtual_offset(layout.virtual_offset()),
687                                cursor,
688                                renderer,
689                                clipboard,
690                                shell,
691                                viewport,
692                            );
693                        }
694
695                        state.pending_drag = None;
696                        state.drag = Some(DragState {
697                            key: pending.key,
698                            item_index: pending.item_index,
699                            original_index: pending.original_index,
700                            current_index: pending.original_index,
701                            cursor_local,
702                            pointer_offset: pending.pointer_offset,
703                        });
704                        let slots = self.item_local_slots_from_layout(bounds, &child_layouts);
705                        self.sync_slot_positions(state, &slots);
706                        shell.capture_event();
707                        shell.request_redraw();
708                        return;
709                    }
710                }
711
712                if let Some(drag) = state.drag.as_mut()
713                    && let Some(cursor_pos) = cursor.position()
714                {
715                    drag.cursor_local =
716                        Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y);
717                    let slots = self.item_local_slots_from_layout(bounds, &child_layouts);
718                    self.sync_slot_positions(state, &slots);
719                    shell.capture_event();
720                    shell.request_redraw();
721                    return;
722                }
723            }
724            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
725                state.pending_drag = None;
726
727                if state.drag.is_some() {
728                    let slots = self.item_local_slots_from_layout(bounds, &child_layouts);
729                    self.sync_slot_positions(state, &slots);
730                }
731                if let Some(drag) = state.drag.take() {
732                    if drag.current_index != drag.original_index {
733                        let locked_by_key: HashMap<Key, bool> = self
734                            .keys
735                            .iter()
736                            .cloned()
737                            .zip(self.locked.iter().copied())
738                            .collect();
739                        let new_order = reordered_keys_for_drag(
740                            &self.keys,
741                            &locked_by_key,
742                            &drag.key,
743                            drag.current_index,
744                        );
745                        shell.publish((self.on_reorder)(new_order));
746                    }
747                    shell.capture_event();
748                    shell.request_redraw();
749                    return;
750                }
751            }
752            _ => {}
753        }
754
755        if state.drag.is_some() {
756            return;
757        }
758
759        for ((item, tree), child_layout) in self
760            .children
761            .iter_mut()
762            .zip(&mut tree.children)
763            .zip(child_layouts.into_iter())
764        {
765            item.as_widget_mut().update(
766                tree,
767                event,
768                child_layout.with_virtual_offset(layout.virtual_offset()),
769                cursor,
770                renderer,
771                clipboard,
772                shell,
773                viewport,
774            );
775        }
776    }
777
778    fn mouse_interaction(
779        &self,
780        tree: &Tree,
781        layout: Layout<'_>,
782        cursor: Cursor,
783        viewport: &Rectangle,
784        renderer: &Renderer,
785    ) -> mouse::Interaction {
786        let state = tree.state.downcast_ref::<State<Key>>();
787
788        if state.drag.is_some() {
789            return mouse::Interaction::Grabbing;
790        }
791
792        if let Some(cursor_pos) = cursor.position()
793            && self
794                .locked
795                .iter()
796                .zip(layout.children())
797                .any(|(locked, child_layout)| {
798                    !*locked && child_layout.bounds().contains(cursor_pos)
799                })
800        {
801            return mouse::Interaction::Grab;
802        }
803
804        self.children
805            .iter()
806            .zip(&tree.children)
807            .zip(layout.children())
808            .map(|((child, tree), child_layout)| {
809                child.as_widget().mouse_interaction(
810                    tree,
811                    child_layout.with_virtual_offset(layout.virtual_offset()),
812                    cursor,
813                    viewport,
814                    renderer,
815                )
816            })
817            .max()
818            .unwrap_or_default()
819    }
820
821    fn draw(
822        &self,
823        tree: &Tree,
824        renderer: &mut Renderer,
825        theme: &crate::Theme,
826        style: &renderer::Style,
827        layout: Layout<'_>,
828        cursor: Cursor,
829        viewport: &Rectangle,
830    ) {
831        let state = tree.state.downcast_ref::<State<Key>>();
832        let bounds = layout.bounds();
833
834        let Some(clipped_viewport) = bounds.intersection(viewport) else {
835            return;
836        };
837
838        let viewport = if self.clip {
839            &clipped_viewport
840        } else {
841            viewport
842        };
843        let drag_key = state.drag.as_ref().map(|drag| &drag.key);
844        let drag_item = state.drag.as_ref().and_then(|drag| {
845            self.keys
846                .iter()
847                .zip(&self.children)
848                .zip(&tree.children)
849                .zip(layout.children())
850                .find_map(|(((key, child), state), child_layout)| {
851                    (key == &drag.key).then_some((key, child, state, child_layout, drag))
852                })
853        });
854
855        for (((key, child), child_tree), child_layout) in self
856            .keys
857            .iter()
858            .zip(&self.children)
859            .zip(&tree.children)
860            .zip(layout.children())
861        {
862            if drag_key.is_some_and(|drag_key| drag_key == key) {
863                continue;
864            }
865
866            let child_layout = child_layout.with_virtual_offset(layout.virtual_offset());
867            let child_bounds = child_layout.bounds();
868            let base_local = Point::new(child_bounds.x - bounds.x, child_bounds.y - bounds.y);
869            let target_local =
870                state.current_slot_position(key, base_local, self.animation_duration);
871            let translation =
872                Vector::new(target_local.x - base_local.x, target_local.y - base_local.y);
873            let translated_bounds = translate_rect(child_bounds, translation);
874
875            if translated_bounds.intersects(viewport) {
876                renderer.with_translation(translation, |renderer| {
877                    child.as_widget().draw(
878                        child_tree,
879                        renderer,
880                        theme,
881                        style,
882                        child_layout,
883                        cursor,
884                        viewport,
885                    );
886                });
887            }
888        }
889
890        if let Some((_key, child, child_tree, child_layout, drag)) = drag_item {
891            let child_layout = child_layout.with_virtual_offset(layout.virtual_offset());
892            let child_bounds = child_layout.bounds();
893            let base_local = Point::new(child_bounds.x - bounds.x, child_bounds.y - bounds.y);
894            let drag_local = Point::new(
895                drag.cursor_local.x - drag.pointer_offset.x,
896                drag.cursor_local.y - drag.pointer_offset.y - self.drag_lift,
897            );
898            let translation = Vector::new(drag_local.x - base_local.x, drag_local.y - base_local.y);
899
900            #[cfg(feature = "wgpu")]
901            {
902                let translated_bounds = translate_rect(child_bounds, translation);
903                draw_drag_backdrop(renderer, theme, translated_bounds);
904            }
905
906            renderer.with_translation(translation, |renderer| {
907                child.as_widget().draw(
908                    child_tree,
909                    renderer,
910                    theme,
911                    style,
912                    child_layout,
913                    cursor,
914                    viewport,
915                );
916            });
917        }
918    }
919
920    fn overlay<'b>(
921        &'b mut self,
922        tree: &'b mut Tree,
923        layout: Layout<'b>,
924        renderer: &Renderer,
925        viewport: &Rectangle,
926        translation: Vector,
927    ) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
928        overlay::from_children(
929            &mut self.children,
930            tree,
931            layout,
932            renderer,
933            viewport,
934            translation,
935        )
936    }
937
938    fn drag_destinations(
939        &self,
940        state: &Tree,
941        layout: Layout<'_>,
942        renderer: &Renderer,
943        dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
944    ) {
945        for ((item, child_layout), child_state) in self
946            .children
947            .iter()
948            .zip(layout.children())
949            .zip(state.children.iter())
950        {
951            item.as_widget().drag_destinations(
952                child_state,
953                child_layout.with_virtual_offset(layout.virtual_offset()),
954                renderer,
955                dnd_rectangles,
956            );
957        }
958    }
959}
960
961impl<'a, Key, Message> From<ReorderableFlexRow<'a, Key, Message>> for Element<'a, Message>
962where
963    Key: Clone + Eq + Hash + 'static,
964    Message: 'a,
965{
966    fn from(row: ReorderableFlexRow<'a, Key, Message>) -> Self {
967        Element::new(row)
968    }
969}
970
971/// Create a horizontal flex row with drag-to-reorder behavior.
972pub fn reorderable_flex_row<'a, Key, Message>(
973    on_reorder: impl Fn(Vec<Key>) -> Message + 'a,
974) -> ReorderableFlexRow<'a, Key, Message>
975where
976    Key: Clone + Eq + Hash + 'static,
977{
978    ReorderableFlexRow::new(on_reorder)
979}
980
981fn compute_wrapped_slots<Key>(
982    ordered_keys: &[Key],
983    locked_by_key: &HashMap<Key, bool>,
984    size_by_key: &HashMap<Key, Size>,
985    wrap_width: f32,
986    padding: Padding,
987    spacing: f32,
988    align: Alignment,
989) -> (Vec<LocalSlot<Key>>, Size)
990where
991    Key: Clone + Eq + Hash + 'static,
992{
993    let wrap_width = if wrap_width.is_finite() {
994        wrap_width.max(0.0)
995    } else {
996        f32::INFINITY
997    };
998
999    let mut slots = Vec::with_capacity(ordered_keys.len());
1000    let mut intrinsic_size = Size::ZERO;
1001    let mut row_start = 0;
1002    let mut row_height = 0.0;
1003    let mut x = 0.0;
1004    let mut y = 0.0;
1005
1006    let align_factor = match align {
1007        Alignment::Start => 0.0,
1008        Alignment::Center => 2.0,
1009        Alignment::End => 1.0,
1010    };
1011
1012    let align_row =
1013        |range: std::ops::Range<usize>, row_height: f32, slots: &mut [LocalSlot<Key>]| {
1014            if align_factor == 0.0 {
1015                return;
1016            }
1017
1018            for slot in &mut slots[range] {
1019                slot.bounds.y += (row_height - slot.bounds.height) / align_factor;
1020            }
1021        };
1022
1023    for (index, key) in ordered_keys.iter().enumerate() {
1024        let size = size_by_key.get(key).copied().unwrap_or(Size::ZERO);
1025
1026        if x != 0.0 && x + size.width > wrap_width {
1027            intrinsic_size.width = intrinsic_size.width.max(x - spacing);
1028            align_row(row_start..index, row_height, &mut slots);
1029            y += row_height + spacing;
1030            x = 0.0;
1031            row_start = index;
1032            row_height = 0.0;
1033        }
1034
1035        row_height = row_height.max(size.height);
1036
1037        slots.push(LocalSlot {
1038            key: key.clone(),
1039            locked: locked_by_key.get(key).copied().unwrap_or(false),
1040            bounds: Rectangle {
1041                x: x + padding.left,
1042                y: y + padding.top,
1043                width: size.width,
1044                height: size.height,
1045            },
1046        });
1047
1048        x += size.width + spacing;
1049    }
1050
1051    if x != 0.0 {
1052        intrinsic_size.width = intrinsic_size.width.max(x - spacing);
1053    }
1054
1055    intrinsic_size.height = y + row_height;
1056    align_row(row_start..slots.len(), row_height, &mut slots);
1057
1058    (slots, intrinsic_size)
1059}
1060
1061fn reordered_keys_for_drag<Key>(
1062    ordered_keys: &[Key],
1063    locked_by_key: &HashMap<Key, bool>,
1064    dragged_key: &Key,
1065    target_index: usize,
1066) -> Vec<Key>
1067where
1068    Key: Clone + Eq + Hash + 'static,
1069{
1070    let movable_keys: Vec<Key> = ordered_keys
1071        .iter()
1072        .filter(|key| !locked_by_key.get(*key).copied().unwrap_or(false))
1073        .cloned()
1074        .collect();
1075    let mut remaining: Vec<Key> = movable_keys
1076        .iter()
1077        .filter(|key| *key != dragged_key)
1078        .cloned()
1079        .collect();
1080
1081    remaining.insert(target_index.min(remaining.len()), dragged_key.clone());
1082    merge_locked_and_reordered_keys(ordered_keys, locked_by_key, &remaining)
1083}
1084
1085/// Picks insertion index among movable items using row-aware midpoint rule.
1086///
1087/// Walks laid-out slots in reading order, counting how many non-dragged movable
1088/// items the cursor has moved past. Skips locked slots. O(n) single pass, no
1089/// allocations.
1090fn target_index_for_drag<Key>(
1091    slots: &[LocalSlot<Key>],
1092    dragged_key: &Key,
1093    dragged_center: Point,
1094) -> usize
1095where
1096    Key: Clone + Eq + Hash + 'static,
1097{
1098    let mut target = 0;
1099    let mut passed_movable: usize = 0;
1100    let mut found_target = false;
1101
1102    let mut i = 0;
1103    while i < slots.len() {
1104        let slot = &slots[i];
1105
1106        if slot.locked || &slot.key == dragged_key {
1107            i += 1;
1108            continue;
1109        }
1110
1111        let row_top = slot.bounds.y;
1112        let row_bottom = row_top + slot.bounds.height;
1113
1114        if !found_target && dragged_center.y < row_top {
1115            target = passed_movable;
1116            found_target = true;
1117            break;
1118        }
1119
1120        if dragged_center.y > row_bottom {
1121            passed_movable += 1;
1122            i += 1;
1123            continue;
1124        }
1125
1126        let center_x = slot.bounds.x + slot.bounds.width / 2.0;
1127        if dragged_center.x < center_x {
1128            target = passed_movable;
1129            found_target = true;
1130            break;
1131        }
1132
1133        passed_movable += 1;
1134        i += 1;
1135    }
1136
1137    if !found_target {
1138        target = passed_movable;
1139    }
1140
1141    target
1142}
1143
1144fn reorderable_index_for_child(locked: &[bool], item_index: usize) -> Option<usize> {
1145    (!locked.get(item_index).copied().unwrap_or(false)).then(|| {
1146        locked[..item_index]
1147            .iter()
1148            .filter(|is_locked| !**is_locked)
1149            .count()
1150    })
1151}
1152
1153fn merge_locked_and_reordered_keys<Key>(
1154    ordered_keys: &[Key],
1155    locked_by_key: &HashMap<Key, bool>,
1156    reordered_movable_keys: &[Key],
1157) -> Vec<Key>
1158where
1159    Key: Clone + Eq + Hash + 'static,
1160{
1161    let mut movable = reordered_movable_keys.iter();
1162
1163    ordered_keys
1164        .iter()
1165        .map(|key| {
1166            if locked_by_key.get(key).copied().unwrap_or(false) {
1167                key.clone()
1168            } else {
1169                movable.next().cloned().unwrap_or_else(|| key.clone())
1170            }
1171        })
1172        .collect()
1173}
1174
1175fn approx_eq_point(a: Point, b: Point) -> bool {
1176    (a.x - b.x).abs() <= POSITION_EPSILON && (a.y - b.y).abs() <= POSITION_EPSILON
1177}
1178
1179fn translate_rect(bounds: Rectangle, translation: Vector) -> Rectangle {
1180    Rectangle {
1181        x: bounds.x + translation.x,
1182        y: bounds.y + translation.y,
1183        width: bounds.width,
1184        height: bounds.height,
1185    }
1186}
1187
1188#[cfg(feature = "wgpu")]
1189fn draw_drag_backdrop(renderer: &mut Renderer, theme: &crate::Theme, bounds: Rectangle) {
1190    let cosmic = theme.cosmic();
1191    let backdrop = iced::Color {
1192        a: 0.08,
1193        ..iced::Color::from(cosmic.bg_component_color())
1194    };
1195
1196    renderer.fill_quad(
1197        renderer::Quad {
1198            bounds,
1199            border: Border {
1200                radius: cosmic.corner_radii.radius_m.into(),
1201                ..Border::default()
1202            },
1203            shadow: Shadow {
1204                color: cosmic.shade.into(),
1205                offset: Vector::new(0.0, 8.0),
1206                blur_radius: SHADOW_BLUR_RADIUS,
1207            },
1208            snap: true,
1209        },
1210        Background::Color(backdrop),
1211    );
1212}
1213
1214#[cfg(test)]
1215mod tests {
1216    use super::{compute_wrapped_slots, reordered_keys_for_drag, target_index_for_drag};
1217    use iced::{Alignment, Padding, Point, Size};
1218    use std::collections::HashMap;
1219
1220    fn size_map(keys: &[usize], width: f32, height: f32) -> HashMap<usize, Size> {
1221        keys.iter()
1222            .copied()
1223            .map(|key| (key, Size::new(width, height)))
1224            .collect()
1225    }
1226
1227    fn locked_map(keys: &[usize], locked_keys: &[usize]) -> HashMap<usize, bool> {
1228        keys.iter()
1229            .copied()
1230            .map(|key| (key, locked_keys.contains(&key)))
1231            .collect()
1232    }
1233
1234    #[test]
1235    fn compute_wrapped_slots_creates_new_rows() {
1236        let ordered_keys = vec![0, 1, 2];
1237        let locked_by_key = locked_map(&ordered_keys, &[]);
1238        let size_by_key = size_map(&ordered_keys, 100.0, 40.0);
1239        let (slots, intrinsic_size) = compute_wrapped_slots(
1240            &ordered_keys,
1241            &locked_by_key,
1242            &size_by_key,
1243            220.0,
1244            Padding::ZERO,
1245            10.0,
1246            Alignment::Start,
1247        );
1248
1249        assert_eq!(slots[0].bounds.x, 0.0);
1250        assert_eq!(slots[0].bounds.y, 0.0);
1251        assert_eq!(slots[1].bounds.x, 110.0);
1252        assert_eq!(slots[1].bounds.y, 0.0);
1253        assert_eq!(slots[2].bounds.x, 0.0);
1254        assert_eq!(slots[2].bounds.y, 50.0);
1255        assert_eq!(intrinsic_size.width, 210.0);
1256        assert_eq!(intrinsic_size.height, 90.0);
1257    }
1258
1259    #[test]
1260    fn reordered_keys_for_drag_inserts_key_at_target_index() {
1261        let keys = [0, 1, 2, 3];
1262        let locked_by_key = locked_map(&keys, &[]);
1263        let reordered = reordered_keys_for_drag(&keys, &locked_by_key, &0, 3);
1264        assert_eq!(reordered, vec![1, 2, 3, 0]);
1265    }
1266
1267    #[test]
1268    fn target_index_tracks_wrapped_drop_positions() {
1269        let ordered_keys = vec![0, 1, 2, 3];
1270        let locked_by_key = locked_map(&ordered_keys, &[]);
1271        let size_by_key = size_map(&ordered_keys, 100.0, 40.0);
1272
1273        let (slots, _) = compute_wrapped_slots(
1274            &ordered_keys,
1275            &locked_by_key,
1276            &size_by_key,
1277            220.0,
1278            Padding::ZERO,
1279            10.0,
1280            Alignment::Start,
1281        );
1282
1283        let target_index = target_index_for_drag(&slots, &0, Point::new(160.0, 70.0));
1284
1285        assert_eq!(target_index, 3);
1286    }
1287
1288    #[test]
1289    fn reordered_keys_for_drag_preserves_locked_positions() {
1290        let keys = [10, 11, 12, 13];
1291        let locked_by_key = locked_map(&keys, &[10, 13]);
1292        let reordered = reordered_keys_for_drag(&keys, &locked_by_key, &11, 1);
1293
1294        assert_eq!(reordered, vec![10, 12, 11, 13]);
1295    }
1296}