libcosmic-yoda/src/widget/reorderable_flex_row/widget.rs
2026-05-24 09:50:50 +02:00

1296 lines
39 KiB
Rust

// Copyright 2026 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::{Element, Renderer};
use iced::{Alignment, Pixels, alignment};
use iced_core::event::Event;
use iced_core::layout::{self, Layout};
use iced_core::mouse::{self, Cursor};
use iced_core::widget::Operation;
use iced_core::widget::tree::{self, Tree};
#[cfg(feature = "wgpu")]
use iced_core::{Background, Border, Shadow};
use iced_core::{
Clipboard, Length, Padding, Point, Rectangle, Renderer as _, Shell, Size, Vector, Widget,
overlay, renderer, window,
};
use std::collections::{HashMap, HashSet};
use std::hash::Hash;
use std::time::{Duration, Instant};
const DEFAULT_ANIMATION_DURATION: Duration = Duration::from_millis(180);
const DEFAULT_DRAG_LIFT: f32 = 10.0;
const DEFAULT_DRAG_THRESHOLD: f32 = 6.0;
const SHADOW_BLUR_RADIUS: f32 = 20.0;
const POSITION_EPSILON: f32 = 0.5;
#[derive(Debug, Clone)]
struct SlotAnimation {
from: Point,
to: Point,
started_at: Option<Instant>,
}
impl SlotAnimation {
fn new(position: Point) -> Self {
Self {
from: position,
to: position,
started_at: None,
}
}
fn current_position(&self, duration: Duration) -> Point {
let Some(started_at) = self.started_at else {
return self.to;
};
let duration_secs = duration.as_secs_f32();
if duration_secs <= f32::EPSILON {
return self.to;
}
let progress = (started_at.elapsed().as_secs_f32() / duration_secs).clamp(0.0, 1.0);
let eased = 1.0 - (1.0 - progress).powi(3);
Point::new(
self.from.x + (self.to.x - self.from.x) * eased,
self.from.y + (self.to.y - self.from.y) * eased,
)
}
fn retarget(&mut self, new_target: Point, duration: Duration) {
if approx_eq_point(self.to, new_target) {
if !self.is_animating(duration) {
self.from = new_target;
self.to = new_target;
self.started_at = None;
}
return;
}
self.from = self.current_position(duration);
self.to = new_target;
self.started_at = Some(Instant::now());
}
fn is_animating(&self, duration: Duration) -> bool {
self.started_at
.is_some_and(|started_at| started_at.elapsed() < duration)
}
fn finish_if_done(&mut self, duration: Duration) {
if self
.started_at
.is_some_and(|started_at| started_at.elapsed() >= duration)
{
self.from = self.to;
self.started_at = None;
}
}
}
#[derive(Debug, Clone)]
struct PendingDragState<Key>
where
Key: Clone + Eq + Hash + 'static,
{
key: Key,
item_index: usize,
original_index: usize,
press_local: Point,
pointer_offset: Vector,
}
#[derive(Debug, Clone)]
struct DragState<Key>
where
Key: Clone + Eq + Hash + 'static,
{
key: Key,
item_index: usize,
original_index: usize,
current_index: usize,
cursor_local: Point,
pointer_offset: Vector,
}
#[derive(Debug, Clone)]
struct State<Key>
where
Key: Clone + Eq + Hash + 'static,
{
keys: Vec<Key>,
slot_positions: HashMap<Key, SlotAnimation>,
pending_drag: Option<PendingDragState<Key>>,
drag: Option<DragState<Key>>,
wrap_width: f32,
initialized: bool,
}
impl<Key> Default for State<Key>
where
Key: Clone + Eq + Hash + 'static,
{
fn default() -> Self {
Self {
keys: Vec::new(),
slot_positions: HashMap::new(),
pending_drag: None,
drag: None,
wrap_width: f32::INFINITY,
initialized: false,
}
}
}
impl<Key> State<Key>
where
Key: Clone + Eq + Hash + 'static,
{
fn retain_keys(&mut self, keys: &[Key]) {
let keep: HashSet<_> = keys.iter().cloned().collect();
self.slot_positions.retain(|key, _| keep.contains(key));
if self
.pending_drag
.as_ref()
.is_some_and(|drag| !keep.contains(&drag.key))
{
self.pending_drag = None;
}
if self
.drag
.as_ref()
.is_some_and(|drag| !keep.contains(&drag.key))
{
self.drag = None;
}
}
fn finish_animations(&mut self, duration: Duration) {
self.slot_positions
.values_mut()
.for_each(|slot| slot.finish_if_done(duration));
}
fn is_animating(&self, duration: Duration) -> bool {
self.slot_positions
.values()
.any(|slot| slot.is_animating(duration))
}
fn current_slot_position(&self, key: &Key, fallback: Point, duration: Duration) -> Point {
self.slot_positions
.get(key)
.map(|slot| slot.current_position(duration))
.unwrap_or(fallback)
}
fn retarget_slot(&mut self, key: &Key, target: Point, duration: Duration) {
self.slot_positions
.entry(key.clone())
.or_insert_with(|| SlotAnimation::new(target))
.retarget(target, duration);
}
fn snap_slot(&mut self, key: &Key, target: Point) {
self.slot_positions
.insert(key.clone(), SlotAnimation::new(target));
}
fn apply_layout_position(&mut self, key: &Key, target: Point, duration: Duration) {
if self.initialized {
self.retarget_slot(key, target, duration);
} else {
self.snap_slot(key, target);
}
}
}
#[derive(Debug, Clone)]
struct LocalSlot<Key>
where
Key: Clone + Eq + Hash + 'static,
{
key: Key,
locked: bool,
bounds: Rectangle,
}
/// A horizontal flex row with drag-to-reorder behavior.
#[must_use]
pub struct ReorderableFlexRow<'a, Key, Message>
where
Key: Clone + Eq + Hash + 'static,
{
spacing: f32,
padding: Padding,
width: Length,
height: Length,
align: Alignment,
clip: bool,
drag_lift: f32,
animation_duration: Duration,
on_reorder: Box<dyn Fn(Vec<Key>) -> Message + 'a>,
keys: Vec<Key>,
locked: Vec<bool>,
children: Vec<Element<'a, Message>>,
}
impl<'a, Key, Message> ReorderableFlexRow<'a, Key, Message>
where
Key: Clone + Eq + Hash + 'static,
{
pub fn new(on_reorder: impl Fn(Vec<Key>) -> Message + 'a) -> Self {
Self {
spacing: 0.0,
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
align: Alignment::Start,
clip: false,
drag_lift: DEFAULT_DRAG_LIFT,
animation_duration: DEFAULT_ANIMATION_DURATION,
on_reorder: Box::new(on_reorder),
keys: Vec::new(),
locked: Vec::new(),
children: Vec::new(),
}
}
pub fn with_capacity(capacity: usize, on_reorder: impl Fn(Vec<Key>) -> Message + 'a) -> Self {
let mut row = Self::new(on_reorder);
row.keys = Vec::with_capacity(capacity);
row.locked = Vec::with_capacity(capacity);
row.children = Vec::with_capacity(capacity);
row
}
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
self.spacing = amount.into().0;
self
}
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self {
self.align = Alignment::from(align.into());
self
}
/// Leave disabled for dragged item to visibly lift above the row.
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
pub fn drag_lift(mut self, lift: f32) -> Self {
self.drag_lift = lift.max(0.0);
self
}
pub fn animation_duration(mut self, duration: Duration) -> Self {
self.animation_duration = duration;
self
}
pub fn push(self, key: Key, child: impl Into<Element<'a, Message>>) -> Self {
self.push_with_lock(key, false, child)
}
/// Item stays fixed, never participates in reordering.
pub fn push_locked(self, key: Key, child: impl Into<Element<'a, Message>>) -> Self {
self.push_with_lock(key, true, child)
}
fn push_with_lock(
mut self,
key: Key,
locked: bool,
child: impl Into<Element<'a, Message>>,
) -> Self {
let child = child.into();
let child_size = child.as_widget().size_hint();
if !child_size.is_void() {
self.width = self.width.enclose(child_size.width);
self.height = self.height.enclose(child_size.height);
self.keys.push(key);
self.locked.push(locked);
self.children.push(child);
}
self
}
pub fn extend(self, items: impl IntoIterator<Item = (Key, Element<'a, Message>)>) -> Self {
items
.into_iter()
.fold(self, |row, (key, child)| row.push(key, child))
}
pub fn extend_locked(
self,
items: impl IntoIterator<Item = (Key, Element<'a, Message>)>,
) -> Self {
items
.into_iter()
.fold(self, |row, (key, child)| row.push_locked(key, child))
}
fn item_local_slots_from_layout(
&self,
bounds: Rectangle,
child_layouts: &[Layout<'_>],
) -> Vec<LocalSlot<Key>> {
self.keys
.iter()
.zip(self.locked.iter())
.zip(child_layouts.iter())
.map(|((key, locked), child_layout)| {
let child_bounds = child_layout.bounds();
LocalSlot {
key: key.clone(),
locked: *locked,
bounds: Rectangle {
x: child_bounds.x - bounds.x,
y: child_bounds.y - bounds.y,
width: child_bounds.width,
height: child_bounds.height,
},
}
})
.collect()
}
fn sync_slot_positions(&self, state: &mut State<Key>, slots: &[LocalSlot<Key>]) {
let ordered_keys: Vec<Key> = slots.iter().map(|slot| slot.key.clone()).collect();
state.retain_keys(&ordered_keys);
let size_by_key: HashMap<Key, Size> = slots
.iter()
.map(|slot| {
(
slot.key.clone(),
Size::new(slot.bounds.width, slot.bounds.height),
)
})
.collect();
let locked_by_key: HashMap<Key, bool> = slots
.iter()
.map(|slot| (slot.key.clone(), slot.locked))
.collect();
let Some(drag_snapshot) = state.drag.as_ref().map(|drag| {
(
drag.key.clone(),
drag.cursor_local,
drag.pointer_offset,
drag.item_index,
)
}) else {
for slot in slots {
state.apply_layout_position(
&slot.key,
Point::new(slot.bounds.x, slot.bounds.y),
self.animation_duration,
);
}
return;
};
let (drag_key, cursor_local, pointer_offset, drag_item_index) = drag_snapshot;
let Some(dragged_slot) = slots.iter().find(|slot| slot.key == drag_key) else {
state.drag = None;
for slot in slots {
state.apply_layout_position(
&slot.key,
Point::new(slot.bounds.x, slot.bounds.y),
self.animation_duration,
);
}
return;
};
if dragged_slot.locked {
state.drag = None;
return;
}
let dragged_center = Point::new(
cursor_local.x - pointer_offset.x + dragged_slot.bounds.width / 2.0,
cursor_local.y - pointer_offset.y + dragged_slot.bounds.height / 2.0,
);
let target_index = target_index_for_drag(slots, &drag_key, dragged_center);
let prior_index = state.drag.as_ref().map(|drag| drag.current_index);
if let Some(drag) = state.drag.as_mut() {
drag.current_index = target_index;
drag.item_index = drag_item_index;
}
if prior_index == Some(target_index) {
return;
}
let reordered_keys =
reordered_keys_for_drag(&ordered_keys, &locked_by_key, &drag_key, target_index);
let (target_slots, _) = compute_wrapped_slots(
&reordered_keys,
&locked_by_key,
&size_by_key,
state.wrap_width,
self.padding,
self.spacing,
self.align,
);
let target_positions: HashMap<Key, Point> = target_slots
.iter()
.map(|slot| (slot.key.clone(), Point::new(slot.bounds.x, slot.bounds.y)))
.collect();
for slot in slots {
let target = target_positions
.get(&slot.key)
.copied()
.unwrap_or(Point::new(slot.bounds.x, slot.bounds.y));
state.retarget_slot(&slot.key, target, self.animation_duration);
}
}
}
impl<'a, Key, Message> Widget<Message, crate::Theme, Renderer>
for ReorderableFlexRow<'a, Key, Message>
where
Key: Clone + Eq + Hash + 'static,
Message: 'a,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<Key>>()
}
fn state(&self) -> tree::State {
tree::State::new(State {
keys: self.keys.clone(),
..State::default()
})
}
fn children(&self) -> Vec<Tree> {
self.children.iter().map(Tree::new).collect()
}
fn diff(&mut self, tree: &mut Tree) {
let Tree {
state, children, ..
} = tree;
let state = state.downcast_mut::<State<Key>>();
let previous_keys = state.keys.clone();
let previous_children = std::mem::take(children);
let mut previous_by_key = HashMap::with_capacity(previous_keys.len());
for (key, child_tree) in previous_keys.into_iter().zip(previous_children) {
previous_by_key.insert(key, child_tree);
}
children.reserve(self.children.len());
for (key, child) in self.keys.iter().cloned().zip(self.children.iter_mut()) {
let mut child_tree = previous_by_key
.remove(&key)
.unwrap_or_else(|| Tree::new(child.as_widget()));
child.as_widget_mut().diff(&mut child_tree);
children.push(child_tree);
}
if state.keys != self.keys {
state.keys.clone_from(&self.keys);
}
state.retain_keys(&self.keys);
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits
.width(self.width)
.height(self.height)
.shrink(self.padding);
let child_limits = limits.loose();
let wrap_width = limits.max().width;
let mut nodes = Vec::with_capacity(self.children.len());
let mut size_by_key = HashMap::with_capacity(self.children.len());
let locked_by_key: HashMap<Key, bool> = self
.keys
.iter()
.cloned()
.zip(self.locked.iter().copied())
.collect();
for ((key, child), child_tree) in self
.keys
.iter()
.zip(self.children.iter_mut())
.zip(tree.children.iter_mut())
{
let node = child
.as_widget_mut()
.layout(child_tree, renderer, &child_limits);
size_by_key.insert(key.clone(), node.size());
nodes.push(node);
}
let (slots, intrinsic_size) = compute_wrapped_slots(
&self.keys,
&locked_by_key,
&size_by_key,
wrap_width,
self.padding,
self.spacing,
self.align,
);
for (node, slot) in nodes.iter_mut().zip(&slots) {
node.move_to_mut(Point::new(slot.bounds.x, slot.bounds.y));
}
let size = limits.resolve(self.width, self.height, intrinsic_size);
let node = layout::Node::with_children(size.expand(self.padding), nodes);
let state = tree.state.downcast_mut::<State<Key>>();
state.wrap_width = wrap_width;
self.sync_slot_positions(state, &slots);
node
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds());
operation.traverse(&mut |operation| {
self.children
.iter_mut()
.zip(&mut tree.children)
.zip(layout.children())
.for_each(|((child, state), child_layout)| {
child.as_widget_mut().operate(
state,
child_layout.with_virtual_offset(layout.virtual_offset()),
renderer,
operation,
);
});
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
let bounds = layout.bounds();
let state = tree.state.downcast_mut::<State<Key>>();
let child_layouts: Vec<_> = layout.children().collect();
if let Event::Window(window::Event::RedrawRequested(_)) = event {
state.initialized = true;
state.finish_animations(self.animation_duration);
if state.drag.is_some() || state.is_animating(self.animation_duration) {
shell.request_redraw();
}
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
if state.drag.is_none()
&& state.pending_drag.is_none()
&& let Some(cursor_pos) = cursor.position()
&& let Some((index, child_layout)) = child_layouts
.iter()
.enumerate()
.find(|(_, child_layout)| child_layout.bounds().contains(cursor_pos))
&& !self.locked.get(index).copied().unwrap_or(false)
&& let Some(reorder_index) = reorderable_index_for_child(&self.locked, index)
{
let child_bounds = child_layout.bounds();
state.pending_drag = Some(PendingDragState {
key: self.keys[index].clone(),
item_index: index,
original_index: reorder_index,
press_local: Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y),
pointer_offset: Vector::new(
cursor_pos.x - child_bounds.x,
cursor_pos.y - child_bounds.y,
),
});
}
}
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some(pending) = state.pending_drag.clone()
&& let Some(cursor_pos) = cursor.position()
{
let cursor_local = Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y);
let delta = Vector::new(
cursor_local.x - pending.press_local.x,
cursor_local.y - pending.press_local.y,
);
let distance = (delta.x.powi(2) + delta.y.powi(2)).sqrt();
if distance >= DEFAULT_DRAG_THRESHOLD {
if let (Some(child), Some(child_tree), Some(child_layout)) = (
self.children.get_mut(pending.item_index),
tree.children.get_mut(pending.item_index),
child_layouts.get(pending.item_index),
) {
let cursor_left = Event::Mouse(mouse::Event::CursorLeft);
child.as_widget_mut().update(
child_tree,
&cursor_left,
child_layout.with_virtual_offset(layout.virtual_offset()),
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
state.pending_drag = None;
state.drag = Some(DragState {
key: pending.key,
item_index: pending.item_index,
original_index: pending.original_index,
current_index: pending.original_index,
cursor_local,
pointer_offset: pending.pointer_offset,
});
let slots = self.item_local_slots_from_layout(bounds, &child_layouts);
self.sync_slot_positions(state, &slots);
shell.capture_event();
shell.request_redraw();
return;
}
}
if let Some(drag) = state.drag.as_mut()
&& let Some(cursor_pos) = cursor.position()
{
drag.cursor_local =
Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y);
let slots = self.item_local_slots_from_layout(bounds, &child_layouts);
self.sync_slot_positions(state, &slots);
shell.capture_event();
shell.request_redraw();
return;
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
state.pending_drag = None;
if state.drag.is_some() {
let slots = self.item_local_slots_from_layout(bounds, &child_layouts);
self.sync_slot_positions(state, &slots);
}
if let Some(drag) = state.drag.take() {
if drag.current_index != drag.original_index {
let locked_by_key: HashMap<Key, bool> = self
.keys
.iter()
.cloned()
.zip(self.locked.iter().copied())
.collect();
let new_order = reordered_keys_for_drag(
&self.keys,
&locked_by_key,
&drag.key,
drag.current_index,
);
shell.publish((self.on_reorder)(new_order));
}
shell.capture_event();
shell.request_redraw();
return;
}
}
_ => {}
}
if state.drag.is_some() {
return;
}
for ((item, tree), child_layout) in self
.children
.iter_mut()
.zip(&mut tree.children)
.zip(child_layouts.into_iter())
{
item.as_widget_mut().update(
tree,
event,
child_layout.with_virtual_offset(layout.virtual_offset()),
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let state = tree.state.downcast_ref::<State<Key>>();
if state.drag.is_some() {
return mouse::Interaction::Grabbing;
}
if let Some(cursor_pos) = cursor.position()
&& self
.locked
.iter()
.zip(layout.children())
.any(|(locked, child_layout)| {
!*locked && child_layout.bounds().contains(cursor_pos)
})
{
return mouse::Interaction::Grab;
}
self.children
.iter()
.zip(&tree.children)
.zip(layout.children())
.map(|((child, tree), child_layout)| {
child.as_widget().mouse_interaction(
tree,
child_layout.with_virtual_offset(layout.virtual_offset()),
cursor,
viewport,
renderer,
)
})
.max()
.unwrap_or_default()
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &crate::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State<Key>>();
let bounds = layout.bounds();
let Some(clipped_viewport) = bounds.intersection(viewport) else {
return;
};
let viewport = if self.clip {
&clipped_viewport
} else {
viewport
};
let drag_key = state.drag.as_ref().map(|drag| &drag.key);
let drag_item = state.drag.as_ref().and_then(|drag| {
self.keys
.iter()
.zip(&self.children)
.zip(&tree.children)
.zip(layout.children())
.find_map(|(((key, child), state), child_layout)| {
(key == &drag.key).then_some((key, child, state, child_layout, drag))
})
});
for (((key, child), child_tree), child_layout) in self
.keys
.iter()
.zip(&self.children)
.zip(&tree.children)
.zip(layout.children())
{
if drag_key.is_some_and(|drag_key| drag_key == key) {
continue;
}
let child_layout = child_layout.with_virtual_offset(layout.virtual_offset());
let child_bounds = child_layout.bounds();
let base_local = Point::new(child_bounds.x - bounds.x, child_bounds.y - bounds.y);
let target_local =
state.current_slot_position(key, base_local, self.animation_duration);
let translation =
Vector::new(target_local.x - base_local.x, target_local.y - base_local.y);
let translated_bounds = translate_rect(child_bounds, translation);
if translated_bounds.intersects(viewport) {
renderer.with_translation(translation, |renderer| {
child.as_widget().draw(
child_tree,
renderer,
theme,
style,
child_layout,
cursor,
viewport,
);
});
}
}
if let Some((_key, child, child_tree, child_layout, drag)) = drag_item {
let child_layout = child_layout.with_virtual_offset(layout.virtual_offset());
let child_bounds = child_layout.bounds();
let base_local = Point::new(child_bounds.x - bounds.x, child_bounds.y - bounds.y);
let drag_local = Point::new(
drag.cursor_local.x - drag.pointer_offset.x,
drag.cursor_local.y - drag.pointer_offset.y - self.drag_lift,
);
let translation = Vector::new(drag_local.x - base_local.x, drag_local.y - base_local.y);
#[cfg(feature = "wgpu")]
{
let translated_bounds = translate_rect(child_bounds, translation);
draw_drag_backdrop(renderer, theme, translated_bounds);
}
renderer.with_translation(translation, |renderer| {
child.as_widget().draw(
child_tree,
renderer,
theme,
style,
child_layout,
cursor,
viewport,
);
});
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
overlay::from_children(
&mut self.children,
tree,
layout,
renderer,
viewport,
translation,
)
}
fn drag_destinations(
&self,
state: &Tree,
layout: Layout<'_>,
renderer: &Renderer,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
for ((item, child_layout), child_state) in self
.children
.iter()
.zip(layout.children())
.zip(state.children.iter())
{
item.as_widget().drag_destinations(
child_state,
child_layout.with_virtual_offset(layout.virtual_offset()),
renderer,
dnd_rectangles,
);
}
}
}
impl<'a, Key, Message> From<ReorderableFlexRow<'a, Key, Message>> for Element<'a, Message>
where
Key: Clone + Eq + Hash + 'static,
Message: 'a,
{
fn from(row: ReorderableFlexRow<'a, Key, Message>) -> Self {
Element::new(row)
}
}
/// Create a horizontal flex row with drag-to-reorder behavior.
pub fn reorderable_flex_row<'a, Key, Message>(
on_reorder: impl Fn(Vec<Key>) -> Message + 'a,
) -> ReorderableFlexRow<'a, Key, Message>
where
Key: Clone + Eq + Hash + 'static,
{
ReorderableFlexRow::new(on_reorder)
}
fn compute_wrapped_slots<Key>(
ordered_keys: &[Key],
locked_by_key: &HashMap<Key, bool>,
size_by_key: &HashMap<Key, Size>,
wrap_width: f32,
padding: Padding,
spacing: f32,
align: Alignment,
) -> (Vec<LocalSlot<Key>>, Size)
where
Key: Clone + Eq + Hash + 'static,
{
let wrap_width = if wrap_width.is_finite() {
wrap_width.max(0.0)
} else {
f32::INFINITY
};
let mut slots = Vec::with_capacity(ordered_keys.len());
let mut intrinsic_size = Size::ZERO;
let mut row_start = 0;
let mut row_height = 0.0;
let mut x = 0.0;
let mut y = 0.0;
let align_factor = match align {
Alignment::Start => 0.0,
Alignment::Center => 2.0,
Alignment::End => 1.0,
};
let align_row =
|range: std::ops::Range<usize>, row_height: f32, slots: &mut [LocalSlot<Key>]| {
if align_factor == 0.0 {
return;
}
for slot in &mut slots[range] {
slot.bounds.y += (row_height - slot.bounds.height) / align_factor;
}
};
for (index, key) in ordered_keys.iter().enumerate() {
let size = size_by_key.get(key).copied().unwrap_or(Size::ZERO);
if x != 0.0 && x + size.width > wrap_width {
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
align_row(row_start..index, row_height, &mut slots);
y += row_height + spacing;
x = 0.0;
row_start = index;
row_height = 0.0;
}
row_height = row_height.max(size.height);
slots.push(LocalSlot {
key: key.clone(),
locked: locked_by_key.get(key).copied().unwrap_or(false),
bounds: Rectangle {
x: x + padding.left,
y: y + padding.top,
width: size.width,
height: size.height,
},
});
x += size.width + spacing;
}
if x != 0.0 {
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
}
intrinsic_size.height = y + row_height;
align_row(row_start..slots.len(), row_height, &mut slots);
(slots, intrinsic_size)
}
fn reordered_keys_for_drag<Key>(
ordered_keys: &[Key],
locked_by_key: &HashMap<Key, bool>,
dragged_key: &Key,
target_index: usize,
) -> Vec<Key>
where
Key: Clone + Eq + Hash + 'static,
{
let movable_keys: Vec<Key> = ordered_keys
.iter()
.filter(|key| !locked_by_key.get(*key).copied().unwrap_or(false))
.cloned()
.collect();
let mut remaining: Vec<Key> = movable_keys
.iter()
.filter(|key| *key != dragged_key)
.cloned()
.collect();
remaining.insert(target_index.min(remaining.len()), dragged_key.clone());
merge_locked_and_reordered_keys(ordered_keys, locked_by_key, &remaining)
}
/// Picks insertion index among movable items using row-aware midpoint rule.
///
/// Walks laid-out slots in reading order, counting how many non-dragged movable
/// items the cursor has moved past. Skips locked slots. O(n) single pass, no
/// allocations.
fn target_index_for_drag<Key>(
slots: &[LocalSlot<Key>],
dragged_key: &Key,
dragged_center: Point,
) -> usize
where
Key: Clone + Eq + Hash + 'static,
{
let mut target = 0;
let mut passed_movable: usize = 0;
let mut found_target = false;
let mut i = 0;
while i < slots.len() {
let slot = &slots[i];
if slot.locked || &slot.key == dragged_key {
i += 1;
continue;
}
let row_top = slot.bounds.y;
let row_bottom = row_top + slot.bounds.height;
if !found_target && dragged_center.y < row_top {
target = passed_movable;
found_target = true;
break;
}
if dragged_center.y > row_bottom {
passed_movable += 1;
i += 1;
continue;
}
let center_x = slot.bounds.x + slot.bounds.width / 2.0;
if dragged_center.x < center_x {
target = passed_movable;
found_target = true;
break;
}
passed_movable += 1;
i += 1;
}
if !found_target {
target = passed_movable;
}
target
}
fn reorderable_index_for_child(locked: &[bool], item_index: usize) -> Option<usize> {
(!locked.get(item_index).copied().unwrap_or(false)).then(|| {
locked[..item_index]
.iter()
.filter(|is_locked| !**is_locked)
.count()
})
}
fn merge_locked_and_reordered_keys<Key>(
ordered_keys: &[Key],
locked_by_key: &HashMap<Key, bool>,
reordered_movable_keys: &[Key],
) -> Vec<Key>
where
Key: Clone + Eq + Hash + 'static,
{
let mut movable = reordered_movable_keys.iter();
ordered_keys
.iter()
.map(|key| {
if locked_by_key.get(key).copied().unwrap_or(false) {
key.clone()
} else {
movable.next().cloned().unwrap_or_else(|| key.clone())
}
})
.collect()
}
fn approx_eq_point(a: Point, b: Point) -> bool {
(a.x - b.x).abs() <= POSITION_EPSILON && (a.y - b.y).abs() <= POSITION_EPSILON
}
fn translate_rect(bounds: Rectangle, translation: Vector) -> Rectangle {
Rectangle {
x: bounds.x + translation.x,
y: bounds.y + translation.y,
width: bounds.width,
height: bounds.height,
}
}
#[cfg(feature = "wgpu")]
fn draw_drag_backdrop(renderer: &mut Renderer, theme: &crate::Theme, bounds: Rectangle) {
let cosmic = theme.cosmic();
let backdrop = iced::Color {
a: 0.08,
..iced::Color::from(cosmic.bg_component_color())
};
renderer.fill_quad(
renderer::Quad {
bounds,
border: Border {
radius: cosmic.corner_radii.radius_m.into(),
..Border::default()
},
shadow: Shadow {
color: cosmic.shade.into(),
offset: Vector::new(0.0, 8.0),
blur_radius: SHADOW_BLUR_RADIUS,
},
snap: true,
},
Background::Color(backdrop),
);
}
#[cfg(test)]
mod tests {
use super::{compute_wrapped_slots, reordered_keys_for_drag, target_index_for_drag};
use iced::{Alignment, Padding, Point, Size};
use std::collections::HashMap;
fn size_map(keys: &[usize], width: f32, height: f32) -> HashMap<usize, Size> {
keys.iter()
.copied()
.map(|key| (key, Size::new(width, height)))
.collect()
}
fn locked_map(keys: &[usize], locked_keys: &[usize]) -> HashMap<usize, bool> {
keys.iter()
.copied()
.map(|key| (key, locked_keys.contains(&key)))
.collect()
}
#[test]
fn compute_wrapped_slots_creates_new_rows() {
let ordered_keys = vec![0, 1, 2];
let locked_by_key = locked_map(&ordered_keys, &[]);
let size_by_key = size_map(&ordered_keys, 100.0, 40.0);
let (slots, intrinsic_size) = compute_wrapped_slots(
&ordered_keys,
&locked_by_key,
&size_by_key,
220.0,
Padding::ZERO,
10.0,
Alignment::Start,
);
assert_eq!(slots[0].bounds.x, 0.0);
assert_eq!(slots[0].bounds.y, 0.0);
assert_eq!(slots[1].bounds.x, 110.0);
assert_eq!(slots[1].bounds.y, 0.0);
assert_eq!(slots[2].bounds.x, 0.0);
assert_eq!(slots[2].bounds.y, 50.0);
assert_eq!(intrinsic_size.width, 210.0);
assert_eq!(intrinsic_size.height, 90.0);
}
#[test]
fn reordered_keys_for_drag_inserts_key_at_target_index() {
let keys = [0, 1, 2, 3];
let locked_by_key = locked_map(&keys, &[]);
let reordered = reordered_keys_for_drag(&keys, &locked_by_key, &0, 3);
assert_eq!(reordered, vec![1, 2, 3, 0]);
}
#[test]
fn target_index_tracks_wrapped_drop_positions() {
let ordered_keys = vec![0, 1, 2, 3];
let locked_by_key = locked_map(&ordered_keys, &[]);
let size_by_key = size_map(&ordered_keys, 100.0, 40.0);
let (slots, _) = compute_wrapped_slots(
&ordered_keys,
&locked_by_key,
&size_by_key,
220.0,
Padding::ZERO,
10.0,
Alignment::Start,
);
let target_index = target_index_for_drag(&slots, &0, Point::new(160.0, 70.0));
assert_eq!(target_index, 3);
}
#[test]
fn reordered_keys_for_drag_preserves_locked_positions() {
let keys = [10, 11, 12, 13];
let locked_by_key = locked_map(&keys, &[10, 13]);
let reordered = reordered_keys_for_drag(&keys, &locked_by_key, &11, 1);
assert_eq!(reordered, vec![10, 12, 11, 13]);
}
}