├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── color │ ├── Cargo.toml │ └── src │ │ └── main.rs └── todo │ ├── Cargo.toml │ └── src │ ├── highlight.rs │ ├── main.rs │ ├── operation.rs │ ├── theme.rs │ ├── theme │ ├── button.rs │ ├── container.rs │ ├── text.rs │ └── text_input.rs │ └── tree.rs └── src ├── lib.rs ├── widget.rs └── widget ├── droppable.rs ├── operation.rs └── operation └── drop.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose --all 21 | - name: Run tests 22 | run: cargo test --verbose --all 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vscode 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "iced_drop" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies.iced] 7 | version = "0.13" 8 | features = ["advanced"] 9 | 10 | [workspace] 11 | members = ["examples/*"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 jhannyj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iced_drop 2 | 3 | A small library which provides a custom widget and operation to make drag and drop easier to implement in [iced](https://github.com/iced-rs/iced/tree/master) 4 | 5 | ## Usage 6 | 7 | To add drag and drog functionality, first define two messages with the following format 8 | 9 | ```rust 10 | enum Message { 11 | Drop(iced::Point, iced::Rectangle) 12 | HandleZones(Vec<(iced::advanced::widget::Id, iced::Rectangle)>) 13 | } 14 | ``` 15 | 16 | The `Drop` message will be sent when the droppable is being dragged, and the left mouse button is released. This message provides the mouse position and layout boundaries of the droppable at the release point. 17 | 18 | The `HandleZones` message will be sent after an operation that finds the drop zones under the mouse position. It provides the Id and bounds for each drop zone. 19 | 20 | Next, create create a droppable in the view method and assign the on_drop message. The dropopable function takes an `impl Into` object, so it's easy to make a droppable from any iced widget. 21 | 22 | ```rust 23 | iced_drop::droppable("Drop me!").on_drop(Message::Drop); 24 | ``` 25 | 26 | Next, create a "drop zone." A drop zone is any widget that operates like a container andhas some assigned Id. It's important that the widget is assigned some Id or it won't be recognized as a drop zone. 27 | 28 | ```rust 29 | iced::widget::container("Drop zone") 30 | .id(iced::widget::container::Id::new("drop_zone")); 31 | ``` 32 | 33 | Finally, handle the updates of the drop messages 34 | 35 | ```rust 36 | match message { 37 | Message::Drop(cursor_pos, _) => { 38 | return iced_drop::zones_on_point( 39 | Message::HandleZonesFound, 40 | point, 41 | None, 42 | None, 43 | ); 44 | } 45 | Message::HandleZones(zones) => { 46 | println!("{:?}", zones) 47 | } 48 | } 49 | ``` 50 | 51 | On Drop, we return a widget operation that looks for drop zones under the cursor_pos. When this operation finishes, it returns the zones found and sends the `HandleZones` message. In this example, we only defined one zone, so the zones vector will either be empty if the droppable was not dropped on the zone, or it will contain the `drop_zone` 52 | 53 | ## Examples 54 | 55 | There are two examples: color, todo. 56 | 57 | The color example is a very basic drag/drop showcase where the user can drag colors into zones and change the zone's color. I would start here. 58 | 59 | [Link to video](https://drive.google.com/file/d/1K1CCi2Lc90IUyDufsvoUBZmUCbeg6_Fi/view?usp=sharing) 60 | 61 | To run this examples: `cargo run -p color` 62 | 63 | The todo example is a basic todo board application similar to Trello. This is a much much more complex example as it handles custom highlighting and nested droppables, but it just shows you can make some pretty cool things with iced. 64 | 65 | [Link to video](https://drive.google.com/file/d/1MLOCk4Imd_oUnrTj_psbpYbwua976HmR/view?usp=sharing) 66 | 67 | To run this example try: `cargo run -p todo` 68 | 69 | Note: the todo example might also be a good example on how one can use operations. Check examples/todo/src/operation.rs. I didn't find any other examples of this in the iced repo except for the built in focus operations. 70 | 71 | ## Future Development 72 | 73 | Right now it's a little annoying having to work with iced's Id type. At some point, I will work on a drop_zone widget that can take some generic clonable type as an id, and I will create a seperate find_zones operation that will return a list of this custom Id. This should make it easier to determine which drop zones were found. 74 | -------------------------------------------------------------------------------- /examples/color/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "color" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | iced = { version = "0.13", features = ["advanced"] } 8 | iced_drop = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/color/src/main.rs: -------------------------------------------------------------------------------- 1 | use iced::widget::container::Id as CId; 2 | use iced::Border; 3 | use iced::{ 4 | advanced::widget::Id, 5 | widget::{column, container, row, text}, 6 | Element, Fill, Length, Point, Rectangle, Task, 7 | }; 8 | use iced_drop::droppable; 9 | 10 | const HEADER_HEIGHT: f32 = 80.0; 11 | const COLORS_HEIGHT: f32 = 40.0; 12 | const COLORS_ROUNDNESS: f32 = 10.0; 13 | const COLORS_CONTAINER_WIDTH: f32 = 130.0; 14 | 15 | fn main() -> iced::Result { 16 | iced::application( 17 | ColorDropper::title, 18 | ColorDropper::update, 19 | ColorDropper::view, 20 | ) 21 | .theme(ColorDropper::theme) 22 | .run() 23 | } 24 | 25 | #[derive(Debug, Clone)] 26 | enum Message { 27 | DropColor(DColor, Point, Rectangle), 28 | HandleZonesFound(DColor, Vec<(Id, Rectangle)>), 29 | } 30 | 31 | struct ColorDropper { 32 | left_color: DColor, 33 | right_color: DColor, 34 | left: iced::widget::container::Id, 35 | right: iced::widget::container::Id, 36 | } 37 | 38 | impl Default for ColorDropper { 39 | fn default() -> Self { 40 | Self { 41 | left_color: DColor::Default, 42 | right_color: DColor::Default, 43 | left: CId::new("left"), 44 | right: CId::new("right"), 45 | } 46 | } 47 | } 48 | 49 | impl ColorDropper { 50 | fn title(&self) -> String { 51 | "Basic".to_string() 52 | } 53 | 54 | fn theme(&self) -> iced::Theme { 55 | iced::Theme::CatppuccinFrappe 56 | } 57 | 58 | fn update(&mut self, message: Message) -> Task { 59 | match message { 60 | Message::DropColor(color, point, _bounds) => { 61 | return iced_drop::zones_on_point( 62 | move |zones| Message::HandleZonesFound(color, zones), 63 | point, 64 | None, 65 | None, 66 | ); 67 | } 68 | Message::HandleZonesFound(color, zones) => { 69 | if let Some((zone, _)) = zones.get(0) { 70 | if *zone == self.left.clone().into() { 71 | self.left_color = color; 72 | } else { 73 | self.right_color = color; 74 | } 75 | } 76 | } 77 | } 78 | Task::none() 79 | } 80 | 81 | fn view(&self) -> Element<'_, Message> { 82 | let header = container(text("Color Dropper").size(30)) 83 | .padding(10.0) 84 | .width(Length::Fill) 85 | .height(Length::Fixed(HEADER_HEIGHT)); 86 | let colors = DColor::SHOWN.iter().map(|color| { 87 | let color = *color; 88 | droppable( 89 | container(text(color.to_string()).size(20)) 90 | .center(Fill) 91 | .style(move |_| color.style()) 92 | .width(Length::Fill) 93 | .height(Length::Fixed(COLORS_HEIGHT)), 94 | ) 95 | .on_drop(move |point, rect| Message::DropColor(color, point, rect)) 96 | .into() 97 | }); 98 | let colors_holder = container(column(colors).spacing(20.0).padding(20.0)) 99 | .center(Fill) 100 | .height(Length::Fill) 101 | .width(Length::Fixed(COLORS_CONTAINER_WIDTH)); 102 | column![ 103 | header, 104 | row![ 105 | colors_holder, 106 | drop_zone(self.left_color, self.left.clone()), 107 | drop_zone(self.right_color, self.right.clone()) 108 | ] 109 | .spacing(5) 110 | ] 111 | .padding(5) 112 | .into() 113 | } 114 | } 115 | 116 | fn drop_zone<'a>( 117 | color: DColor, 118 | id: iced::widget::container::Id, 119 | ) -> iced::Element<'a, Message, iced::Theme, iced::Renderer> { 120 | container(text(color.fun_fact()).size(20)) 121 | .style(move |_| color.style()) 122 | .id(id) 123 | .width(Fill) 124 | .height(Fill) 125 | .center(Fill) 126 | .into() 127 | } 128 | 129 | #[derive(Debug, Clone, Copy, Default)] 130 | enum DColor { 131 | #[default] 132 | Default, 133 | Red, 134 | Green, 135 | Blue, 136 | Yellow, 137 | Purple, 138 | Orange, 139 | Black, 140 | White, 141 | Gray, 142 | Pink, 143 | } 144 | 145 | impl std::fmt::Display for DColor { 146 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 147 | write!(f, "{:?}", self) 148 | } 149 | } 150 | 151 | impl DColor { 152 | const SHOWN: [DColor; 10] = [ 153 | DColor::Red, 154 | DColor::Green, 155 | DColor::Blue, 156 | DColor::Yellow, 157 | DColor::Purple, 158 | DColor::Orange, 159 | DColor::Black, 160 | DColor::White, 161 | DColor::Gray, 162 | DColor::Pink, 163 | ]; 164 | 165 | fn color(&self) -> iced::Color { 166 | match self { 167 | DColor::Default => iced::Color::from_rgb8(245, 245, 245), 168 | DColor::Red => iced::Color::from_rgb8(220, 20, 20), 169 | DColor::Green => iced::Color::from_rgb8(50, 205, 50), 170 | DColor::Blue => iced::Color::from_rgb8(40, 80, 150), 171 | DColor::Yellow => iced::Color::from_rgb8(255, 215, 0), 172 | DColor::Purple => iced::Color::from_rgb8(100, 50, 150), 173 | DColor::Orange => iced::Color::from_rgb8(255, 140, 0), 174 | DColor::Black => iced::Color::from_rgb8(20, 20, 20), 175 | DColor::White => iced::Color::from_rgb8(250, 250, 250), 176 | DColor::Gray => iced::Color::from_rgb8(105, 105, 105), 177 | DColor::Pink => iced::Color::from_rgb8(255, 105, 180), 178 | } 179 | } 180 | 181 | fn text_color(&self) -> iced::Color { 182 | match self { 183 | DColor::Default 184 | | DColor::Yellow 185 | | DColor::Orange 186 | | DColor::Green 187 | | DColor::White 188 | | DColor::Pink => iced::Color::BLACK, 189 | _ => iced::Color::WHITE, 190 | } 191 | } 192 | 193 | fn style(&self) -> container::Style { 194 | iced::widget::container::Style { 195 | background: Some(self.color().into()), 196 | border: Border { 197 | color: iced::Color::BLACK, 198 | width: 1.0, 199 | radius: COLORS_ROUNDNESS.into(), 200 | }, 201 | text_color: Some(self.text_color()), 202 | ..Default::default() 203 | } 204 | } 205 | 206 | fn fun_fact<'a>(&self) -> &'a str { 207 | match self { 208 | DColor::Default => "Drop a color here to learn about it!", 209 | DColor::Red => "Red is the color of fire and blood", 210 | DColor::Green => "Green is the color of life and renewal", 211 | DColor::Blue => "Blue is the color of the sky and sea", 212 | DColor::Yellow => "Yellow is the color of sunshine and happiness", 213 | DColor::Purple => "Purple is the color of royalty and luxury", 214 | DColor::Orange => "Orange is the color of creativity and determination", 215 | DColor::Black => "Black is the color of power and elegance", 216 | DColor::White => "White is the color of purity and innocence", 217 | DColor::Gray => "Gray is the color of compromise and control", 218 | DColor::Pink => "Pink is the color of love and compassion", 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /examples/todo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todo" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | iced = { version = "0.13", features = ["advanced"] } 8 | iced_drop = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/todo/src/highlight.rs: -------------------------------------------------------------------------------- 1 | //! Highlighting of droppable elements and drop zones 2 | use iced::Rectangle; 3 | 4 | use crate::tree::{TreeData, TreeElement, TreeLocation}; 5 | 6 | /// The state of the drag and drop highlight. 7 | /// None means no highlight 8 | #[derive(Clone, Default)] 9 | pub struct Highlight { 10 | pub dragging: Option<(TreeLocation, Rectangle)>, 11 | pub hovered: Option, 12 | } 13 | 14 | /// Describes some ['Droppable'] that is to be highlighted 15 | pub trait Highlightable { 16 | fn set_highlight(&mut self, highlight: bool); 17 | } 18 | 19 | /// Set the droppable to be highlighted 20 | pub fn dragged(info: &Highlight, loc: TreeLocation, bounds: Rectangle) -> Highlight { 21 | Highlight { 22 | dragging: Some((loc, bounds)), 23 | ..info.clone() 24 | } 25 | } 26 | 27 | /// Determine if the current zone should be de-highlighted and if there is a new zone to be highlighted 28 | pub fn zones_found(info: &Highlight, zones: &Vec<(TreeLocation, Rectangle)>) -> Highlight { 29 | let mut new_info = info.clone(); 30 | 31 | if zones.is_empty() { 32 | new_info.hovered = None; 33 | } 34 | 35 | if let Some((_, bounds)) = info.dragging { 36 | let mut split_zones: [Vec<(TreeLocation, Rectangle)>; 2] = [vec![], vec![]]; 37 | for zone in zones { 38 | let is_task = match zone.0.element() { 39 | TreeElement::Todo(_) => true, 40 | _ => false, 41 | }; 42 | 43 | if is_task { 44 | split_zones[0].push(zone.clone()); 45 | } else { 46 | split_zones[1].push(zone.clone()); 47 | } 48 | } 49 | let valid_zones = if split_zones[0].is_empty() { 50 | &split_zones[1] 51 | } else { 52 | &split_zones[0] 53 | }; 54 | if let Some((id, _)) = bigggest_intersect_area(valid_zones, &bounds) { 55 | new_info.hovered = Some(id.clone()); 56 | } 57 | } 58 | new_info 59 | } 60 | 61 | /// De-highlight everything 62 | pub fn dropped() -> Highlight { 63 | Highlight::default() 64 | } 65 | 66 | pub fn should_update_droppable( 67 | old_info: &Highlight, 68 | new_info: &Highlight, 69 | loc: &TreeLocation, 70 | ) -> bool { 71 | match &old_info.dragging { 72 | Some((d_id, _)) => *d_id == *loc, 73 | None => { 74 | if new_info.dragging.is_some() { 75 | true 76 | } else { 77 | false 78 | } 79 | } 80 | } 81 | } 82 | 83 | pub fn zone_update(old_info: &Highlight, new_info: &Highlight) -> ZoneUpdate { 84 | match &old_info.hovered { 85 | Some(o_id) => match &new_info.hovered { 86 | Some(n_id) => { 87 | if *o_id != *n_id { 88 | ZoneUpdate::Replace 89 | } else { 90 | ZoneUpdate::None 91 | } 92 | } 93 | None => ZoneUpdate::RemoveHighlight, 94 | }, 95 | None => match &new_info.hovered { 96 | Some(_) => ZoneUpdate::Highlight, 97 | None => ZoneUpdate::None, 98 | }, 99 | } 100 | } 101 | 102 | pub fn set_hovered(tree: &mut TreeData, info: &Highlight, highlight: bool) { 103 | if let Some(loc) = info.hovered.as_ref() { 104 | match loc.element() { 105 | &TreeElement::Slot => tree.slot_mut(loc.slot()).set_highlight(highlight), 106 | &TreeElement::List => tree.list_mut(&loc).set_highlight(highlight), 107 | &TreeElement::Todo(_) => { 108 | if let Some(task) = tree.todo_mut(&loc) { 109 | task.set_highlight(highlight); 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | #[derive(PartialEq, Eq, Debug)] 117 | pub enum ZoneUpdate { 118 | RemoveHighlight, 119 | Highlight, 120 | Replace, 121 | None, 122 | } 123 | 124 | impl ZoneUpdate { 125 | pub fn update(&self, tree: &mut TreeData, old_info: &Highlight, new_info: &Highlight) { 126 | match self { 127 | ZoneUpdate::RemoveHighlight => set_hovered(tree, old_info, false), 128 | ZoneUpdate::Highlight => set_hovered(tree, new_info, true), 129 | ZoneUpdate::Replace => { 130 | set_hovered(tree, old_info, false); 131 | set_hovered(tree, new_info, true); 132 | } 133 | ZoneUpdate::None => (), 134 | } 135 | } 136 | } 137 | 138 | /// Returns the id and area of the zone with the biggest intersection with the droppable rectangle 139 | fn bigggest_intersect_area<'a>( 140 | zones: &'a Vec<(TreeLocation, Rectangle)>, 141 | droppable: &Rectangle, 142 | ) -> Option<(&'a TreeLocation, f32)> { 143 | zones 144 | .iter() 145 | .map(|(id, rect)| { 146 | ( 147 | id, 148 | rect.intersection(&droppable) 149 | .unwrap_or(Rectangle::default()), 150 | ) 151 | }) 152 | .map(|(id, rect)| (id, rect.area())) 153 | .max_by(|(_, a), (_, b)| a.total_cmp(b)) 154 | } 155 | -------------------------------------------------------------------------------- /examples/todo/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(get_many_mut)] 2 | #![feature(hash_raw_entry)] 3 | 4 | use std::time::Instant; 5 | 6 | use highlight::{should_update_droppable, zone_update, Highlight, Highlightable, ZoneUpdate}; 7 | use iced::{ 8 | advanced::widget::Id, 9 | widget::{column, container, text, text_input}, 10 | Element, Length, Point, Rectangle, Task, 11 | }; 12 | use iced_drop::find_zones; 13 | use iced_drop::widget::droppable::State as DroppableState; 14 | use operation::swap_modify_states; 15 | use tree::{List, Slot, Todo, TreeData, TreeElement, TreeLocation}; 16 | 17 | mod highlight; 18 | mod operation; 19 | mod theme; 20 | mod tree; 21 | 22 | const HEADER_HEIGHT: f32 = 80.0; 23 | const DOUBLE_CLICK_TIME: u128 = 500; 24 | 25 | fn main() -> iced::Result { 26 | iced::application(TodoBoard::title, TodoBoard::update, TodoBoard::view) 27 | .theme(TodoBoard::theme) 28 | .run() 29 | } 30 | 31 | #[derive(Debug, Clone)] 32 | enum Message { 33 | // To-do editing 34 | EditTodo(TreeLocation, iced::widget::text_input::Id), 35 | UpdateTodo(TreeLocation, String), 36 | StopEditingTodo, 37 | 38 | // To-do creation 39 | UpdateTodoWriter(TreeLocation, String), 40 | WriteTodo(TreeLocation), 41 | 42 | // Drag/drop to-dos 43 | DragTodo(TreeLocation, Point, Rectangle), 44 | HandleTodoZones(Vec<(Id, Rectangle)>), 45 | #[allow(dead_code)] 46 | DropTodo(TreeLocation, Point, Rectangle), 47 | TodoDropCanceled, 48 | 49 | // Drag/drop lists 50 | #[allow(dead_code)] 51 | DragList(TreeLocation, Point, Rectangle), 52 | HandleListZones(Vec<(Id, Rectangle)>), 53 | #[allow(dead_code)] 54 | DropList(TreeLocation, Point, Rectangle), 55 | ListDropCanceled, 56 | } 57 | 58 | struct TodoBoard { 59 | tree: TreeData, 60 | clicked: (TreeLocation, Instant), 61 | editing: Option, 62 | todos_highlight: highlight::Highlight, 63 | lists_highlight: highlight::Highlight, 64 | } 65 | 66 | impl Default for TodoBoard { 67 | fn default() -> Self { 68 | Self { 69 | tree: TreeData::new(vec![ 70 | Slot::new(List::new("Todo", vec![Todo::new("Fix bugs")])), 71 | Slot::new(List::new("Doing", vec![Todo::new("Write code")])), 72 | Slot::new(List::new("Done", vec![Todo::new("Drag and drop")])), 73 | ]), 74 | clicked: (tree::NULL_TODO_LOC, Instant::now()), 75 | editing: None, 76 | todos_highlight: Highlight::default(), 77 | lists_highlight: Highlight::default(), 78 | } 79 | } 80 | } 81 | 82 | impl TodoBoard { 83 | fn title(&self) -> String { 84 | "Todo".to_string() 85 | } 86 | 87 | fn theme(&self) -> iced::Theme { 88 | iced::Theme::CatppuccinFrappe 89 | } 90 | 91 | fn view(&self) -> Element<'_, Message> { 92 | let header = container(text("TODO Board").size(30).style(theme::text::title)) 93 | .padding(10.0) 94 | .width(Length::Fill) 95 | .height(Length::Fixed(HEADER_HEIGHT)) 96 | .style(theme::container::title); 97 | container( 98 | column![header, self.tree.view()] 99 | .height(Length::Fill) 100 | .width(Length::Fill), 101 | ) 102 | .width(Length::Fill) 103 | .height(Length::Fill) 104 | .style(theme::container::background) 105 | .into() 106 | } 107 | 108 | fn update(&mut self, message: Message) -> Task { 109 | match message { 110 | Message::EditTodo(t_loc, ti_id) => { 111 | self.stop_editing(); 112 | 113 | let (clicked, time) = &self.clicked; 114 | if *clicked == t_loc && time.elapsed().as_millis() < DOUBLE_CLICK_TIME { 115 | if let Some(todo) = self.tree.todo_mut(&t_loc) { 116 | todo.editing = true; 117 | self.editing = Some(t_loc); 118 | return text_input::focus(ti_id); 119 | } 120 | } 121 | self.clicked = (t_loc, Instant::now()); 122 | } 123 | Message::UpdateTodo(t_loc, content) => { 124 | if let Some(todo) = self.tree.todo_mut(&t_loc) { 125 | todo.content = content; 126 | } 127 | } 128 | Message::StopEditingTodo => { 129 | self.stop_editing(); 130 | } 131 | // To-do drag/drop 132 | Message::DragTodo(t_loc, __, t_bounds) => { 133 | let new_highlight = 134 | highlight::dragged(&self.todos_highlight, t_loc.clone(), t_bounds); 135 | if should_update_droppable(&self.todos_highlight, &new_highlight, &t_loc) { 136 | if let Some(todo) = self.tree.todo_mut(&t_loc) { 137 | todo.set_highlight(true) 138 | } 139 | } 140 | self.todos_highlight = new_highlight; 141 | return find_zones( 142 | Message::HandleTodoZones, 143 | move |zone_bounds| zone_bounds.intersects(&t_bounds), 144 | Some(self.tree.todo_options(&t_loc)), 145 | None, 146 | ); 147 | } 148 | Message::HandleTodoZones(zones) => { 149 | let new_highlight = 150 | highlight::zones_found(&self.todos_highlight, &map_zones(&self.tree, zones)); 151 | zone_update(&self.todos_highlight, &new_highlight).update( 152 | &mut self.tree, 153 | &self.todos_highlight, 154 | &new_highlight, 155 | ); 156 | self.todos_highlight = new_highlight; 157 | } 158 | Message::DropTodo(t_loc, _, _) => { 159 | if let Some(h_loc) = &self.todos_highlight.hovered { 160 | match h_loc.element() { 161 | TreeElement::List => todo_dropped_on_list(&mut self.tree, &t_loc, &h_loc), 162 | TreeElement::Todo(_) => { 163 | todo_dropped_on_todo(&mut self.tree, &t_loc, &h_loc) 164 | } 165 | _ => (), 166 | } 167 | } else { 168 | self.tree.list_mut(&t_loc).remove(&t_loc); 169 | } 170 | self.todos_highlight = highlight::dropped(); 171 | } 172 | 173 | // List drag/drop 174 | Message::DragList(l_loc, _, l_bounds) => { 175 | let new_highlight = 176 | highlight::dragged(&self.lists_highlight, l_loc.clone(), l_bounds); 177 | if should_update_droppable(&self.lists_highlight, &new_highlight, &l_loc) { 178 | self.tree.list_mut(&l_loc).set_highlight(false); 179 | } 180 | self.lists_highlight = new_highlight; 181 | return find_zones( 182 | Message::HandleListZones, 183 | move |zone_bounds| zone_bounds.intersects(&l_bounds), 184 | Some(self.tree.list_options()), 185 | None, 186 | ); 187 | } 188 | Message::HandleListZones(zones) => { 189 | let new_info = 190 | highlight::zones_found(&self.lists_highlight, &map_zones(&self.tree, zones)); 191 | let highlight_update = zone_update(&self.lists_highlight, &new_info); 192 | highlight_update.update(&mut self.tree, &self.lists_highlight, &new_info); 193 | self.lists_highlight = new_info; 194 | 195 | if highlight_update == ZoneUpdate::Replace { 196 | if let Some(d_loc) = &self.lists_highlight.dragging { 197 | if let Some(h_loc) = &self.lists_highlight.hovered { 198 | return move_list_to_zone(&mut self.tree, &d_loc.0, &h_loc); 199 | } 200 | } 201 | } 202 | } 203 | Message::DropList(l_loc, _, _) => { 204 | self.tree.list_mut(&l_loc).set_highlight(false); 205 | if let Some(s_loc) = &self.lists_highlight.hovered { 206 | self.tree.slot_mut(s_loc.slot()).set_highlight(false); 207 | } 208 | self.todos_highlight = highlight::dropped(); 209 | } 210 | Message::UpdateTodoWriter(l_loc, new_str) => { 211 | self.stop_editing(); 212 | self.tree.list_mut(&l_loc).todo_adder.text = new_str; 213 | } 214 | Message::WriteTodo(l_loc) => { 215 | let (text, id) = { 216 | let adder = &mut self.tree.list_mut(&l_loc).todo_adder; 217 | let text = adder.text.clone(); 218 | adder.text.clear(); 219 | (text, adder.id()) 220 | }; 221 | if text.is_empty() { 222 | return Task::none(); 223 | } 224 | let todo = Todo::new(&text); 225 | self.tree.list_mut(&l_loc).push(todo); 226 | return text_input::focus(id); 227 | } 228 | Message::TodoDropCanceled => { 229 | if let Some(d_loc) = &self.todos_highlight.dragging { 230 | if let Some(todo) = self.tree.todo_mut(&d_loc.0) { 231 | todo.set_highlight(false); 232 | highlight::set_hovered(&mut self.tree, &self.todos_highlight, false); 233 | } 234 | } 235 | self.todos_highlight = highlight::dropped(); 236 | } 237 | Message::ListDropCanceled => { 238 | if let Some(d_loc) = &self.lists_highlight.dragging { 239 | self.tree.list_mut(&d_loc.0).set_highlight(false); 240 | self.tree.slot_mut(d_loc.0.slot()).set_highlight(false); 241 | } 242 | self.lists_highlight = highlight::dropped(); 243 | } 244 | } 245 | Task::none() 246 | } 247 | } 248 | 249 | impl TodoBoard { 250 | fn stop_editing(&mut self) { 251 | if let Some(loc) = self.editing { 252 | if let Some(todo) = self.tree.todo_mut(&loc) { 253 | todo.editing = false; 254 | self.editing = None; 255 | } 256 | } 257 | } 258 | } 259 | 260 | fn map_zones(tree: &TreeData, zones: Vec<(Id, Rectangle)>) -> Vec<(TreeLocation, Rectangle)> { 261 | zones 262 | .into_iter() 263 | .filter_map(|(id, rect)| { 264 | if let Some(loc) = tree.find(&id) { 265 | Some((loc, rect)) 266 | } else { 267 | None 268 | } 269 | }) 270 | .collect() 271 | } 272 | 273 | fn todo_dropped_on_list(tree: &mut TreeData, d_loc: &TreeLocation, h_loc: &TreeLocation) { 274 | if let Some(todo) = tree.todo_mut(d_loc) { 275 | todo.set_highlight(false); 276 | let todo = { 277 | let list = tree.list_mut(h_loc); 278 | list.set_highlight(false); 279 | if d_loc.slot() == h_loc.slot() { 280 | return; 281 | } 282 | if let Some(todo) = tree.list_mut(d_loc).remove(d_loc) { 283 | todo 284 | } else { 285 | return; 286 | } 287 | }; 288 | tree.list_mut(h_loc).push(todo); 289 | } 290 | } 291 | 292 | fn todo_dropped_on_todo(tree: &mut TreeData, d_loc: &TreeLocation, h_loc: &TreeLocation) { 293 | if let Some(d_todo) = tree.todo_mut(d_loc) { 294 | d_todo.set_highlight(false); 295 | let h_todo = tree.todo_mut(h_loc); 296 | if let Some(todo) = h_todo { 297 | todo.set_highlight(false); 298 | } else { 299 | return; 300 | } 301 | } 302 | 303 | if d_loc.slot() != h_loc.slot() { 304 | if let TreeElement::Todo(i) = h_loc.element() { 305 | let todo = tree.list_mut(d_loc).remove(d_loc).unwrap(); 306 | tree.list_mut(h_loc).insert(todo, *i); 307 | } 308 | } else { 309 | tree.list_mut(d_loc).move_todo(d_loc, h_loc); 310 | } 311 | } 312 | 313 | fn move_list_to_zone( 314 | tree: &mut TreeData, 315 | d_loc: &TreeLocation, 316 | h_loc: &TreeLocation, 317 | ) -> Task { 318 | let l1 = tree.list_mut(d_loc).id(); 319 | let l2 = tree.list_mut(h_loc).id(); 320 | tree.swap_lists(d_loc, h_loc); 321 | return swap_modify_states(l1, l2, |_old: &DroppableState, new: &DroppableState| { 322 | new.clone() 323 | }); 324 | } 325 | -------------------------------------------------------------------------------- /examples/todo/src/operation.rs: -------------------------------------------------------------------------------- 1 | use iced::{ 2 | advanced::widget::{operate, Id, Operation}, 3 | Task, 4 | }; 5 | 6 | pub fn swap_modify_states(t1: Id, t2: Id, modify: Modify) -> Task 7 | where 8 | Message: Send + 'static, 9 | State: Clone + Send + 'static, 10 | Modify: Fn(&State, &State) -> State + Clone + Send + 'static, 11 | { 12 | operate(swap_modify_states_operation(t1, t2, modify)) 13 | } 14 | 15 | pub fn swap_modify_states_operation( 16 | t1: Id, 17 | t2: Id, 18 | modify: Modify, 19 | ) -> impl Operation 20 | where 21 | State: Clone + Send + 'static, 22 | Modify: Fn(&State, &State) -> State + Clone + Send + 'static, 23 | { 24 | struct FindTargets 25 | where 26 | State: Clone + Send + 'static, 27 | Modify: Fn(&State, &State) -> State + Clone + Send + 'static, 28 | { 29 | t1: Id, 30 | t2: Id, 31 | modify: Modify, 32 | t1_state: Option, 33 | t2_state: Option, 34 | } 35 | 36 | impl Operation for FindTargets 37 | where 38 | State: Clone + Send + 'static, 39 | Modify: Fn(&State, &State) -> State + Clone + Send + 'static, 40 | { 41 | fn container( 42 | &mut self, 43 | _id: Option<&Id>, 44 | _bounds: iced::Rectangle, 45 | operate_on_children: &mut dyn FnMut(&mut dyn Operation), 46 | ) { 47 | if self.t1_state.is_some() && self.t2_state.is_some() { 48 | return; 49 | } 50 | operate_on_children(self); 51 | } 52 | 53 | fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { 54 | if self.t1_state.is_some() && self.t2_state.is_some() { 55 | return; 56 | } 57 | if let Some(state) = state.downcast_mut::() { 58 | match id { 59 | Some(id) => { 60 | if id == &self.t1 { 61 | self.t1_state = Some(state.clone()); 62 | } else if id == &self.t2 { 63 | self.t2_state = Some(state.clone()); 64 | } 65 | } 66 | None => (), 67 | } 68 | } 69 | } 70 | 71 | fn finish(&self) -> iced::advanced::widget::operation::Outcome { 72 | if self.t1_state.is_none() || self.t2_state.is_none() { 73 | iced::advanced::widget::operation::Outcome::None 74 | } else { 75 | iced::advanced::widget::operation::Outcome::Chain(Box::new(SwapModify { 76 | t1: self.t1.clone(), 77 | t2: self.t2.clone(), 78 | modify: self.modify.clone(), 79 | t1_state: self.t1_state.clone().unwrap(), 80 | t2_state: self.t2_state.clone().unwrap(), 81 | swapped_t1: false, 82 | swapped_t2: false, 83 | })) 84 | } 85 | } 86 | } 87 | 88 | struct SwapModify 89 | where 90 | State: Clone + 'static, 91 | Modify: Fn(&State, &State) -> State + Clone + 'static, 92 | { 93 | t1: Id, 94 | t2: Id, 95 | modify: Modify, 96 | t1_state: State, 97 | t2_state: State, 98 | swapped_t1: bool, 99 | swapped_t2: bool, 100 | } 101 | 102 | impl Operation for SwapModify 103 | where 104 | State: Clone + Send + 'static, 105 | Modify: Fn(&State, &State) -> State + Clone + Send + 'static, 106 | { 107 | fn container( 108 | &mut self, 109 | _id: Option<&Id>, 110 | _bounds: iced::Rectangle, 111 | operate_on_children: &mut dyn FnMut(&mut dyn Operation), 112 | ) { 113 | if self.swapped_t1 && self.swapped_t2 { 114 | return; 115 | } 116 | operate_on_children(self); 117 | } 118 | 119 | fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { 120 | if self.swapped_t1 && self.swapped_t2 { 121 | return; 122 | } 123 | if let Some(state) = state.downcast_mut::() { 124 | match id { 125 | Some(id) => { 126 | if id == &self.t1 { 127 | *state = (self.modify)(state, &self.t2_state); 128 | self.swapped_t1 = true; 129 | } else if id == &self.t2 { 130 | *state = (self.modify)(state, &self.t1_state); 131 | self.swapped_t2 = true; 132 | } 133 | } 134 | None => (), 135 | } 136 | } 137 | } 138 | 139 | fn finish(&self) -> iced::advanced::widget::operation::Outcome { 140 | iced::advanced::widget::operation::Outcome::None 141 | } 142 | } 143 | 144 | FindTargets { 145 | t1, 146 | t2, 147 | modify, 148 | t1_state: None, 149 | t2_state: None, 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /examples/todo/src/theme.rs: -------------------------------------------------------------------------------- 1 | pub mod button; 2 | pub mod container; 3 | pub mod text; 4 | pub mod text_input; 5 | -------------------------------------------------------------------------------- /examples/todo/src/theme/button.rs: -------------------------------------------------------------------------------- 1 | use iced::widget::button::{Status, Style}; 2 | use iced::{color, Border, Theme}; 3 | 4 | pub fn adder(_theme: &Theme, _status: Status) -> Style { 5 | Style { 6 | background: Some(color!(96, 91, 86).into()), 7 | text_color: color!(242, 251, 224), 8 | border: Border { 9 | radius: 10.0.into(), 10 | ..Default::default() 11 | }, 12 | ..Default::default() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/todo/src/theme/container.rs: -------------------------------------------------------------------------------- 1 | use iced::widget::container::Style; 2 | use iced::{color, Border, Theme}; 3 | 4 | pub fn active_slot(_theme: &Theme) -> Style { 5 | Style { 6 | background: Some(color!(202, 233, 255).into()), 7 | border: Border { 8 | color: color!(202, 233, 255), 9 | radius: 10.0.into(), 10 | width: 5.0, 11 | }, 12 | ..Default::default() 13 | } 14 | } 15 | 16 | pub fn title(_theme: &Theme) -> Style { 17 | Style { 18 | background: Some(color!(131, 122, 117).into()), 19 | ..Default::default() 20 | } 21 | } 22 | 23 | pub fn list(_theme: &Theme) -> Style { 24 | Style { 25 | background: Some(color!(172, 193, 138).into()), 26 | border: Border { 27 | radius: 10.0.into(), 28 | ..Default::default() 29 | }, 30 | ..Default::default() 31 | } 32 | } 33 | 34 | pub fn active_list(_theme: &Theme) -> Style { 35 | Style { 36 | background: Some(color!(172, 193, 138).into()), 37 | border: Border { 38 | color: color!(202, 233, 255), 39 | radius: 10.0.into(), 40 | width: 5.0, 41 | }, 42 | ..Default::default() 43 | } 44 | } 45 | 46 | pub fn todo(_theme: &Theme) -> Style { 47 | Style { 48 | background: Some(color!(218, 254, 183).into()), 49 | border: Border { 50 | radius: 10.0.into(), 51 | ..Default::default() 52 | }, 53 | ..Default::default() 54 | } 55 | } 56 | 57 | pub fn active_todo(_theme: &Theme) -> Style { 58 | Style { 59 | background: Some(color!(218, 254, 183).into()), 60 | border: Border { 61 | color: color!(202, 233, 255), 62 | radius: 10.0.into(), 63 | width: 5.0, 64 | }, 65 | ..Default::default() 66 | } 67 | } 68 | 69 | pub fn background(_theme: &Theme) -> Style { 70 | Style { 71 | background: Some(color!(96, 91, 86).into()), 72 | ..Default::default() 73 | } 74 | } 75 | 76 | pub fn adder_tooltip(_theme: &Theme) -> Style { 77 | Style { 78 | text_color: Some(color!(242, 251, 224)), 79 | background: Some(color!(131, 122, 117).into()), 80 | border: Border { 81 | radius: 5.0.into(), 82 | ..Default::default() 83 | }, 84 | ..Default::default() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /examples/todo/src/theme/text.rs: -------------------------------------------------------------------------------- 1 | use iced::widget::text::Style; 2 | use iced::{color, Theme}; 3 | 4 | pub fn title(_theme: &Theme) -> Style { 5 | Style { 6 | color: Some(color!(242, 251, 224)), 7 | ..Default::default() 8 | } 9 | } 10 | 11 | pub fn list_name(_theme: &Theme) -> Style { 12 | Style { 13 | color: Some(color!(96, 91, 86)), 14 | ..Default::default() 15 | } 16 | } 17 | 18 | pub fn todo(_theme: &Theme) -> Style { 19 | Style { 20 | color: Some(color!(131, 122, 117)), 21 | ..Default::default() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/todo/src/theme/text_input.rs: -------------------------------------------------------------------------------- 1 | use iced::widget::text_input::{Status, Style}; 2 | use iced::{color, Border, Color, Theme}; 3 | 4 | pub fn element_adder(_theme: &Theme, _status: Status) -> Style { 5 | Style { 6 | background: color!(218, 254, 183).into(), 7 | border: Border { 8 | color: color!(96, 91, 86), 9 | width: 1.0, 10 | radius: 10.0.into(), 11 | }, 12 | icon: Color::BLACK, 13 | placeholder: color!(131, 122, 117, 0.7), 14 | value: color!(131, 122, 117), 15 | selection: Color::WHITE, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/todo/src/tree.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::AtomicUsize; 2 | 3 | use iced::advanced::widget::Id; 4 | 5 | use iced::widget::tooltip; 6 | use iced::{ 7 | alignment, 8 | widget::{button, column, container, horizontal_space, row, text, text_input}, 9 | Center, Element, Length, Size, 10 | }; 11 | use iced_drop::droppable; 12 | 13 | use crate::{highlight::Highlightable, theme, Message}; 14 | 15 | pub const NULL_TODO_LOC: TreeLocation = TreeLocation { 16 | slot: 0, 17 | element: TreeElement::Slot, 18 | }; 19 | 20 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 21 | pub struct TreeLocation { 22 | slot: usize, 23 | element: TreeElement, 24 | } 25 | 26 | impl TreeLocation { 27 | fn new(slot: usize, element: TreeElement) -> Self { 28 | Self { slot, element } 29 | } 30 | 31 | pub fn element(&self) -> &TreeElement { 32 | &self.element 33 | } 34 | 35 | pub fn slot(&self) -> usize { 36 | self.slot 37 | } 38 | } 39 | 40 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 41 | pub enum TreeElement { 42 | Slot, 43 | List, 44 | Todo(usize), 45 | } 46 | 47 | pub struct ElementAdder { 48 | pub text: String, 49 | id: iced::widget::text_input::Id, 50 | } 51 | 52 | /// Contains items organized by slots, and lists 53 | pub struct TreeData { 54 | slots: Vec, 55 | } 56 | 57 | impl TreeData { 58 | pub fn new(slots: Vec) -> Self { 59 | Self { slots } 60 | } 61 | /// Convert the tree into an element that iced can render 62 | pub fn view(&self) -> Element { 63 | let children = self.slots.iter().enumerate().map(|(i, slot)| slot.view(i)); 64 | row(children) 65 | .spacing(10.0) 66 | .padding(20.0) 67 | .width(Length::Fill) 68 | .height(Length::Fill) 69 | .into() 70 | } 71 | 72 | pub fn find(&self, id: &Id) -> Option { 73 | for (i, slot) in self.slots.iter().enumerate() { 74 | if slot.id == *id { 75 | return Some(TreeLocation::new(i, TreeElement::Slot)); 76 | } 77 | if slot.list.id == *id { 78 | return Some(TreeLocation::new(i, TreeElement::List)); 79 | } 80 | for (j, list) in slot.list.todos.iter().enumerate() { 81 | if list.id == *id { 82 | return Some(TreeLocation::new(i, TreeElement::Todo(j))); 83 | } 84 | } 85 | } 86 | None 87 | } 88 | 89 | pub fn slot_mut(&mut self, index: usize) -> &mut Slot { 90 | self.slots.get_mut(index).unwrap() 91 | } 92 | 93 | pub fn list_mut(&mut self, location: &TreeLocation) -> &mut List { 94 | let i = location.slot; 95 | match location.element { 96 | TreeElement::Slot => &mut self.slots[i].list, 97 | TreeElement::List => &mut self.slots[i].list, 98 | TreeElement::Todo(_) => &mut self.slots[i].list, 99 | } 100 | } 101 | 102 | pub fn todo(&self, location: &TreeLocation) -> Option<&Todo> { 103 | let i = location.slot; 104 | match location.element { 105 | TreeElement::Slot => None, 106 | TreeElement::List => None, 107 | TreeElement::Todo(j) => Some(&self.slots[i].list.todos[j]), 108 | } 109 | } 110 | 111 | pub fn todo_mut(&mut self, location: &TreeLocation) -> Option<&mut Todo> { 112 | let i = location.slot; 113 | match location.element { 114 | TreeElement::Slot => None, 115 | TreeElement::List => None, 116 | TreeElement::Todo(j) => Some(&mut self.slots[i].list.todos[j]), 117 | } 118 | } 119 | 120 | pub fn swap_lists(&mut self, l1: &TreeLocation, l2: &TreeLocation) { 121 | let [s1, s2] = if let Ok(slots) = self.slots.get_many_mut([l1.slot, l2.slot]) { 122 | slots 123 | } else { 124 | return; 125 | }; 126 | std::mem::swap(&mut s1.list, &mut s2.list); 127 | } 128 | 129 | /// Returns the widget Id of all the widgets wich a item can be dropped on 130 | pub fn todo_options(&self, t_loc: &TreeLocation) -> Vec { 131 | let todo_id = if let Some(todo) = self.todo(t_loc) { 132 | todo.id.clone() 133 | } else { 134 | return vec![]; 135 | }; 136 | self.slots 137 | .iter() 138 | .map(|slot| { 139 | slot.list.todos.iter().filter_map(|todo| { 140 | if todo.id != todo_id { 141 | Some(todo.id.clone()) 142 | } else { 143 | None 144 | } 145 | }) 146 | }) 147 | .flatten() 148 | .chain(self.slots.iter().map(|slot| slot.list.id.clone())) 149 | .collect() 150 | } 151 | 152 | /// Returns the widget Id of all the widgets wich a list can be dropped on 153 | pub fn list_options(&self) -> Vec { 154 | self.slots.iter().map(|slot| slot.id.clone()).collect() 155 | } 156 | } 157 | 158 | static NEXT_SLOT: AtomicUsize = AtomicUsize::new(0); 159 | 160 | /// Some slot that a list can be dragged into 161 | pub struct Slot { 162 | id: Id, 163 | list: List, 164 | c_id: iced::widget::container::Id, 165 | highlight: bool, 166 | } 167 | 168 | impl Highlightable for Slot { 169 | fn set_highlight(&mut self, highlight: bool) { 170 | self.highlight = highlight; 171 | } 172 | } 173 | 174 | impl Slot { 175 | /// Create a new slot with a list 176 | pub fn new(list: List) -> Self { 177 | let id = NEXT_SLOT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); 178 | let c_id = iced::widget::container::Id::new(format!("slot_{}", id)); 179 | Self { 180 | id: Id::from(c_id.clone()), 181 | c_id, 182 | list, 183 | highlight: false, 184 | } 185 | } 186 | 187 | /// Convert the slot into an element that iced can render 188 | fn view(&self, index: usize) -> Element { 189 | container(self.list.view(index)) 190 | .id(self.c_id.clone()) 191 | .style(if self.highlight { 192 | theme::container::active_slot 193 | } else { 194 | container::transparent 195 | }) 196 | .width(Length::Fill) 197 | .height(Length::Fill) 198 | .padding(3.5) 199 | .into() 200 | } 201 | } 202 | 203 | impl ElementAdder { 204 | pub fn new(id: usize) -> Self { 205 | Self { 206 | id: iced::widget::text_input::Id::new(format!("todo_adder_{}", id)), 207 | text: String::new(), 208 | } 209 | } 210 | 211 | pub fn id(&self) -> iced::widget::text_input::Id { 212 | self.id.clone() 213 | } 214 | } 215 | 216 | static NEXT_LIST: AtomicUsize = AtomicUsize::new(0); 217 | 218 | /// Some list that contains to-do tasks and can be dragged into a slot. 219 | /// Tasks can also be dragged into a list. 220 | pub struct List { 221 | pub todo_adder: ElementAdder, 222 | id: Id, 223 | title: String, 224 | todos: Vec, 225 | highlight: bool, 226 | } 227 | 228 | impl Highlightable for List { 229 | fn set_highlight(&mut self, highlight: bool) { 230 | self.highlight = highlight; 231 | } 232 | } 233 | 234 | impl List { 235 | /// Create a new list with a title 236 | pub fn new(title: &str, todos: Vec) -> Self { 237 | let id = NEXT_LIST.fetch_add(1, std::sync::atomic::Ordering::Relaxed); 238 | Self { 239 | id: Id::new(format!("list_{}", id)), 240 | title: title.to_string(), 241 | todo_adder: ElementAdder::new(id), 242 | todos, 243 | highlight: false, 244 | } 245 | } 246 | 247 | pub fn remove(&mut self, loc: &TreeLocation) -> Option { 248 | if let TreeElement::Todo(i) = loc.element() { 249 | Some(self.todos.remove(*i)) 250 | } else { 251 | None 252 | } 253 | } 254 | 255 | pub fn push(&mut self, todo: Todo) { 256 | self.todos.push(todo); 257 | } 258 | 259 | pub fn insert(&mut self, todo: Todo, index: usize) { 260 | self.todos.insert(index, todo); 261 | } 262 | 263 | pub fn move_todo(&mut self, from: &TreeLocation, to: &TreeLocation) { 264 | if let (TreeElement::Todo(i), TreeElement::Todo(j)) = (from.element(), to.element()) { 265 | let insert_index = if i < j { j - 1 } else { *j }; 266 | let todo = self.todos.remove(*i); 267 | self.todos.insert(insert_index, todo); 268 | } 269 | } 270 | 271 | pub fn id(&self) -> Id { 272 | self.id.clone() 273 | } 274 | 275 | /// Convert the list into an element that iced can render 276 | fn view(&self, slot_index: usize) -> Element { 277 | let name = text(self.title.clone()) 278 | .size(20) 279 | .style(theme::text::list_name); 280 | let location = TreeLocation::new(slot_index, TreeElement::List); 281 | let todos = column( 282 | self.todos 283 | .iter() 284 | .enumerate() 285 | .map(|(i, todo)| todo.view(TreeLocation::new(slot_index, TreeElement::Todo(i)))), 286 | ) 287 | .spacing(10.0) 288 | .width(Length::Fill) 289 | .height(Length::Shrink) 290 | .push(self.adder(location)); 291 | let content = container(column![name, todos].spacing(20.0)) 292 | .width(Length::Fill) 293 | .height(Length::Shrink) 294 | .padding(10.0) 295 | .style(if self.highlight { 296 | theme::container::active_list 297 | } else { 298 | theme::container::list 299 | }); 300 | droppable(content) 301 | .id(self.id.clone()) 302 | .on_click(Message::StopEditingTodo) 303 | .on_drop(move |p, r| Message::DropList(location, p, r)) 304 | .on_drag(move |p, r| Message::DragList(location, p, r)) 305 | .on_cancel(Message::ListDropCanceled) 306 | .drag_hide(true) 307 | .into() 308 | } 309 | 310 | fn adder(&self, location: TreeLocation) -> Element { 311 | let input = text_input("Add task...", self.todo_adder.text.as_str()) 312 | .id(self.todo_adder.id.clone()) 313 | .on_input(move |new_str| Message::UpdateTodoWriter(location, new_str)) 314 | .on_submit(Message::WriteTodo(location)) 315 | .style(theme::text_input::element_adder) 316 | .size(14.0) 317 | .width(Length::Fill); 318 | let spacing = horizontal_space().width(Length::Fixed(10.0)); 319 | let add_btn = tooltip( 320 | button(text("+").align_y(Center).align_x(Center)) 321 | .on_press(Message::WriteTodo(location)) 322 | .style(theme::button::adder) 323 | .width(Length::Fixed(30.0)), 324 | "Add task", 325 | tooltip::Position::FollowCursor, 326 | ) 327 | .style(theme::container::adder_tooltip); 328 | row![input, spacing, add_btn].width(Length::Fill).into() 329 | } 330 | } 331 | 332 | static NEXT_TODO: AtomicUsize = AtomicUsize::new(0); 333 | 334 | /// Some to-do task that can be dragged into a list 335 | #[derive(Debug)] 336 | pub struct Todo { 337 | pub content: String, 338 | pub editing: bool, 339 | id: Id, 340 | t_id: iced::widget::text_input::Id, 341 | highlight: bool, 342 | } 343 | 344 | impl Highlightable for Todo { 345 | fn set_highlight(&mut self, highlight: bool) { 346 | self.highlight = highlight; 347 | } 348 | } 349 | 350 | impl Todo { 351 | /// Create a new to-do task with some content 352 | pub fn new(content: &str) -> Self { 353 | let id = NEXT_TODO.fetch_add(1, std::sync::atomic::Ordering::Relaxed); 354 | Self { 355 | id: Id::new(format!("todo_{}", id)), 356 | t_id: iced::widget::text_input::Id::new(format!("todo_input_{}", id)), 357 | content: content.to_string(), 358 | highlight: false, 359 | editing: false, 360 | } 361 | } 362 | 363 | /// Convert the task into an element that iced can render 364 | fn view(&self, location: TreeLocation) -> Element { 365 | let txt = text(&self.content) 366 | .size(15) 367 | .style(theme::text::todo) 368 | .align_y(Center); 369 | let content = container(txt) 370 | .align_y(alignment::Vertical::Center) 371 | .padding(10.0) 372 | .width(Length::Fill) 373 | .height(Length::Shrink) 374 | .style(if self.highlight { 375 | theme::container::active_todo 376 | } else { 377 | theme::container::todo 378 | }); 379 | let element = if !self.editing { 380 | droppable(content) 381 | .id(self.id.clone()) 382 | .on_click(Message::EditTodo(location, self.t_id.clone())) 383 | .on_drop(move |p, r| Message::DropTodo(location, p, r)) 384 | .on_drag(move |p, r| Message::DragTodo(location, p, r)) 385 | .on_cancel(Message::TodoDropCanceled) 386 | .drag_hide(true) 387 | .drag_size(Size::ZERO) 388 | .into() 389 | } else { 390 | text_input("", &self.content) 391 | .id(self.t_id.clone()) 392 | .padding(10.0) 393 | .size(15) 394 | .on_input(move |new_str| Message::UpdateTodo(location, new_str)) 395 | .on_submit(Message::StopEditingTodo) 396 | .into() 397 | }; 398 | element 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod widget; 2 | 3 | use iced::{ 4 | advanced::widget::{operate, Id}, 5 | advanced::{graphics::futures::MaybeSend, renderer}, 6 | task::Task, 7 | Element, Point, Rectangle, 8 | }; 9 | 10 | use widget::droppable::*; 11 | use widget::operation::drop; 12 | 13 | pub fn droppable<'a, Message, Theme, Renderer>( 14 | content: impl Into>, 15 | ) -> Droppable<'a, Message, Theme, Renderer> 16 | where 17 | Message: Clone, 18 | Renderer: renderer::Renderer, 19 | { 20 | Droppable::new(content) 21 | } 22 | 23 | pub fn zones_on_point( 24 | msg: MF, 25 | point: Point, 26 | options: Option>, 27 | depth: Option, 28 | ) -> Task 29 | where 30 | T: Send + 'static, 31 | MF: Fn(Vec<(Id, Rectangle)>) -> T + MaybeSend + Sync + Clone + 'static, 32 | { 33 | operate(drop::find_zones( 34 | move |bounds| bounds.contains(point), 35 | options, 36 | depth, 37 | )) 38 | .map(move |id| msg(id)) 39 | } 40 | 41 | pub fn find_zones( 42 | msg: MF, 43 | filter: F, 44 | options: Option>, 45 | depth: Option, 46 | ) -> Task 47 | where 48 | Message: Send + 'static, 49 | MF: Fn(Vec<(Id, Rectangle)>) -> Message + MaybeSend + Sync + Clone + 'static, 50 | F: Fn(&Rectangle) -> bool + Send + 'static, 51 | { 52 | operate(drop::find_zones(filter, options, depth)).map(move |id| msg(id)) 53 | } 54 | -------------------------------------------------------------------------------- /src/widget.rs: -------------------------------------------------------------------------------- 1 | pub mod droppable; 2 | pub mod operation; 3 | -------------------------------------------------------------------------------- /src/widget/droppable.rs: -------------------------------------------------------------------------------- 1 | //! Encapsulates a widget that can be dragged and dropped. 2 | use std::fmt::Debug; 3 | use std::vec; 4 | 5 | use iced::advanced::widget::{Operation, Tree, Widget}; 6 | use iced::advanced::{self, layout, mouse, overlay, renderer, Layout}; 7 | use iced::event::Status; 8 | use iced::{Element, Point, Rectangle, Size, Vector}; 9 | 10 | /// An element that can be dragged and dropped on a [`DropZone`] 11 | pub struct Droppable<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> 12 | where 13 | Message: Clone, 14 | Renderer: renderer::Renderer, 15 | { 16 | content: Element<'a, Message, Theme, Renderer>, 17 | id: Option, 18 | on_click: Option, 19 | on_drop: Option Message + 'a>>, 20 | on_drag: Option Message + 'a>>, 21 | on_cancel: Option, 22 | drag_mode: Option<(bool, bool)>, 23 | drag_overlay: bool, 24 | drag_hide: bool, 25 | drag_center: bool, 26 | drag_size: Option, 27 | reset_delay: usize, 28 | } 29 | 30 | impl<'a, Message, Theme, Renderer> Droppable<'a, Message, Theme, Renderer> 31 | where 32 | Message: Clone, 33 | Renderer: renderer::Renderer, 34 | { 35 | /// Creates a new [`Droppable`]. 36 | pub fn new(content: impl Into>) -> Self { 37 | Self { 38 | content: content.into(), 39 | id: None, 40 | on_click: None, 41 | on_drop: None, 42 | on_drag: None, 43 | on_cancel: None, 44 | drag_mode: Some((true, true)), 45 | drag_overlay: true, 46 | drag_hide: false, 47 | drag_center: false, 48 | drag_size: None, 49 | reset_delay: 0, 50 | } 51 | } 52 | 53 | /// Sets the unique identifier of the [`Droppable`]. 54 | pub fn id(mut self, id: iced::advanced::widget::Id) -> Self { 55 | self.id = Some(id); 56 | self 57 | } 58 | 59 | /// Sets the message that will be produced when the [`Droppable`] is clicked. 60 | pub fn on_click(mut self, message: Message) -> Self { 61 | self.on_click = Some(message); 62 | self 63 | } 64 | 65 | /// Sets the message that will be produced when the [`Droppable`] is dropped on a [`DropZone`]. 66 | /// 67 | /// Unless this is set, the [`Droppable`] will be disabled. 68 | pub fn on_drop(mut self, message: F) -> Self 69 | where 70 | F: Fn(Point, Rectangle) -> Message + 'a, 71 | { 72 | self.on_drop = Some(Box::new(message)); 73 | self 74 | } 75 | 76 | /// Sets the message that will be produced when the [`Droppable`] is dragged. 77 | pub fn on_drag(mut self, message: F) -> Self 78 | where 79 | F: Fn(Point, Rectangle) -> Message + 'a, 80 | { 81 | self.on_drag = Some(Box::new(message)); 82 | self 83 | } 84 | 85 | /// Sets the message that will be produced when the user right clicks while dragging the [`Droppable`]. 86 | pub fn on_cancel(mut self, message: Message) -> Self { 87 | self.on_cancel = Some(message); 88 | self 89 | } 90 | 91 | /// Sets whether the [`Droppable`] should be drawn under the cursor while dragging. 92 | pub fn drag_overlay(mut self, drag_overlay: bool) -> Self { 93 | self.drag_overlay = drag_overlay; 94 | self 95 | } 96 | 97 | /// Sets whether the [`Droppable`] should be hidden while dragging. 98 | pub fn drag_hide(mut self, drag_hide: bool) -> Self { 99 | self.drag_hide = drag_hide; 100 | self 101 | } 102 | 103 | /// Sets whether the [`Droppable`] should be centered on the cursor while dragging. 104 | pub fn drag_center(mut self, drag_center: bool) -> Self { 105 | self.drag_center = drag_center; 106 | self 107 | } 108 | 109 | // Sets whether the [`Droppable`] can be dragged along individual axes. 110 | pub fn drag_mode(mut self, drag_x: bool, drag_y: bool) -> Self { 111 | self.drag_mode = Some((drag_x, drag_y)); 112 | self 113 | } 114 | 115 | /// Sets whether the [`Droppable`] should be be resized to a given size while dragging. 116 | pub fn drag_size(mut self, hide_size: Size) -> Self { 117 | self.drag_size = Some(hide_size); 118 | self 119 | } 120 | 121 | /// Sets the number of frames/layout calls to wait before resetting the size of the [`Droppable`] after dropping. 122 | /// 123 | /// This is useful for cases where the [`Droppable`] is being moved to a new location after some widget operation. 124 | /// In this case, the [`Droppable`] will mainting the 'drag_size' for the given number of frames before resetting to its original size. 125 | /// This prevents the [`Droppable`] from 'jumping' back to its original size before the new location is rendered which 126 | /// prevents flickering. 127 | /// 128 | /// Warning: this should only be set if there's is some noticeble flickering when the [`Droppable`] is dropped. That is, if the 129 | /// [`Droppable`] returns to its original size before it's moved to it's new location. 130 | pub fn reset_delay(mut self, reset_delay: usize) -> Self { 131 | self.reset_delay = reset_delay; 132 | self 133 | } 134 | } 135 | 136 | impl<'a, Message, Theme, Renderer> Widget 137 | for Droppable<'a, Message, Theme, Renderer> 138 | where 139 | Message: Clone, 140 | Renderer: renderer::Renderer, 141 | { 142 | fn state(&self) -> iced::advanced::widget::tree::State { 143 | advanced::widget::tree::State::new(State::default()) 144 | } 145 | 146 | fn tag(&self) -> iced::advanced::widget::tree::Tag { 147 | advanced::widget::tree::Tag::of::() 148 | } 149 | 150 | fn children(&self) -> Vec { 151 | vec![advanced::widget::Tree::new(&self.content)] 152 | } 153 | 154 | fn diff(&self, tree: &mut iced::advanced::widget::Tree) { 155 | tree.diff_children(std::slice::from_ref(&self.content)) 156 | } 157 | 158 | fn size(&self) -> iced::Size { 159 | self.content.as_widget().size() 160 | } 161 | 162 | fn on_event( 163 | &mut self, 164 | tree: &mut iced::advanced::widget::Tree, 165 | event: iced::Event, 166 | layout: iced::advanced::Layout<'_>, 167 | cursor: iced::advanced::mouse::Cursor, 168 | _renderer: &Renderer, 169 | _clipboard: &mut dyn iced::advanced::Clipboard, 170 | shell: &mut iced::advanced::Shell<'_, Message>, 171 | _viewport: &iced::Rectangle, 172 | ) -> iced::advanced::graphics::core::event::Status { 173 | // handle the on event of the content first, in case that the droppable is nested 174 | let status = self.content.as_widget_mut().on_event( 175 | &mut tree.children[0], 176 | event.clone(), 177 | layout, 178 | cursor, 179 | _renderer, 180 | _clipboard, 181 | shell, 182 | _viewport, 183 | ); 184 | // this should really only be captured if the droppable is nested or it contains some other 185 | // widget that captures the event 186 | if status == Status::Captured { 187 | return status; 188 | }; 189 | 190 | if let Some(on_drop) = self.on_drop.as_deref() { 191 | let state = tree.state.downcast_mut::(); 192 | if let iced::Event::Mouse(mouse) = event { 193 | match mouse { 194 | mouse::Event::ButtonPressed(btn) => { 195 | if btn == mouse::Button::Left && cursor.is_over(layout.bounds()) { 196 | // select the droppable and store the position of the widget before dragging 197 | state.action = Action::Select(cursor.position().unwrap()); 198 | let bounds = layout.bounds(); 199 | state.widget_pos = bounds.position(); 200 | state.overlay_bounds.width = bounds.width; 201 | state.overlay_bounds.height = bounds.height; 202 | 203 | if let Some(on_click) = self.on_click.clone() { 204 | shell.publish(on_click); 205 | } 206 | return Status::Captured; 207 | } else if btn == mouse::Button::Right { 208 | if let Action::Drag(_, _) = state.action { 209 | shell.invalidate_layout(); 210 | state.action = Action::None; 211 | if let Some(on_cancel) = self.on_cancel.clone() { 212 | shell.publish(on_cancel); 213 | } 214 | } 215 | } 216 | } 217 | mouse::Event::CursorMoved { mut position } => match state.action { 218 | Action::Select(start) | Action::Drag(start, _) => { 219 | // calculate the new position of the widget after dragging 220 | 221 | if let Some((drag_x, drag_y)) = self.drag_mode { 222 | position = Point { 223 | x: if drag_x { position.x } else { start.x }, 224 | y: if drag_y { position.y } else { start.y }, 225 | }; 226 | } 227 | 228 | state.action = Action::Drag(start, position); 229 | // update the position of the overlay since the cursor was moved 230 | if self.drag_center { 231 | state.overlay_bounds.x = 232 | position.x - state.overlay_bounds.width / 2.0; 233 | state.overlay_bounds.y = 234 | position.y - state.overlay_bounds.height / 2.0; 235 | } else { 236 | state.overlay_bounds.x = state.widget_pos.x + position.x - start.x; 237 | state.overlay_bounds.y = state.widget_pos.y + position.y - start.y; 238 | } 239 | // send on drag msg 240 | if let Some(on_drag) = self.on_drag.as_deref() { 241 | let message = (on_drag)(position, state.overlay_bounds); 242 | shell.publish(message); 243 | } 244 | } 245 | _ => (), 246 | }, 247 | mouse::Event::ButtonReleased(btn) => { 248 | if btn == mouse::Button::Left { 249 | match state.action { 250 | Action::Select(_) => { 251 | state.action = Action::None; 252 | } 253 | Action::Drag(_, current) => { 254 | // send on drop msg 255 | let message = (on_drop)(current, state.overlay_bounds); 256 | shell.publish(message); 257 | 258 | if self.reset_delay == 0 { 259 | state.action = Action::None; 260 | } else { 261 | state.action = Action::Wait(self.reset_delay); 262 | } 263 | } 264 | _ => (), 265 | } 266 | } 267 | } 268 | _ => {} 269 | } 270 | } 271 | } 272 | Status::Ignored 273 | } 274 | 275 | fn layout( 276 | &self, 277 | tree: &mut iced::advanced::widget::Tree, 278 | renderer: &Renderer, 279 | limits: &iced::advanced::layout::Limits, 280 | ) -> iced::advanced::layout::Node { 281 | let state: &mut State = tree.state.downcast_mut::(); 282 | let content_node = self 283 | .content 284 | .as_widget() 285 | .layout(&mut tree.children[0], renderer, limits); 286 | 287 | // Adjust the size of the original widget if it's being dragged or we're wating to reset the size 288 | if let Some(new_size) = self.drag_size { 289 | match state.action { 290 | Action::Drag(_, _) => { 291 | return iced::advanced::layout::Node::with_children( 292 | new_size, 293 | content_node.children().to_vec(), 294 | ); 295 | } 296 | Action::Wait(reveal_index) => { 297 | if reveal_index <= 1 { 298 | state.action = Action::None; 299 | } else { 300 | state.action = Action::Wait(reveal_index - 1); 301 | } 302 | 303 | return iced::advanced::layout::Node::with_children( 304 | new_size, 305 | content_node.children().to_vec(), 306 | ); 307 | } 308 | _ => (), 309 | } 310 | } 311 | 312 | content_node 313 | } 314 | 315 | fn operate( 316 | &self, 317 | tree: &mut Tree, 318 | layout: Layout<'_>, 319 | renderer: &Renderer, 320 | operation: &mut dyn Operation, 321 | ) { 322 | let state = tree.state.downcast_mut::(); 323 | operation.custom(state, self.id.as_ref()); 324 | operation.container(self.id.as_ref(), layout.bounds(), &mut |operation| { 325 | self.content 326 | .as_widget() 327 | .operate(&mut tree.children[0], layout, renderer, operation); 328 | }); 329 | } 330 | 331 | fn draw( 332 | &self, 333 | tree: &iced::advanced::widget::Tree, 334 | renderer: &mut Renderer, 335 | theme: &Theme, 336 | style: &renderer::Style, 337 | layout: iced::advanced::Layout<'_>, 338 | cursor: iced::advanced::mouse::Cursor, 339 | viewport: &iced::Rectangle, 340 | ) { 341 | let state: &State = tree.state.downcast_ref::(); 342 | if let Action::Drag(_, _) = state.action { 343 | if self.drag_hide { 344 | return; 345 | } 346 | } 347 | 348 | self.content.as_widget().draw( 349 | &tree.children[0], 350 | renderer, 351 | theme, 352 | style, 353 | layout, 354 | cursor, 355 | &viewport, 356 | ); 357 | } 358 | 359 | fn overlay<'b>( 360 | &'b mut self, 361 | tree: &'b mut Tree, 362 | layout: Layout<'_>, 363 | renderer: &Renderer, 364 | _translation: Vector, 365 | ) -> Option> { 366 | let state: &mut State = tree.state.downcast_mut::(); 367 | let mut children = tree.children.iter_mut(); 368 | if self.drag_overlay { 369 | if let Action::Drag(_, _) = state.action { 370 | return Some(overlay::Element::new(Box::new(Overlay { 371 | content: &self.content, 372 | tree: children.next().unwrap(), 373 | overlay_bounds: state.overlay_bounds, 374 | }))); 375 | } 376 | } 377 | self.content.as_widget_mut().overlay( 378 | children.next().unwrap(), 379 | layout, 380 | renderer, 381 | _translation, 382 | ) 383 | } 384 | 385 | fn mouse_interaction( 386 | &self, 387 | tree: &iced::advanced::widget::Tree, 388 | layout: iced::advanced::Layout<'_>, 389 | cursor: iced::advanced::mouse::Cursor, 390 | _viewport: &iced::Rectangle, 391 | _renderer: &Renderer, 392 | ) -> iced::advanced::mouse::Interaction { 393 | let child_interact = self.content.as_widget().mouse_interaction( 394 | &tree.children[0], 395 | layout, 396 | cursor, 397 | _viewport, 398 | _renderer, 399 | ); 400 | if child_interact != mouse::Interaction::default() { 401 | return child_interact; 402 | } 403 | 404 | let state = tree.state.downcast_ref::(); 405 | 406 | if self.on_drop.is_none() { 407 | return mouse::Interaction::NotAllowed; 408 | } 409 | if let Action::Drag(_, _) = state.action { 410 | return mouse::Interaction::Grabbing; 411 | } 412 | if cursor.is_over(layout.bounds()) { 413 | return mouse::Interaction::Pointer; 414 | } 415 | mouse::Interaction::default() 416 | } 417 | } 418 | 419 | impl<'a, Message, Theme, Renderer> From> 420 | for Element<'a, Message, Theme, Renderer> 421 | where 422 | Message: 'a + Clone, 423 | Theme: 'a, 424 | Renderer: 'a + renderer::Renderer, 425 | { 426 | fn from( 427 | droppable: Droppable<'a, Message, Theme, Renderer>, 428 | ) -> Element<'a, Message, Theme, Renderer> { 429 | Element::new(droppable) 430 | } 431 | } 432 | 433 | #[derive(Default, Clone, Copy, PartialEq, Debug)] 434 | pub struct State { 435 | widget_pos: Point, 436 | overlay_bounds: Rectangle, 437 | action: Action, 438 | } 439 | 440 | #[derive(Default, Clone, Copy, PartialEq, Debug)] 441 | pub enum Action { 442 | #[default] 443 | None, 444 | /// (point clicked) 445 | Select(Point), 446 | /// (start pos, current pos) 447 | Drag(Point, Point), 448 | /// (frames to wait) 449 | Wait(usize), 450 | } 451 | 452 | struct Overlay<'a, 'b, Message, Theme, Renderer> 453 | where 454 | Renderer: renderer::Renderer, 455 | { 456 | content: &'b Element<'a, Message, Theme, Renderer>, 457 | tree: &'b mut advanced::widget::Tree, 458 | overlay_bounds: Rectangle, 459 | } 460 | 461 | impl<'a, 'b, Message, Theme, Renderer> overlay::Overlay 462 | for Overlay<'a, 'b, Message, Theme, Renderer> 463 | where 464 | Renderer: renderer::Renderer, 465 | { 466 | fn layout(&mut self, renderer: &Renderer, _bounds: Size) -> layout::Node { 467 | Widget::::layout( 468 | self.content.as_widget(), 469 | self.tree, 470 | renderer, 471 | &layout::Limits::new(Size::ZERO, self.overlay_bounds.size()), 472 | ) 473 | .move_to(self.overlay_bounds.position()) 474 | } 475 | 476 | fn draw( 477 | &self, 478 | renderer: &mut Renderer, 479 | theme: &Theme, 480 | inherited_style: &renderer::Style, 481 | layout: Layout<'_>, 482 | cursor_position: mouse::Cursor, 483 | ) { 484 | Widget::::draw( 485 | self.content.as_widget(), 486 | self.tree, 487 | renderer, 488 | theme, 489 | inherited_style, 490 | layout, 491 | cursor_position, 492 | &Rectangle::with_size(Size::INFINITY), 493 | ); 494 | } 495 | 496 | fn is_over(&self, _layout: Layout<'_>, _renderer: &Renderer, _cursor_position: Point) -> bool { 497 | false 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /src/widget/operation.rs: -------------------------------------------------------------------------------- 1 | pub mod drop; 2 | -------------------------------------------------------------------------------- /src/widget/operation/drop.rs: -------------------------------------------------------------------------------- 1 | use iced::{ 2 | advanced::widget::{ 3 | operation::{Outcome, Scrollable}, 4 | Id, Operation, 5 | }, 6 | Rectangle, Vector, 7 | }; 8 | 9 | /// Produces an [`Operation`] that will find the drop zones that pass a filter on the zone's bounds. 10 | /// For any drop zone to be considered, the Element must have some Id. 11 | /// If `options` is `None`, all drop zones will be considered. 12 | /// Depth determines how how deep into nested drop zones to go. 13 | /// If 'depth' is `None`, nested dropzones will be fully explored 14 | pub fn find_zones( 15 | filter: F, 16 | options: Option>, 17 | depth: Option, 18 | ) -> impl Operation> 19 | where 20 | F: Fn(&Rectangle) -> bool + Send + 'static, 21 | { 22 | struct FindDropZone { 23 | filter: F, 24 | options: Option>, 25 | zones: Vec<(Id, Rectangle)>, 26 | max_depth: Option, 27 | c_depth: usize, 28 | offset: Vector, 29 | } 30 | 31 | impl Operation> for FindDropZone 32 | where 33 | F: Fn(&Rectangle) -> bool + Send + 'static, 34 | { 35 | fn container( 36 | &mut self, 37 | id: Option<&Id>, 38 | bounds: iced::Rectangle, 39 | operate_on_children: &mut dyn FnMut(&mut dyn Operation>), 40 | ) { 41 | match id { 42 | Some(id) => { 43 | let is_option = match &self.options { 44 | Some(options) => options.contains(id), 45 | None => true, 46 | }; 47 | let bounds = bounds - self.offset; 48 | if is_option && (self.filter)(&bounds) { 49 | self.c_depth += 1; 50 | self.zones.push((id.clone(), bounds)); 51 | } 52 | } 53 | None => (), 54 | } 55 | let goto_next = match &self.max_depth { 56 | Some(m_depth) => self.c_depth < *m_depth, 57 | None => true, 58 | }; 59 | if goto_next { 60 | operate_on_children(self); 61 | } 62 | } 63 | 64 | fn finish(&self) -> Outcome> { 65 | Outcome::Some(self.zones.clone()) 66 | } 67 | 68 | fn scrollable( 69 | &mut self, 70 | _state: &mut dyn Scrollable, 71 | _id: Option<&Id>, 72 | bounds: Rectangle, 73 | _content_bounds: Rectangle, 74 | translation: Vector, 75 | ) { 76 | if (self.filter)(&bounds) { 77 | self.offset = self.offset + translation; 78 | } 79 | } 80 | } 81 | 82 | FindDropZone { 83 | filter, 84 | options, 85 | zones: vec![], 86 | max_depth: depth, 87 | c_depth: 0, 88 | offset: Vector { x: 0.0, y: 0.0 }, 89 | } 90 | } 91 | --------------------------------------------------------------------------------