diff options
Diffstat (limited to 'crates/iced_drop/src')
| -rw-r--r-- | crates/iced_drop/src/lib.rs | 55 | ||||
| -rw-r--r-- | crates/iced_drop/src/widget.rs | 2 | ||||
| -rw-r--r-- | crates/iced_drop/src/widget/droppable.rs | 574 | ||||
| -rw-r--r-- | crates/iced_drop/src/widget/operation.rs | 1 | ||||
| -rw-r--r-- | crates/iced_drop/src/widget/operation/drop.rs | 88 |
5 files changed, 720 insertions, 0 deletions
diff --git a/crates/iced_drop/src/lib.rs b/crates/iced_drop/src/lib.rs new file mode 100644 index 0000000..c1e1b03 --- /dev/null +++ b/crates/iced_drop/src/lib.rs @@ -0,0 +1,55 @@ +pub mod widget; + +use iced::advanced::graphics::futures::MaybeSend; +use iced::advanced::renderer; +use iced::advanced::widget::{operate, Id}; +use iced::task::Task; +use iced::{Element, Point, Rectangle}; +use widget::droppable::*; +use widget::operation::drop; + +pub fn droppable<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Droppable<'a, Message, Theme, Renderer> +where + Message: Clone, + Renderer: renderer::Renderer, +{ + Droppable::new(content) +} + +pub fn zones_on_point<T, MF>( + msg: MF, + point: Point, + options: Option<Vec<Id>>, + depth: Option<usize>, +) -> Task<T> +where + T: Send + 'static, + MF: Fn(Vec<(Id, Rectangle)>) -> T + MaybeSend + Sync + Clone + 'static, +{ + operate(drop::find_zones( + move |bounds| bounds.contains(point), + options, + depth, + )) + .map(move |id| msg(id)) +} + +pub fn find_zones<Message, MF, F>( + msg: MF, + filter: F, + options: Option<Vec<Id>>, + depth: Option<usize>, +) -> Task<Message> +where + Message: Send + 'static, + MF: Fn(Vec<(Id, Rectangle)>) -> Message + + MaybeSend + + Sync + + Clone + + 'static, + F: Fn(&Rectangle) -> bool + Send + 'static, +{ + operate(drop::find_zones(filter, options, depth)).map(move |id| msg(id)) +} diff --git a/crates/iced_drop/src/widget.rs b/crates/iced_drop/src/widget.rs new file mode 100644 index 0000000..6b3fed2 --- /dev/null +++ b/crates/iced_drop/src/widget.rs @@ -0,0 +1,2 @@ +pub mod droppable; +pub mod operation; diff --git a/crates/iced_drop/src/widget/droppable.rs b/crates/iced_drop/src/widget/droppable.rs new file mode 100644 index 0000000..947cf5b --- /dev/null +++ b/crates/iced_drop/src/widget/droppable.rs @@ -0,0 +1,574 @@ +//! Encapsulates a widget that can be dragged and dropped. +use std::fmt::Debug; +use std::vec; + +use iced::advanced::widget::{Operation, Tree, Widget}; +use iced::advanced::{self, Layout, layout, mouse, overlay, renderer}; +use iced::{Element, Point, Rectangle, Size, Vector}; + +/// An element that can be dragged and dropped on a [`DropZone`] +pub struct Droppable< + 'a, + Message, + Theme = iced::Theme, + Renderer = iced::Renderer, +> where + Message: Clone, + Renderer: renderer::Renderer, +{ + content: Element<'a, Message, Theme, Renderer>, + id: Option<iced::advanced::widget::Id>, + on_click: Option<Message>, + on_drop: Option<Box<dyn Fn(Point, Rectangle) -> Message + 'a>>, + on_drag: Option<Box<dyn Fn(Point, Rectangle) -> Message + 'a>>, + on_cancel: Option<Message>, + drag_mode: Option<(bool, bool)>, + drag_overlay: bool, + drag_hide: bool, + drag_center: bool, + drag_size: Option<Size>, + reset_delay: usize, + status: Option<Status>, +} + +impl<'a, Message, Theme, Renderer> Droppable<'a, Message, Theme, Renderer> +where + Message: Clone, + Renderer: renderer::Renderer, +{ + /// Creates a new [`Droppable`]. + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + content: content.into(), + id: None, + on_click: None, + on_drop: None, + on_drag: None, + on_cancel: None, + drag_mode: Some((true, true)), + drag_overlay: true, + drag_hide: false, + drag_center: false, + drag_size: None, + reset_delay: 0, + status: None, + } + } + + /// Sets the unique identifier of the [`Droppable`]. + pub fn id(mut self, id: iced::advanced::widget::Id) -> Self { + self.id = Some(id); + self + } + + /// Sets the message that will be produced when the [`Droppable`] is clicked. + pub fn on_click(mut self, message: Message) -> Self { + self.on_click = Some(message); + self + } + + /// Sets the message that will be produced when the [`Droppable`] is dropped on a [`DropZone`]. + /// + /// Unless this is set, the [`Droppable`] will be disabled. + pub fn on_drop<F>(mut self, message: F) -> Self + where + F: Fn(Point, Rectangle) -> Message + 'a, + { + self.on_drop = Some(Box::new(message)); + self + } + + /// Sets the message that will be produced when the [`Droppable`] is dragged. + pub fn on_drag<F>(mut self, message: F) -> Self + where + F: Fn(Point, Rectangle) -> Message + 'a, + { + self.on_drag = Some(Box::new(message)); + self + } + + /// Sets the message that will be produced when the user right clicks while dragging the [`Droppable`]. + pub fn on_cancel(mut self, message: Message) -> Self { + self.on_cancel = Some(message); + self + } + + /// Sets whether the [`Droppable`] should be drawn under the cursor while dragging. + pub fn drag_overlay(mut self, drag_overlay: bool) -> Self { + self.drag_overlay = drag_overlay; + self + } + + /// Sets whether the [`Droppable`] should be hidden while dragging. + pub fn drag_hide(mut self, drag_hide: bool) -> Self { + self.drag_hide = drag_hide; + self + } + + /// Sets whether the [`Droppable`] should be centered on the cursor while dragging. + pub fn drag_center(mut self, drag_center: bool) -> Self { + self.drag_center = drag_center; + self + } + + // Sets whether the [`Droppable`] can be dragged along individual axes. + pub fn drag_mode(mut self, drag_x: bool, drag_y: bool) -> Self { + self.drag_mode = Some((drag_x, drag_y)); + self + } + + /// Sets whether the [`Droppable`] should be be resized to a given size while dragging. + pub fn drag_size(mut self, hide_size: Size) -> Self { + self.drag_size = Some(hide_size); + self + } + + /// Sets the number of frames/layout calls to wait before resetting the size of the [`Droppable`] after dropping. + /// + /// This is useful for cases where the [`Droppable`] is being moved to a new location after some widget operation. + /// In this case, the [`Droppable`] will mainting the 'drag_size' for the given number of frames before resetting to its original size. + /// This prevents the [`Droppable`] from 'jumping' back to its original size before the new location is rendered which + /// prevents flickering. + /// + /// Warning: this should only be set if there's is some noticeble flickering when the [`Droppable`] is dropped. That is, if the + /// [`Droppable`] returns to its original size before it's moved to it's new location. + pub fn reset_delay(mut self, reset_delay: usize) -> Self { + self.reset_delay = reset_delay; + self + } +} + +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Droppable<'a, Message, Theme, Renderer> +where + Message: Clone, + Renderer: renderer::Renderer, +{ + fn state(&self) -> iced::advanced::widget::tree::State { + advanced::widget::tree::State::new(State::default()) + } + + fn tag(&self) -> iced::advanced::widget::tree::Tag { + advanced::widget::tree::Tag::of::<State>() + } + + fn children(&self) -> Vec<iced::advanced::widget::Tree> { + vec![advanced::widget::Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut iced::advanced::widget::Tree) { + tree.diff_children(std::slice::from_ref(&self.content)) + } + + fn size(&self) -> iced::Size<iced::Length> { + self.content.as_widget().size() + } + + fn update( + &mut self, + tree: &mut iced::advanced::widget::Tree, + event: &iced::Event, + layout: iced::advanced::Layout<'_>, + cursor: iced::advanced::mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn iced::advanced::Clipboard, + shell: &mut iced::advanced::Shell<'_, Message>, + _viewport: &iced::Rectangle, + ) { + // handle the on event of the content first, in case that the droppable is nested + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + _renderer, + _clipboard, + shell, + _viewport, + ); + // this should really only be captured if the droppable is nested or it contains some other + // widget that captures the event + if shell.is_event_captured() { + return; + } + + if let Some(on_drop) = self.on_drop.as_deref() { + let state = tree.state.downcast_mut::<State>(); + if let iced::Event::Mouse(mouse) = event { + match mouse { + mouse::Event::ButtonPressed(btn) => { + if *btn == mouse::Button::Left + && cursor.is_over(layout.bounds()) + { + // select the droppable and store the position of the widget before dragging + state.action = + Action::Select(cursor.position().unwrap()); + let bounds = layout.bounds(); + state.widget_pos = bounds.position(); + state.overlay_bounds.width = bounds.width; + state.overlay_bounds.height = bounds.height; + + if let Some(on_click) = self.on_click.clone() { + shell.publish(on_click); + } + shell.capture_event(); + } else if *btn == mouse::Button::Right { + if let Action::Drag(_, _) = state.action { + shell.invalidate_layout(); + state.action = Action::None; + if let Some(on_cancel) = self.on_cancel.clone() + { + shell.publish(on_cancel); + } + } + } + } + mouse::Event::CursorMoved { mut position } => match state + .action + { + Action::Select(start) | Action::Drag(start, _) => { + // calculate the new position of the widget after dragging + + if let Some((drag_x, drag_y)) = self.drag_mode { + position = Point { + x: if drag_x { + position.x + } else { + start.x + }, + y: if drag_y { + position.y + } else { + start.y + }, + }; + } + + state.action = Action::Drag(start, position); + // update the position of the overlay since the cursor was moved + if self.drag_center { + state.overlay_bounds.x = position.x + - state.overlay_bounds.width / 2.0; + state.overlay_bounds.y = position.y + - state.overlay_bounds.height / 2.0; + } else { + state.overlay_bounds.x = + state.widget_pos.x + position.x - start.x; + state.overlay_bounds.y = + state.widget_pos.y + position.y - start.y; + } + // send on drag msg + if let Some(on_drag) = self.on_drag.as_deref() { + let message = + (on_drag)(position, state.overlay_bounds); + shell.publish(message); + } + + shell.request_redraw(); + } + _ => (), + }, + mouse::Event::ButtonReleased(mouse::Button::Left) => { + match state.action { + Action::Select(_) => { + state.action = Action::None; + } + Action::Drag(_, current) => { + // send on drop msg + let message = + (on_drop)(current, state.overlay_bounds); + shell.publish(message); + + if self.reset_delay == 0 { + state.action = Action::None; + } else { + state.action = + Action::Wait(self.reset_delay); + } + } + _ => (), + } + } + _ => {} + } + } + } + + let current_status = if cursor.is_over(layout.bounds()) { + if self.on_drop.is_none() { + Status::Disabled + } else { + let state = tree.state.downcast_ref::<State>(); + + if let Action::Drag(_, _) = state.action { + Status::Dragged + } else { + Status::Hovered + } + } + } else { + Status::Active + }; + + if let iced::Event::Window(iced::window::Event::RedrawRequested(_now)) = + event + { + self.status = Some(current_status); + } else if self.status.is_some_and(|status| status != current_status) { + shell.request_redraw(); + } + } + + fn layout( + &self, + tree: &mut iced::advanced::widget::Tree, + renderer: &Renderer, + limits: &iced::advanced::layout::Limits, + ) -> iced::advanced::layout::Node { + let state: &mut State = tree.state.downcast_mut::<State>(); + let content_node = self.content.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ); + + // Adjust the size of the original widget if it's being dragged or we're wating to reset the size + if let Some(new_size) = self.drag_size { + match state.action { + Action::Drag(_, _) => { + return iced::advanced::layout::Node::with_children( + new_size, + content_node.children().to_vec(), + ); + } + Action::Wait(reveal_index) => { + if reveal_index <= 1 { + state.action = Action::None; + } else { + state.action = Action::Wait(reveal_index - 1); + } + + return iced::advanced::layout::Node::with_children( + new_size, + content_node.children().to_vec(), + ); + } + _ => (), + } + } + + content_node + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + let state = tree.state.downcast_mut::<State>(); + operation.custom(self.id.as_ref(), layout.bounds(), state); + operation.container( + self.id.as_ref(), + layout.bounds(), + &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + }, + ); + } + + fn draw( + &self, + tree: &iced::advanced::widget::Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: iced::advanced::Layout<'_>, + cursor: iced::advanced::mouse::Cursor, + viewport: &iced::Rectangle, + ) { + let state: &State = tree.state.downcast_ref::<State>(); + if let Action::Drag(_, _) = state.action { + if self.drag_hide { + return; + } + } + + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + &viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + _translation: Vector, + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + let state: &mut State = tree.state.downcast_mut::<State>(); + if self.drag_overlay { + if let Action::Drag(_, _) = state.action { + return Some(overlay::Element::new(Box::new(Overlay { + content: &self.content, + tree: &mut tree.children[0], + overlay_bounds: state.overlay_bounds, + }))); + } + } + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + _translation, + ) + } + + fn mouse_interaction( + &self, + tree: &iced::advanced::widget::Tree, + layout: iced::advanced::Layout<'_>, + cursor: iced::advanced::mouse::Cursor, + _viewport: &iced::Rectangle, + _renderer: &Renderer, + ) -> iced::advanced::mouse::Interaction { + let child_interact = self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + _viewport, + _renderer, + ); + + if child_interact != mouse::Interaction::default() { + return child_interact; + } + + let state = tree.state.downcast_ref::<State>(); + + if self.on_drop.is_none() && cursor.is_over(layout.bounds()) { + return mouse::Interaction::NotAllowed; + } + + if let Action::Drag(_, _) = state.action { + return mouse::Interaction::Grabbing; + } + + if cursor.is_over(layout.bounds()) { + return mouse::Interaction::Pointer; + } + + mouse::Interaction::default() + } +} + +impl<'a, Message, Theme, Renderer> From<Droppable<'a, Message, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a + Clone, + Theme: 'a, + Renderer: 'a + renderer::Renderer, +{ + fn from( + droppable: Droppable<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(droppable) + } +} + +#[derive(Default, Clone, Copy, PartialEq, Debug)] +pub struct State { + widget_pos: Point, + overlay_bounds: Rectangle, + action: Action, +} + +#[derive(Default, Clone, Copy, PartialEq, Debug)] +pub enum Status { + #[default] + Active, + Hovered, + Dragged, + Disabled, +} + +#[derive(Default, Clone, Copy, PartialEq, Debug)] +pub enum Action { + #[default] + None, + /// (point clicked) + Select(Point), + /// (start pos, current pos) + Drag(Point, Point), + /// (frames to wait) + Wait(usize), +} + +struct Overlay<'a, 'b, Message, Theme, Renderer> +where + Renderer: renderer::Renderer, +{ + content: &'b Element<'a, Message, Theme, Renderer>, + tree: &'b mut advanced::widget::Tree, + overlay_bounds: Rectangle, +} + +impl<'a, 'b, Message, Theme, Renderer> + overlay::Overlay<Message, Theme, Renderer> + for Overlay<'a, 'b, Message, Theme, Renderer> +where + Renderer: renderer::Renderer, +{ + fn layout(&mut self, renderer: &Renderer, _bounds: Size) -> layout::Node { + Widget::<Message, Theme, Renderer>::layout( + self.content.as_widget(), + self.tree, + renderer, + &layout::Limits::new(Size::ZERO, self.overlay_bounds.size()), + ) + .move_to(self.overlay_bounds.position()) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Theme, + inherited_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + ) { + Widget::<Message, Theme, Renderer>::draw( + self.content.as_widget(), + self.tree, + renderer, + theme, + inherited_style, + layout, + cursor_position, + &Rectangle::with_size(Size::INFINITY), + ); + } + + fn is_over( + &self, + _layout: Layout<'_>, + _renderer: &Renderer, + _cursor_position: Point, + ) -> bool { + false + } +} diff --git a/crates/iced_drop/src/widget/operation.rs b/crates/iced_drop/src/widget/operation.rs new file mode 100644 index 0000000..3d7dcff --- /dev/null +++ b/crates/iced_drop/src/widget/operation.rs @@ -0,0 +1 @@ +pub mod drop; diff --git a/crates/iced_drop/src/widget/operation/drop.rs b/crates/iced_drop/src/widget/operation/drop.rs new file mode 100644 index 0000000..ead412c --- /dev/null +++ b/crates/iced_drop/src/widget/operation/drop.rs @@ -0,0 +1,88 @@ +use iced::advanced::widget::operation::{Outcome, Scrollable}; +use iced::advanced::widget::{Id, Operation}; +use iced::{Rectangle, Vector}; + +/// Produces an [`Operation`] that will find the drop zones that pass a filter on the zone's bounds. +/// For any drop zone to be considered, the Element must have some Id. +/// If `options` is `None`, all drop zones will be considered. +/// Depth determines how how deep into nested drop zones to go. +/// If 'depth' is `None`, nested dropzones will be fully explored +pub fn find_zones<F>( + filter: F, + options: Option<Vec<Id>>, + depth: Option<usize>, +) -> impl Operation<Vec<(Id, Rectangle)>> +where + F: Fn(&Rectangle) -> bool + Send + 'static, +{ + struct FindDropZone<F> { + filter: F, + options: Option<Vec<Id>>, + zones: Vec<(Id, Rectangle)>, + max_depth: Option<usize>, + c_depth: usize, + offset: Vector, + } + + impl<F> Operation<Vec<(Id, Rectangle)>> for FindDropZone<F> + where + F: Fn(&Rectangle) -> bool + Send + 'static, + { + fn container( + &mut self, + id: Option<&Id>, + bounds: iced::Rectangle, + operate_on_children: &mut dyn FnMut( + &mut dyn Operation<Vec<(Id, Rectangle)>>, + ), + ) { + match id { + Some(id) => { + let is_option = match &self.options { + Some(options) => options.contains(id), + None => true, + }; + let bounds = bounds - self.offset; + if is_option && (self.filter)(&bounds) { + self.c_depth += 1; + self.zones.push((id.clone(), bounds)); + } + } + None => (), + } + let goto_next = match &self.max_depth { + Some(m_depth) => self.c_depth < *m_depth, + None => true, + }; + if goto_next { + operate_on_children(self); + } + } + + fn finish(&self) -> Outcome<Vec<(Id, Rectangle)>> { + Outcome::Some(self.zones.clone()) + } + + fn scrollable( + &mut self, + _id: Option<&Id>, + bounds: Rectangle, + _content_bounds: Rectangle, + translation: Vector, + _state: &mut dyn Scrollable, + ) { + if (self.filter)(&bounds) { + self.offset = self.offset + translation; + } + } + } + + FindDropZone { + filter, + options, + zones: vec![], + max_depth: depth, + c_depth: 0, + offset: Vector { x: 0.0, y: 0.0 }, + } +} |
