From d4d59d94b95a50fb6c249cd7dbed66df1568dc3c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 2 Nov 2020 17:53:28 +0100 Subject: [PATCH] [demos] Add drag-and-drop demo (+ dancing strings demo) --- egui/src/containers/collapsing_header.rs | 4 +- egui/src/containers/frame.rs | 10 ++ egui/src/demos/color_test.rs | 6 +- egui/src/demos/dancing_strings.rs | 69 ++++++++++ egui/src/demos/demo_windows.rs | 63 +++++++-- egui/src/demos/drag_and_drop.rs | 161 +++++++++++++++++++++++ egui/src/demos/fractal_clock.rs | 2 +- egui/src/demos/mod.rs | 22 +++- egui/src/demos/sliders.rs | 2 +- egui/src/memory.rs | 8 ++ egui/src/painter.rs | 14 ++ egui/src/ui.rs | 40 ++++-- 12 files changed, 370 insertions(+), 31 deletions(-) create mode 100644 egui/src/demos/dancing_strings.rs create mode 100644 egui/src/demos/drag_and_drop.rs diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index b1fe66be..dedfccc5 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -66,7 +66,7 @@ impl State { let openness = self.openness(ui.ctx(), id); let animate = 0.0 < openness && openness < 1.0; if animate { - Some(ui.add_custom(|child_ui| { + Some(ui.wrap(|child_ui| { let max_height = if self.open { if let Some(full_height) = self.open_height { remap_clamp(openness, 0.0..=1.0, 0.0..=full_height) @@ -96,7 +96,7 @@ impl State { r })) } else if self.open || ui.memory().all_collpasing_are_open { - let (ret, response) = ui.add_custom(add_contents); + let (ret, response) = ui.wrap(add_contents); let full_size = response.rect.size(); self.open_height = Some(full_size.y); Some((ret, response)) diff --git a/egui/src/containers/frame.rs b/egui/src/containers/frame.rs index f62527f1..0154f066 100644 --- a/egui/src/containers/frame.rs +++ b/egui/src/containers/frame.rs @@ -31,6 +31,16 @@ impl Frame { } } + /// dark canvas to draw on + pub fn dark_canvas(style: &Style) -> Self { + Self { + margin: Vec2::new(10.0, 10.0), + corner_radius: 5.0, + fill: Srgba::black_alpha(250), + stroke: style.visuals.widgets.noninteractive.bg_stroke, + } + } + /// Suitable for a fullscreen app pub fn background(style: &Style) -> Self { Self { diff --git a/egui/src/demos/color_test.rs b/egui/src/demos/color_test.rs index b8fe423c..d7fc71ff 100644 --- a/egui/src/demos/color_test.rs +++ b/egui/src/demos/color_test.rs @@ -39,7 +39,7 @@ impl ColorTest { ui.heading("sRGB color test"); ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500"); - ui.add_custom(|ui| { + ui.wrap(|ui| { ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients let g = Gradient::one_color(Srgba::new(255, 165, 0, 255)); self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g); @@ -55,7 +55,7 @@ impl ColorTest { ui.separator(); ui.label("Test that vertex color times texture color is done in linear space:"); - ui.add_custom(|ui| { + ui.wrap(|ui| { ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients let tex_color = Rgba::new(1.0, 0.25, 0.25, 1.0); @@ -152,7 +152,7 @@ impl ColorTest { show_color(ui, right, color_size); }); - ui.add_custom(|ui| { + ui.wrap(|ui| { ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients if is_opaque { let g = Gradient::ground_truth_linear_gradient(left, right); diff --git a/egui/src/demos/dancing_strings.rs b/egui/src/demos/dancing_strings.rs new file mode 100644 index 00000000..f41a31ff --- /dev/null +++ b/egui/src/demos/dancing_strings.rs @@ -0,0 +1,69 @@ +use std::sync::Arc; + +use crate::{containers::*, demos::*, *}; + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct DancingStrings {} + +impl Default for DancingStrings { + fn default() -> Self { + Self {} + } +} + +impl Demo for DancingStrings { + fn name(&self) -> &str { + "Dancing Strings" + } + + fn show(&mut self, ctx: &Arc, open: &mut bool) { + Window::new(self.name()) + .open(open) + .default_size(vec2(512.0, 256.0)) + .scroll(false) + .show(ctx, |ui| self.ui(ui)); + } +} + +impl View for DancingStrings { + fn ui(&mut self, ui: &mut Ui) { + Frame::dark_canvas(ui.style()).show(ui, |ui| { + ui.ctx().request_repaint(); + let time = ui.input().time; + + let desired_size = ui.available().width() * vec2(1.0, 0.35); + let rect = ui.allocate_space(desired_size); + + let mut cmds = vec![]; + + for &mode in &[2, 3, 5] { + let mode = mode as f32; + let n = 120; + let speed = 1.5; + + let points: Vec = (0..=n) + .map(|i| { + let t = i as f32 / (n as f32); + let amp = (time as f32 * speed * mode).sin() / mode; + let y = amp * (t * math::TAU / 2.0 * mode).sin(); + + pos2( + lerp(rect.x_range(), t), + remap(y, -1.0..=1.0, rect.y_range()), + ) + }) + .collect(); + + let thickness = 10.0 / mode; + cmds.push(paint::PaintCmd::line( + points, + Stroke::new(thickness, Srgba::additive_luminance(196)), + )); + } + + ui.painter().extend(cmds); + }); + ui.add(__egui_github_link_file!()); + } +} diff --git a/egui/src/demos/demo_windows.rs b/egui/src/demos/demo_windows.rs index e45655cb..504d46e7 100644 --- a/egui/src/demos/demo_windows.rs +++ b/egui/src/demos/demo_windows.rs @@ -1,6 +1,10 @@ use std::sync::Arc; -use crate::{app, demos, Context, Id, Resize, ScrollArea, Ui, Window}; +use crate::{ + app, + demos::{self, Demo}, + Context, Id, Resize, ScrollArea, Ui, Window, +}; // ---------------------------------------------------------------------------- @@ -22,6 +26,39 @@ pub struct DemoEnvironment { // ---------------------------------------------------------------------------- +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +struct Demos { + /// open, view + #[serde(skip)] // TODO + demos: Vec<(bool, Box)>, +} +impl Default for Demos { + fn default() -> Self { + Self { + demos: vec![ + (false, Box::new(crate::demos::DancingStrings::default())), + (false, Box::new(crate::demos::DragAndDropDemo::default())), + ], + } + } +} +impl Demos { + pub fn checkboxes(&mut self, ui: &mut Ui) { + for (ref mut open, demo) in &mut self.demos { + ui.checkbox(open, demo.name()); + } + } + + pub fn show(&mut self, ctx: &Arc) { + for (ref mut open, demo) in &mut self.demos { + demo.show(ctx, open); + } + } +} + +// ---------------------------------------------------------------------------- + /// A menu bar in which you can select different demo windows to show. #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -36,6 +73,9 @@ pub struct DemoWindows { fractal_clock: demos::FractalClock, + /// open, title, view + demos: Demos, + #[cfg_attr(feature = "serde", serde(skip))] previous_link: Option, } @@ -78,7 +118,8 @@ impl DemoWindows { ui.heading("Windows:"); ui.indent("windows", |ui| { - self.open_windows.ui(ui); + self.open_windows.checkboxes(ui); + self.demos.checkboxes(ui); }); }); @@ -101,6 +142,7 @@ impl DemoWindows { demo_window, color_test, fractal_clock, + demos, .. } = self; @@ -139,6 +181,8 @@ impl DemoWindows { color_test.ui(ui, tex_allocator); }); + demos.show(ctx); + fractal_clock.window( ctx, &mut open_windows.fractal_clock, @@ -243,7 +287,7 @@ impl OpenWindows { } } - fn ui(&mut self, ui: &mut Ui) { + fn checkboxes(&mut self, ui: &mut Ui) { let Self { demo, fractal_clock, @@ -253,16 +297,19 @@ impl OpenWindows { resize, color_test, } = self; - ui.checkbox(demo, "Demo"); - ui.checkbox(fractal_clock, "Fractal Clock"); - ui.separator(); + ui.label("Egui:"); ui.checkbox(settings, "Settings"); ui.checkbox(inspection, "Inspection"); ui.checkbox(memory, "Memory"); - ui.checkbox(resize, "Resize examples"); ui.separator(); + ui.checkbox(demo, "Demo"); + ui.separator(); + ui.checkbox(resize, "Resize examples"); ui.checkbox(color_test, "Color test") .on_hover_text("For testing the integrations painter"); + ui.separator(); + ui.label("Misc:"); + ui.checkbox(fractal_clock, "Fractal Clock"); } } @@ -282,7 +329,7 @@ fn show_menu_bar(ui: &mut Ui, windows: &mut OpenWindows, seconds_since_midnight: *ui.ctx().memory() = Default::default(); } }); - menu::menu(ui, "Windows", |ui| windows.ui(ui)); + menu::menu(ui, "Windows", |ui| windows.checkboxes(ui)); menu::menu(ui, "About", |ui| { ui.label("This is Egui"); ui.add(Hyperlink::new("https://github.com/emilk/egui").text("Egui home page")); diff --git a/egui/src/demos/drag_and_drop.rs b/egui/src/demos/drag_and_drop.rs new file mode 100644 index 00000000..69a38bc3 --- /dev/null +++ b/egui/src/demos/drag_and_drop.rs @@ -0,0 +1,161 @@ +use crate::{ + demos::{Demo, View}, + *, +}; + +pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) { + let is_being_dragged = ui.memory().is_being_dragged(id); + + if !is_being_dragged { + let response = ui.wrap(body).1; + + // Check for drags: + let response = ui.interact(response.rect, id, Sense::drag()); + if response.hovered { + ui.output().cursor_icon = CursorIcon::Grab; + } + } else { + ui.output().cursor_icon = CursorIcon::Grabbing; + + // Paint the body to a new layer: + let layer_id = LayerId::new(layers::Order::Tooltip, id); + let response = ui.with_layer_id(layer_id, body).1; + + // Now we move the visuals of the body to where the mouse is. + // Normally you need to decide a location for a widget first, + // because otherwise that widget cannot interact with the mouse. + // However, a dragged component cannot be interacted with anyway + // (anything with `Order::Tooltip` always gets an empty `Response`) + // So this is fine! + + if let Some(mouse_pos) = ui.input().mouse.pos { + let delta = mouse_pos - response.rect.center(); + ui.ctx().graphics().list(layer_id).translate(delta); + } + } +} + +pub fn drop_target( + ui: &mut Ui, + can_accept_what_is_being_dragged: bool, + body: impl FnOnce(&mut Ui) -> R, +) -> (R, Response) { + let is_being_dragged = ui.memory().is_anything_being_dragged(); + + let margin = Vec2::splat(4.0); + + let outer_rect_bounds = ui.available(); + let inner_rect = outer_rect_bounds.shrink2(margin); + let where_to_put_background = ui.painter().add(PaintCmd::Noop); + let mut content_ui = ui.child_ui(inner_rect, *ui.layout()); + let ret = body(&mut content_ui); + let outer_rect = Rect::from_min_max(outer_rect_bounds.min, content_ui.min_rect().max + margin); + + let response = ui.interact_hover(outer_rect); + + let style = if is_being_dragged && can_accept_what_is_being_dragged && response.hovered { + ui.style().visuals.widgets.active + } else if is_being_dragged && can_accept_what_is_being_dragged { + ui.style().visuals.widgets.inactive + } else if is_being_dragged && !can_accept_what_is_being_dragged { + ui.style().visuals.widgets.disabled + } else { + ui.style().visuals.widgets.inactive + }; + + ui.painter().set( + where_to_put_background, + PaintCmd::Rect { + corner_radius: style.corner_radius, + fill: style.bg_fill, + stroke: style.bg_stroke, + rect: outer_rect, + }, + ); + + ui.allocate_space(outer_rect.size()); + + (ret, response) +} + +pub struct DragAndDropDemo { + /// columns with items + columns: Vec>, +} + +impl Default for DragAndDropDemo { + fn default() -> Self { + Self { + columns: vec![ + vec!["Item A", "Item B", "Item C"], + vec!["Item D", "Item E"], + vec!["Item F", "Item G", "Item H"], + ], + } + } +} + +impl Demo for DragAndDropDemo { + fn name(&self) -> &str { + "Drag and Drop" + } + + fn show(&mut self, ctx: &std::sync::Arc, open: &mut bool) { + Window::new(self.name()) + .open(open) + .default_size(vec2(256.0, 256.0)) + .scroll(false) + .resizable(false) + .show(ctx, |ui| self.ui(ui)); + } +} + +impl View for DragAndDropDemo { + fn ui(&mut self, ui: &mut Ui) { + ui.label("This is a proof-of-concept of drag-and-drop in Egui"); + ui.label("Drag items between columns."); + + let mut source_col_row = None; + let mut drop_col = None; + + ui.columns(self.columns.len(), |uis| { + for (col_idx, column) in self.columns.iter().enumerate() { + let ui = &mut uis[col_idx]; + let can_accept_what_is_being_dragged = true; // We accept anything being dragged (for now) ¯\_(ツ)_/¯ + let response = drop_target(ui, can_accept_what_is_being_dragged, |ui| { + ui.set_min_size(vec2(64.0, 100.0)); + + for (row_idx, &item) in column.iter().enumerate() { + let item_id = Id::new("item").with(col_idx).with(row_idx); + drag_source(ui, item_id, |ui| { + ui.label(item); + }); + + let this_item_being_dragged = ui.memory().is_being_dragged(item_id); + if this_item_being_dragged { + source_col_row = Some((col_idx, row_idx)); + } + } + }) + .1; + + let is_being_dragged = ui.memory().is_anything_being_dragged(); + if is_being_dragged && can_accept_what_is_being_dragged && response.hovered { + drop_col = Some(col_idx); + } + } + }); + + if let Some((source_col, source_row)) = source_col_row { + if let Some(drop_col) = drop_col { + if ui.input().mouse.released { + // do the drop: + let item = self.columns[source_col].remove(source_row); + self.columns[drop_col].push(item); + } + } + } + + ui.add(__egui_github_link_file!()); + } +} diff --git a/egui/src/demos/fractal_clock.rs b/egui/src/demos/fractal_clock.rs index 84e33a9e..bf9c1588 100644 --- a/egui/src/demos/fractal_clock.rs +++ b/egui/src/demos/fractal_clock.rs @@ -39,7 +39,7 @@ impl FractalClock { ) { Window::new("FractalClock") .open(open) - .default_rect(ctx.available_rect().expand(-42.0)) + .default_size(vec2(512.0, 512.0)) .scroll(false) // Dark background frame to make it pop: .frame(Frame::window(&ctx.style()).fill(Srgba::black_alpha(250))) diff --git a/egui/src/demos/mod.rs b/egui/src/demos/mod.rs index 163e2635..be40f938 100644 --- a/egui/src/demos/mod.rs +++ b/egui/src/demos/mod.rs @@ -3,16 +3,19 @@ //! The demo-code is also used in benchmarks and tests. mod app; mod color_test; +mod dancing_strings; pub mod demo_window; mod demo_windows; +mod drag_and_drop; mod fractal_clock; mod sliders; pub mod toggle_switch; mod widgets; pub use { - app::*, color_test::ColorTest, demo_window::DemoWindow, demo_windows::*, - fractal_clock::FractalClock, sliders::Sliders, widgets::Widgets, + app::*, color_test::ColorTest, dancing_strings::DancingStrings, demo_window::DemoWindow, + demo_windows::*, drag_and_drop::*, fractal_clock::FractalClock, sliders::Sliders, + widgets::Widgets, }; pub const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; @@ -23,6 +26,21 @@ Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, tur // ---------------------------------------------------------------------------- +/// Something to view in the demo windows +pub trait View { + fn ui(&mut self, ui: &mut crate::Ui); +} + +/// Something to view +pub trait Demo { + fn name(&self) -> &str; + + /// Show windows, etc + fn show(&mut self, ctx: &std::sync::Arc, open: &mut bool); +} + +// ---------------------------------------------------------------------------- + #[cfg(debug_assertions)] pub fn has_debug_assertions() -> bool { true diff --git a/egui/src/demos/sliders.rs b/egui/src/demos/sliders.rs index 012f3c46..7cf22297 100644 --- a/egui/src/demos/sliders.rs +++ b/egui/src/demos/sliders.rs @@ -67,7 +67,7 @@ impl Sliders { .text("f64 demo slider"), ); - ui.label("Sliders will automatically figure out how many decimals to show."); + ui.label("Sliders will automatically figure out how many decimals to show."); if ui.button("Assign PI").clicked { self.value = std::f64::consts::PI; diff --git a/egui/src/memory.rs b/egui/src/memory.rs index 822b9488..ef4ce820 100644 --- a/egui/src/memory.rs +++ b/egui/src/memory.rs @@ -149,6 +149,14 @@ impl Memory { self.interaction.kb_focus_id = None; } + pub fn is_anything_being_dragged(&self) -> bool { + self.interaction.drag_id.is_some() + } + + pub fn is_being_dragged(&self, id: Id) -> bool { + self.interaction.drag_id == Some(id) + } + /// Forget window positions, sizes etc. /// Can be used to auto-layout windows. pub fn reset_areas(&mut self) { diff --git a/egui/src/painter.rs b/egui/src/painter.rs index 86ee16e2..2b3232ca 100644 --- a/egui/src/painter.rs +++ b/egui/src/painter.rs @@ -32,6 +32,20 @@ impl Painter { } } + #[must_use] + pub fn with_layer_id(self, layer_id: LayerId) -> Self { + Self { + ctx: self.ctx, + layer_id, + clip_rect: self.clip_rect, + } + } + + /// redirect + pub fn set_layer_id(&mut self, layer_id: LayerId) { + self.layer_id = layer_id; + } + /// Create a painter for a sub-region of this `Painter`. /// /// The clip-rect of the returned `Painter` will be the intersection diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 9ec86254..9cd327ae 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -659,12 +659,26 @@ impl Ui { /// # Adding Containers / Sub-uis: impl Ui { - pub fn collapsing( + /// Create a child ui. You can use this to temporarily change the Style of a sub-region, for instance. + pub fn wrap(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) { + let child_rect = self.available(); + let mut child_ui = self.child_ui(child_rect, self.layout); + let ret = add_contents(&mut child_ui); + let size = child_ui.min_size(); + let rect = self.allocate_space(size); + (ret, self.interact_hover(rect)) + } + + /// Redirect paint commands to another paint layer. + pub fn with_layer_id( &mut self, - heading: impl Into, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> CollapsingResponse { - CollapsingHeader::new(heading).show(self, add_contents) + layer_id: LayerId, + add_contents: impl FnOnce(&mut Self) -> R, + ) -> (R, Response) { + self.wrap(|ui| { + ui.painter.set_layer_id(layer_id); + add_contents(ui) + }) } /// Create a child ui at the current cursor. @@ -682,14 +696,12 @@ impl Ui { self.allocate_space(child_ui.min_size()) } - /// Create a child ui. You can use this to temporarily change the Style of a sub-region, for instance. - pub fn add_custom(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) { - let child_rect = self.available(); - let mut child_ui = self.child_ui(child_rect, self.layout); - let ret = add_contents(&mut child_ui); - let size = child_ui.min_size(); - let rect = self.allocate_space(size); - (ret, self.interact_hover(rect)) + pub fn collapsing( + &mut self, + heading: impl Into, + add_contents: impl FnOnce(&mut Ui) -> R, + ) -> CollapsingResponse { + CollapsingHeader::new(heading).show(self, add_contents) } /// Create a child ui which is indented to the right @@ -786,7 +798,7 @@ impl Ui { self.with_layout(Layout::vertical(Align::Min), add_contents) } - pub fn inner_layout( + fn inner_layout( &mut self, layout: Layout, initial_size: Vec2,