From d76ec0f47487c9a3f75e2ed1c45a88b7012ae7c9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 17 Feb 2024 15:48:23 +0100 Subject: [PATCH] New widget interaction logic (#4026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Closes https://github.com/emilk/egui/issues/3936 * Closes https://github.com/emilk/egui/issues/3923 * Closes https://github.com/emilk/egui/pull/4058 The interaction code is now done at the start of the frame, using stored `WidgetRect`s from the previous frame. The intention is that the new interaction code should be more accurate, making it easier to hit widgets, and better respecting the rules of overlapping widgets. There is a new `style::Interaction::interact_radius` controlling how far away from a widget the cursor can be and still hit it. This helps big fat fingers hit small widgets on touch screens. This PR adds a new `Context::read_response` which lets you read the `Response` of a `Widget` _before_ you create the widget. This can be used for styling, or for reading the result of an interaction early (to prevent frame-delay) for a widget you add late (so it is on top of other widgets). # ⚠️ BREAKING CHANGES `Memory::dragged_id`, `Memory::set_dragged_id` etc have been moved to `Context`. The semantics for `Context::dragged_id` is slightly different: a widget is not considered dragged until egui it is sure this is not a click-in-progress. For a widget that is only sensitive to drags, that is right away, but for widgets sensitive to both clicks and drags it is not until the mouse has moved a certain distance. # TODO * [x] Fix panel resizing * [x] Fix scroll hover weirdness * [x] Fix Resize widget * [x] Fix drag-and-drop * [x] Test all of egui_demo_app * [x] Change `is_dragging` API * [x] Consistent naming of start/stop or begin/end drag * [x] Test `egui_tiles` * [x] Test Rerun * [x] Document * [x] Document breaking changes in PR description * [x] Test one final time # Saving for a later PR * [ ] Fix https://github.com/emilk/egui/issues/4047 * [ ] Specify what the response order for e.g. `ui.horizontal` is I think both these can be fixed if each `Ui` registers themselves as a `WidgetRect`, with the possibility to interact with it later, as if the interaction was under all widgets on top of it. --- crates/egui/src/containers/area.rs | 13 +- crates/egui/src/containers/panel.rs | 135 ++-- crates/egui/src/containers/resize.rs | 43 +- crates/egui/src/containers/window.rs | 418 ++++++----- crates/egui/src/context.rs | 704 +++++++++++-------- crates/egui/src/hit_test.rs | 422 +++++++++++ crates/egui/src/interaction.rs | 230 ++++++ crates/egui/src/introspection.rs | 15 +- crates/egui/src/lib.rs | 33 + crates/egui/src/memory.rs | 98 +-- crates/egui/src/response.rs | 69 +- crates/egui/src/sense.rs | 7 + crates/egui/src/style.rs | 33 +- crates/egui/src/ui.rs | 42 +- crates/egui/src/widgets/drag_value.rs | 2 +- crates/egui/src/widgets/text_edit/builder.rs | 2 +- crates/egui_demo_lib/src/demo/tests.rs | 4 +- crates/egui_extras/src/layout.rs | 1 + crates/egui_plot/src/lib.rs | 2 +- examples/test_viewports/src/main.rs | 8 +- 20 files changed, 1513 insertions(+), 768 deletions(-) create mode 100644 crates/egui/src/hit_test.rs create mode 100644 crates/egui/src/interaction.rs diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 4aff4edb3d05..e93217e58a62 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -313,22 +313,21 @@ impl Area { let mut move_response = { let interact_id = layer_id.id.with("move"); let sense = if movable { - Sense::click_and_drag() + Sense::drag() } else if interactable { Sense::click() // allow clicks to bring to front } else { Sense::hover() }; - let move_response = ctx.interact( - Rect::EVERYTHING, - ctx.style().spacing.item_spacing, + let move_response = ctx.create_widget(WidgetRect { + id: interact_id, layer_id, - interact_id, - state.rect(), + rect: state.rect(), + interact_rect: state.rect(), sense, enabled, - ); + }); if movable && move_response.dragged() { state.pivot_pos += move_response.drag_delta(); diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 705dfd733cdd..11e69b8289cb 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -238,48 +238,22 @@ impl SidePanel { ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel"); } + let resize_id = id.with("__resize"); let mut resize_hover = false; let mut is_resizing = false; if resizable { - let resize_id = id.with("__resize"); - if let Some(pointer) = ui.ctx().pointer_latest_pos() { - let we_are_on_top = ui - .ctx() - .layer_id_at(pointer) - .map_or(true, |top_layer_id| top_layer_id == ui.layer_id()); - let pointer = if let Some(transform) = ui - .ctx() - .memory(|m| m.layer_transforms.get(&ui.layer_id()).cloned()) - { - transform.inverse() * pointer - } else { - pointer - }; - - let resize_x = side.opposite().side_x(panel_rect); - let mouse_over_resize_line = we_are_on_top - && panel_rect.y_range().contains(pointer.y) - && (resize_x - pointer.x).abs() - <= ui.style().interaction.resize_grab_radius_side; - - if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) - && mouse_over_resize_line - { - ui.memory_mut(|mem| mem.set_dragged_id(resize_id)); - } - is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id)); - if is_resizing { - let width = (pointer.x - side.side_x(panel_rect)).abs(); - let width = clamp_to_range(width, width_range).at_most(available_rect.width()); - side.set_rect_width(&mut panel_rect, width); - } + // First we read the resize interaction results, to avoid frame latency in the resize: + if let Some(resize_response) = ui.ctx().read_response(resize_id) { + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); - let dragging_something_else = - ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed()); - resize_hover = mouse_over_resize_line && !dragging_something_else; - - if resize_hover || is_resizing { - ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal); + if is_resizing { + if let Some(pointer) = resize_response.interact_pointer_pos() { + let width = (pointer.x - side.side_x(panel_rect)).abs(); + let width = + clamp_to_range(width, width_range).at_most(available_rect.width()); + side.set_rect_width(&mut panel_rect, width); + } } } } @@ -309,6 +283,22 @@ impl SidePanel { } ui.expand_to_include_rect(rect); + if resizable { + // Now we do the actual resize interaction, on top of all the contents. + // Otherwise its input could be eaten by the contents, e.g. a + // `ScrollArea` on either side of the panel boundary. + let resize_x = side.opposite().side_x(panel_rect); + let resize_rect = Rect::from_x_y_ranges(resize_x..=resize_x, panel_rect.y_range()) + .expand2(vec2(ui.style().interaction.resize_grab_radius_side, 0.0)); + let resize_response = ui.interact(resize_rect, resize_id, Sense::drag()); + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); + } + + if resize_hover || is_resizing { + ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal); + } + PanelState { rect }.store(ui.ctx(), id); { @@ -706,50 +696,22 @@ impl TopBottomPanel { .check_for_id_clash(id, panel_rect, "TopBottomPanel"); } + let resize_id = id.with("__resize"); let mut resize_hover = false; let mut is_resizing = false; if resizable { - let resize_id = id.with("__resize"); - let latest_pos = ui.input(|i| i.pointer.latest_pos()); - if let Some(pointer) = latest_pos { - let we_are_on_top = ui - .ctx() - .layer_id_at(pointer) - .map_or(true, |top_layer_id| top_layer_id == ui.layer_id()); - let pointer = if let Some(transform) = ui - .ctx() - .memory(|m| m.layer_transforms.get(&ui.layer_id()).cloned()) - { - transform.inverse() * pointer - } else { - pointer - }; - - let resize_y = side.opposite().side_y(panel_rect); - let mouse_over_resize_line = we_are_on_top - && panel_rect.x_range().contains(pointer.x) - && (resize_y - pointer.y).abs() - <= ui.style().interaction.resize_grab_radius_side; - - if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) - && mouse_over_resize_line - { - ui.memory_mut(|mem| mem.set_dragged_id(resize_id)); - } - is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id)); - if is_resizing { - let height = (pointer.y - side.side_y(panel_rect)).abs(); - let height = - clamp_to_range(height, height_range).at_most(available_rect.height()); - side.set_rect_height(&mut panel_rect, height); - } + // First we read the resize interaction results, to avoid frame latency in the resize: + if let Some(resize_response) = ui.ctx().read_response(resize_id) { + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); - let dragging_something_else = - ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed()); - resize_hover = mouse_over_resize_line && !dragging_something_else; - - if resize_hover || is_resizing { - ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical); + if is_resizing { + if let Some(pointer) = resize_response.interact_pointer_pos() { + let height = (pointer.y - side.side_y(panel_rect)).abs(); + let height = + clamp_to_range(height, height_range).at_most(available_rect.height()); + side.set_rect_height(&mut panel_rect, height); + } } } } @@ -779,6 +741,23 @@ impl TopBottomPanel { } ui.expand_to_include_rect(rect); + if resizable { + // Now we do the actual resize interaction, on top of all the contents. + // Otherwise its input could be eaten by the contents, e.g. a + // `ScrollArea` on either side of the panel boundary. + + let resize_y = side.opposite().side_y(panel_rect); + let resize_rect = Rect::from_x_y_ranges(panel_rect.x_range(), resize_y..=resize_y) + .expand2(vec2(0.0, ui.style().interaction.resize_grab_radius_side)); + let resize_response = ui.interact(resize_rect, resize_id, Sense::drag()); + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); + } + + if resize_hover || is_resizing { + ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical); + } + PanelState { rect }.store(ui.ctx(), id); { diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 9670ecb415e7..22286368b7c6 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -188,8 +188,8 @@ impl Resize { struct Prepared { id: Id, + corner_id: Option, state: State, - corner_response: Option, content_ui: Ui, } @@ -226,22 +226,17 @@ impl Resize { let mut user_requested_size = state.requested_size.take(); - let corner_response = if self.resizable { - // Resize-corner: - let corner_size = Vec2::splat(ui.visuals().resize_corner_size); - let corner_rect = - Rect::from_min_size(position + state.desired_size - corner_size, corner_size); - let corner_response = ui.interact(corner_rect, id.with("corner"), Sense::drag()); + let corner_id = self.resizable.then(|| id.with("__resize_corner")); - if let Some(pointer_pos) = corner_response.interact_pointer_pos() { - user_requested_size = - Some(pointer_pos - position + 0.5 * corner_response.rect.size()); + if let Some(corner_id) = corner_id { + if let Some(corner_response) = ui.ctx().read_response(corner_id) { + if let Some(pointer_pos) = corner_response.interact_pointer_pos() { + // Respond to the interaction early to avoid frame delay. + user_requested_size = + Some(pointer_pos - position + 0.5 * corner_response.rect.size()); + } } - - Some(corner_response) - } else { - None - }; + } if let Some(user_requested_size) = user_requested_size { state.desired_size = user_requested_size; @@ -279,8 +274,8 @@ impl Resize { Prepared { id, + corner_id, state, - corner_response, content_ui, } } @@ -295,8 +290,8 @@ impl Resize { fn end(self, ui: &mut Ui, prepared: Prepared) { let Prepared { id, + corner_id, mut state, - corner_response, content_ui, } = prepared; @@ -320,6 +315,20 @@ impl Resize { // ------------------------------ + let corner_response = if let Some(corner_id) = corner_id { + // We do the corner interaction last to place it on top of the content: + let corner_size = Vec2::splat(ui.visuals().resize_corner_size); + let corner_rect = Rect::from_min_size( + content_ui.min_rect().left_top() + size - corner_size, + corner_size, + ); + Some(ui.interact(corner_rect, corner_id, Sense::drag())) + } else { + None + }; + + // ------------------------------ + if self.with_stroke && corner_response.is_some() { let rect = Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size); let rect = rect.expand(2.0); // breathing room for content diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index f63952641c33..8cf5475ebbde 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -411,8 +411,7 @@ impl<'open> Window<'open> { let is_collapsed = with_title_bar && !collapsing.is_open(); let possible = PossibleInteractions::new(&area, &resize, is_collapsed); - let area = area.movable(false); // We move it manually, or the area will move the window when we want to resize it - let resize = resize.resizable(false); // We move it manually + let resize = resize.resizable(false); // We resize it manually let mut resize = resize.id(resize_id); let on_top = Some(area_layer_id) == ctx.top_layer_id(); @@ -429,34 +428,23 @@ impl<'open> Window<'open> { (0.0, 0.0) }; - // First interact (move etc) to avoid frame delay: + // First check for resize to avoid frame delay: let last_frame_outer_rect = area.state().rect(); - let interaction = if possible.movable || possible.resizable() { - window_interaction( - ctx, - possible, - area_layer_id, - area_id.with("frame_resize"), - last_frame_outer_rect, - ) - .and_then(|window_interaction| { - let margins = window_frame.outer_margin.sum() - + window_frame.inner_margin.sum() - + vec2(0.0, title_bar_height); - - interact( - window_interaction, - ctx, - margins, - area_layer_id, - &mut area, - resize_id, - ) - }) - } else { - None - }; - let hover_interaction = resize_hover(ctx, possible, area_layer_id, last_frame_outer_rect); + let resize_interaction = + resize_interaction(ctx, possible, area_layer_id, last_frame_outer_rect); + + let margins = window_frame.outer_margin.sum() + + window_frame.inner_margin.sum() + + vec2(0.0, title_bar_height); + + resize_response( + resize_interaction, + ctx, + margins, + area_layer_id, + &mut area, + resize_id, + ); let mut area_content_ui = area.content_ui(ctx); @@ -550,23 +538,8 @@ impl<'open> Window<'open> { collapsing.store(ctx); - if let Some(interaction) = interaction { - paint_frame_interaction( - &area_content_ui, - outer_rect, - interaction, - ctx.style().visuals.widgets.active, - ); - } else if let Some(hover_interaction) = hover_interaction { - if ctx.input(|i| i.pointer.has_pointer()) { - paint_frame_interaction( - &area_content_ui, - outer_rect, - hover_interaction, - ctx.style().visuals.widgets.hovered, - ); - } - } + paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); + content_inner }; @@ -606,10 +579,10 @@ fn paint_resize_corner( // ---------------------------------------------------------------------------- +/// Which sides can be resized? #[derive(Clone, Copy, Debug)] struct PossibleInteractions { - movable: bool, - // Which sides can we drag to resize? + // Which sides can we drag to resize or move? resize_left: bool, resize_right: bool, resize_top: bool, @@ -622,7 +595,6 @@ impl PossibleInteractions { let resizable = area.is_enabled() && resize.is_resizable() && !is_collapsed; let pivot = area.get_pivot(); Self { - movable, resize_left: resizable && (movable || pivot.x() != Align::LEFT), resize_right: resizable && (movable || pivot.x() != Align::RIGHT), resize_top: resizable && (movable || pivot.y() != Align::TOP), @@ -635,44 +607,76 @@ impl PossibleInteractions { } } -/// Either a move or resize +/// Resizing the window edges. #[derive(Clone, Copy, Debug)] -pub(crate) struct WindowInteraction { - pub(crate) area_layer_id: LayerId, - pub(crate) start_rect: Rect, - pub(crate) left: bool, - pub(crate) right: bool, - pub(crate) top: bool, - pub(crate) bottom: bool, +struct ResizeInteraction { + start_rect: Rect, + left: SideResponse, + right: SideResponse, + top: SideResponse, + bottom: SideResponse, +} + +/// A minitature version of `Response`, for each side of the window. +#[derive(Clone, Copy, Debug, Default)] +struct SideResponse { + hover: bool, + drag: bool, +} + +impl SideResponse { + pub fn any(&self) -> bool { + self.hover || self.drag + } +} + +impl std::ops::BitOrAssign for SideResponse { + fn bitor_assign(&mut self, rhs: Self) { + *self = Self { + hover: self.hover || rhs.hover, + drag: self.drag || rhs.drag, + }; + } } -impl WindowInteraction { +impl ResizeInteraction { pub fn set_cursor(&self, ctx: &Context) { - if (self.left && self.top) || (self.right && self.bottom) { + let left = self.left.any(); + let right = self.right.any(); + let top = self.top.any(); + let bottom = self.bottom.any(); + + if (left && top) || (right && bottom) { ctx.set_cursor_icon(CursorIcon::ResizeNwSe); - } else if (self.right && self.top) || (self.left && self.bottom) { + } else if (right && top) || (left && bottom) { ctx.set_cursor_icon(CursorIcon::ResizeNeSw); - } else if self.left || self.right { + } else if left || right { ctx.set_cursor_icon(CursorIcon::ResizeHorizontal); - } else if self.bottom || self.top { + } else if bottom || top { ctx.set_cursor_icon(CursorIcon::ResizeVertical); } } - pub fn is_resize(&self) -> bool { - self.left || self.right || self.top || self.bottom + pub fn any_hovered(&self) -> bool { + self.left.hover || self.right.hover || self.top.hover || self.bottom.hover + } + + pub fn any_dragged(&self) -> bool { + self.left.drag || self.right.drag || self.top.drag || self.bottom.drag } } -fn interact( - window_interaction: WindowInteraction, +fn resize_response( + resize_interaction: ResizeInteraction, ctx: &Context, margins: Vec2, area_layer_id: LayerId, area: &mut area::Prepared, resize_id: Id, -) -> Option { - let new_rect = move_and_resize_window(ctx, &window_interaction)?; +) { + let Some(new_rect) = move_and_resize_window(ctx, &resize_interaction) else { + return; + }; let mut new_rect = ctx.round_rect_to_pixels(new_rect); if area.constrain() { @@ -682,7 +686,7 @@ fn interact( // TODO(emilk): add this to a Window state instead as a command "move here next frame" area.state_mut().set_left_top_pos(new_rect.left_top()); - if window_interaction.is_resize() { + if resize_interaction.any_dragged() { if let Some(mut state) = resize::State::load(ctx, resize_id) { state.requested_size = Some(new_rect.size() - margins); state.store(ctx, resize_id); @@ -690,191 +694,179 @@ fn interact( } ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id)); - Some(window_interaction) } -fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction) -> Option { - window_interaction.set_cursor(ctx); - - // Only move/resize windows with primary mouse button: - if !ctx.input(|i| i.pointer.primary_down()) { +fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option { + if !interaction.any_dragged() { return None; } let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?; - let mut rect = window_interaction.start_rect; // prevent drift + let mut rect = interaction.start_rect; // prevent drift - if window_interaction.is_resize() { - if window_interaction.left { - rect.min.x = ctx.round_to_pixel(pointer_pos.x); - } else if window_interaction.right { - rect.max.x = ctx.round_to_pixel(pointer_pos.x); - } + if interaction.left.drag { + rect.min.x = ctx.round_to_pixel(pointer_pos.x); + } else if interaction.right.drag { + rect.max.x = ctx.round_to_pixel(pointer_pos.x); + } - if window_interaction.top { - rect.min.y = ctx.round_to_pixel(pointer_pos.y); - } else if window_interaction.bottom { - rect.max.y = ctx.round_to_pixel(pointer_pos.y); - } - } else { - // Movement. - - // We do window interaction first (to avoid frame delay), - // but we want anything interactive in the window (e.g. slider) to steal - // the drag from us. It is therefor important not to move the window the first frame, - // but instead let other widgets to the steal. HACK. - if !ctx.input(|i| i.pointer.any_pressed()) { - let press_origin = ctx.input(|i| i.pointer.press_origin())?; - let delta = pointer_pos - press_origin; - rect = rect.translate(delta); - } + if interaction.top.drag { + rect.min.y = ctx.round_to_pixel(pointer_pos.y); + } else if interaction.bottom.drag { + rect.max.y = ctx.round_to_pixel(pointer_pos.y); } Some(rect) } -/// Returns `Some` if there is a move or resize -fn window_interaction( +fn resize_interaction( ctx: &Context, possible: PossibleInteractions, - area_layer_id: LayerId, - id: Id, + layer_id: LayerId, rect: Rect, -) -> Option { - if ctx.memory(|mem| mem.dragging_something_else(id)) { - return None; +) -> ResizeInteraction { + if !possible.resizable() { + return ResizeInteraction { + start_rect: rect, + left: Default::default(), + right: Default::default(), + top: Default::default(), + bottom: Default::default(), + }; } - let mut window_interaction = ctx.memory(|mem| mem.window_interaction()); - - if window_interaction.is_none() { - if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) { - hover_window_interaction.set_cursor(ctx); - if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) { - ctx.memory_mut(|mem| { - mem.interaction_mut().drag_id = Some(id); - mem.interaction_mut().drag_is_window = true; - window_interaction = Some(hover_window_interaction); - mem.set_window_interaction(window_interaction); - }); - } + let is_dragging = |rect, id| { + let response = ctx.create_widget(WidgetRect { + layer_id, + id, + rect, + interact_rect: rect, + sense: Sense::drag(), + enabled: true, + }); + SideResponse { + hover: response.hovered(), + drag: response.dragged(), } - } + }; - if let Some(window_interaction) = window_interaction { - let is_active = ctx.memory_mut(|mem| mem.interaction().drag_id == Some(id)); + let id = Id::new(layer_id).with("edge_drag"); - if is_active && window_interaction.area_layer_id == area_layer_id { - return Some(window_interaction); - } - } + let side_grab_radius = ctx.style().interaction.resize_grab_radius_side; + let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner; - None -} + let corner_rect = + |center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius)); -fn resize_hover( - ctx: &Context, - possible: PossibleInteractions, - area_layer_id: LayerId, - rect: Rect, -) -> Option { - let pointer = ctx.input(|i| i.pointer.interact_pos())?; + // What are we dragging/hovering? + let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4]; - if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) { - return None; // already dragging (something) - } + // ---------------------------------------- + // Check sides first, so that corners are on top, covering the sides (i.e. corners have priority) - if let Some(top_layer_id) = ctx.layer_id_at(pointer) { - if top_layer_id != area_layer_id && top_layer_id.order != Order::Background { - return None; // Another window is on top here - } + if possible.resize_right { + let response = is_dragging( + Rect::from_min_max(rect.right_top(), rect.right_bottom()).expand(side_grab_radius), + id.with("right"), + ); + right |= response; + } + if possible.resize_left { + let response = is_dragging( + Rect::from_min_max(rect.left_top(), rect.left_bottom()).expand(side_grab_radius), + id.with("left"), + ); + left |= response; + } + if possible.resize_bottom { + let response = is_dragging( + Rect::from_min_max(rect.left_bottom(), rect.right_bottom()).expand(side_grab_radius), + id.with("bottom"), + ); + bottom |= response; + } + if possible.resize_top { + let response = is_dragging( + Rect::from_min_max(rect.left_top(), rect.right_top()).expand(side_grab_radius), + id.with("top"), + ); + top |= response; } - if ctx.memory(|mem| mem.interaction().drag_interest) { - // Another widget will become active if we drag here - return None; + // ---------------------------------------- + // Now check corners: + + if possible.resize_right && possible.resize_bottom { + let response = is_dragging(corner_rect(rect.right_bottom()), id.with("right_bottom")); + right |= response; + bottom |= response; } - let side_grab_radius = ctx.style().interaction.resize_grab_radius_side; - let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner; - if !rect.expand(side_grab_radius).contains(pointer) { - return None; + if possible.resize_right && possible.resize_top { + let response = is_dragging(corner_rect(rect.right_top()), id.with("right_top")); + right |= response; + top |= response; } - let mut left = possible.resize_left && (rect.left() - pointer.x).abs() <= side_grab_radius; - let mut right = possible.resize_right && (rect.right() - pointer.x).abs() <= side_grab_radius; - let mut top = possible.resize_top && (rect.top() - pointer.y).abs() <= side_grab_radius; - let mut bottom = - possible.resize_bottom && (rect.bottom() - pointer.y).abs() <= side_grab_radius; - - if possible.resize_right - && possible.resize_bottom - && rect.right_bottom().distance(pointer) < corner_grab_radius - { - right = true; - bottom = true; - } - if possible.resize_right - && possible.resize_top - && rect.right_top().distance(pointer) < corner_grab_radius - { - right = true; - top = true; - } - if possible.resize_left - && possible.resize_top - && rect.left_top().distance(pointer) < corner_grab_radius - { - left = true; - top = true; - } - if possible.resize_left - && possible.resize_bottom - && rect.left_bottom().distance(pointer) < corner_grab_radius - { - left = true; - bottom = true; - } - - let any_resize = left || right || top || bottom; - - if !any_resize && !possible.movable { - return None; + if possible.resize_left && possible.resize_bottom { + let response = is_dragging(corner_rect(rect.left_bottom()), id.with("left_bottom")); + left |= response; + bottom |= response; } - if any_resize || possible.movable { - Some(WindowInteraction { - area_layer_id, - start_rect: rect, - left, - right, - top, - bottom, - }) - } else { - None + if possible.resize_left && possible.resize_top { + let response = is_dragging(corner_rect(rect.left_top()), id.with("left_top")); + left |= response; + top |= response; } + + let interaction = ResizeInteraction { + start_rect: rect, + left, + right, + top, + bottom, + }; + interaction.set_cursor(ctx); + interaction } /// Fill in parts of the window frame when we resize by dragging that part -fn paint_frame_interaction( - ui: &Ui, - rect: Rect, - interaction: WindowInteraction, - visuals: style::WidgetVisuals, -) { +fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) { use epaint::tessellator::path::add_circle_quadrant; + let visuals = if interaction.any_dragged() { + ui.style().visuals.widgets.active + } else if interaction.any_hovered() { + ui.style().visuals.widgets.hovered + } else { + return; + }; + + let [left, right, top, bottom]: [bool; 4]; + + if interaction.any_dragged() { + left = interaction.left.drag; + right = interaction.right.drag; + top = interaction.top.drag; + bottom = interaction.bottom.drag; + } else { + left = interaction.left.hover; + right = interaction.right.hover; + top = interaction.top.hover; + bottom = interaction.bottom.hover; + } + let rounding = ui.visuals().window_rounding; let Rect { min, max } = rect; let mut points = Vec::new(); - if interaction.right && !interaction.bottom && !interaction.top { + if right && !bottom && !top { points.push(pos2(max.x, min.y + rounding.ne)); points.push(pos2(max.x, max.y - rounding.se)); } - if interaction.right && interaction.bottom { + if right && bottom { points.push(pos2(max.x, min.y + rounding.ne)); points.push(pos2(max.x, max.y - rounding.se)); add_circle_quadrant( @@ -884,11 +876,11 @@ fn paint_frame_interaction( 0.0, ); } - if interaction.bottom { + if bottom { points.push(pos2(max.x - rounding.se, max.y)); points.push(pos2(min.x + rounding.sw, max.y)); } - if interaction.left && interaction.bottom { + if left && bottom { add_circle_quadrant( &mut points, pos2(min.x + rounding.sw, max.y - rounding.sw), @@ -896,11 +888,11 @@ fn paint_frame_interaction( 1.0, ); } - if interaction.left { + if left { points.push(pos2(min.x, max.y - rounding.sw)); points.push(pos2(min.x, min.y + rounding.nw)); } - if interaction.left && interaction.top { + if left && top { add_circle_quadrant( &mut points, pos2(min.x + rounding.nw, min.y + rounding.nw), @@ -908,11 +900,11 @@ fn paint_frame_interaction( 2.0, ); } - if interaction.top { + if top { points.push(pos2(min.x + rounding.nw, min.y)); points.push(pos2(max.x - rounding.ne, min.y)); } - if interaction.right && interaction.top { + if right && top { add_circle_quadrant( &mut points, pos2(max.x - rounding.ne, min.y + rounding.ne), diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 440fe522c650..22741b237d5c 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -22,6 +22,8 @@ use crate::{ TextureHandle, ViewportCommand, *, }; +use self::{hit_test::WidgetHits, interaction::InteractionSnapshot}; + /// Information given to the backend about when it is time to repaint the ui. /// /// This is given in the callback set by [`Context::set_request_repaint_callback`]. @@ -200,11 +202,6 @@ impl ContextImpl { /// Used to check for overlaps between widgets when handling events. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct WidgetRect { - /// Where the widget is. - /// - /// This is after clipping with the parent ui clip rect. - pub interact_rect: Rect, - /// The globally unique widget id. /// /// For interactive widgets, this better be globally unique. @@ -215,8 +212,22 @@ pub struct WidgetRect { /// You can ensure globally unique ids using [`Ui::push_id`]. pub id: Id, + /// What layer the widget is on. + pub layer_id: LayerId, + + /// The full widget rectangle. + pub rect: Rect, + + /// Where the widget is. + /// + /// This is after clipping with the parent ui clip rect. + pub interact_rect: Rect, + /// How the widget responds to interaction. pub sense: Sense, + + /// Is the widget enabled? + pub enabled: bool, } /// Stores the positions of all widgets generated during a single egui update/frame. @@ -226,14 +237,21 @@ pub struct WidgetRect { pub struct WidgetRects { /// All widgets, in painting order. pub by_layer: HashMap>, + + /// All widgets + pub by_id: IdMap, } impl WidgetRects { /// Clear the contents while retaining allocated memory. pub fn clear(&mut self) { - for rects in self.by_layer.values_mut() { + let Self { by_layer, by_id } = self; + + for rects in by_layer.values_mut() { rects.clear(); } + + by_id.clear(); } /// Insert the given widget rect in the given layer. @@ -242,18 +260,33 @@ impl WidgetRects { return; } - let layer_widgets = self.by_layer.entry(layer_id).or_default(); + let Self { by_layer, by_id } = self; + + let layer_widgets = by_layer.entry(layer_id).or_default(); - if let Some(last) = layer_widgets.last_mut() { - if last.id == widget_rect.id { - // e.g. calling `response.interact(…)` right after interacting. - last.sense |= widget_rect.sense; - last.interact_rect = last.interact_rect.union(widget_rect.interact_rect); - return; + match by_id.entry(widget_rect.id) { + std::collections::hash_map::Entry::Vacant(entry) => { + // A new widget + entry.insert(widget_rect); + layer_widgets.push(widget_rect); + } + std::collections::hash_map::Entry::Occupied(mut entry) => { + // e.g. calling `response.interact(…)` to add more interaction. + let existing = entry.get_mut(); + existing.rect = existing.rect.union(widget_rect.rect); + existing.interact_rect = existing.interact_rect.union(widget_rect.interact_rect); + existing.sense |= widget_rect.sense; + existing.enabled |= widget_rect.enabled; + + // Find the existing widget in this layer and update it: + for previous in layer_widgets.iter_mut().rev() { + if previous.id == widget_rect.id { + *previous = *existing; + break; + } + } } } - - layer_widgets.push(widget_rect); } } @@ -285,16 +318,28 @@ struct ViewportState { used: bool, /// Written to during the frame. - layer_rects_this_frame: WidgetRects, + widgets_this_frame: WidgetRects, /// Read - layer_rects_prev_frame: WidgetRects, + widgets_prev_frame: WidgetRects, /// State related to repaint scheduling. repaint: ViewportRepaintInfo, + // ---------------------- + // Updated at the start of the frame: + // + /// Which widgets are under the pointer? + hits: WidgetHits, + + /// What widgets are being interacted with this frame? + /// + /// Based on the widgets from last frame, and input in this frame. + interact_widgets: InteractionSnapshot, + // ---------------------- // The output of a frame: + // graphics: GraphicLayers, // Most of the things in `PlatformOutput` are not actually viewport dependent. output: PlatformOutput, @@ -491,6 +536,56 @@ impl ContextImpl { viewport.frame_state.begin_frame(&viewport.input); + { + let area_order: HashMap = self + .memory + .areas() + .order() + .iter() + .enumerate() + .map(|(i, id)| (*id, i)) + .collect(); + + let mut layers: Vec = viewport + .widgets_prev_frame + .by_layer + .keys() + .copied() + .collect(); + + layers.sort_by(|a, b| { + if a.order == b.order { + // Maybe both are windows, so respect area order: + area_order.get(a).cmp(&area_order.get(b)) + } else { + // comparing e.g. background to tooltips + a.order.cmp(&b.order) + } + }); + + viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() { + let interact_radius = self.memory.options.style.interaction.interact_radius; + + crate::hit_test::hit_test( + &viewport.widgets_prev_frame, + &layers, + &self.memory.layer_transforms, + pos, + interact_radius, + ) + } else { + WidgetHits::default() + }; + + viewport.interact_widgets = crate::interaction::interact( + &viewport.interact_widgets, + &viewport.widgets_prev_frame, + &viewport.hits, + &viewport.input, + self.memory.interaction_mut(), + ); + } + // Ensure we register the background area so panels and background ui can catch clicks: let screen_rect = viewport.input.screen_rect(); self.memory.areas_mut().set_state( @@ -621,7 +716,7 @@ impl ContextImpl { } /// The current active viewport - fn viewport(&mut self) -> &mut ViewportState { + pub(crate) fn viewport(&mut self) -> &mut ViewportState { self.viewports.entry(self.viewport_id()).or_default() } @@ -1015,69 +1110,99 @@ impl Context { // --------------------------------------------------------------------- - /// Use `ui.interact` instead + /// Create a widget and check for interaction. + /// + /// If this is not called, the widget doesn't exist. + /// + /// You should use [`Ui::interact`] instead. + /// + /// If the widget already exists, its state (sense, Rect, etc) will be updated. #[allow(clippy::too_many_arguments)] - pub(crate) fn interact( - &self, - clip_rect: Rect, - item_spacing: Vec2, - layer_id: LayerId, - id: Id, - rect: Rect, - sense: Sense, - enabled: bool, - ) -> Response { - let gap = 0.1; // Just to make sure we don't accidentally hover two things at once (a small eps should be sufficient). - - // Make it easier to click things: - let interact_rect = rect.expand2( - (0.5 * item_spacing - Vec2::splat(gap)) - .at_least(Vec2::splat(0.0)) - .at_most(Vec2::splat(5.0)), - ); + pub(crate) fn create_widget(&self, mut w: WidgetRect) -> Response { + if !w.enabled { + w.sense.click = false; + w.sense.drag = false; + } - // Respect clip rectangle when interacting: - let interact_rect = clip_rect.intersect(interact_rect); + if w.interact_rect.is_positive() { + // Remember this widget + self.write(|ctx| { + let viewport = ctx.viewport(); - let contains_pointer = self.widget_contains_pointer(layer_id, id, sense, interact_rect); + // We add all widgets here, even non-interactive ones, + // because we need this list not only for checking for blocking widgets, + // but also to know when we have reached the widget we are checking for cover. + viewport.widgets_this_frame.insert(w.layer_id, w); - #[cfg(debug_assertions)] - if sense.interactive() - && interact_rect.is_positive() - && self.style().debug.show_interactive_widgets - { - Self::layer_painter(self, LayerId::debug()).rect( - interact_rect, - 0.0, - Color32::YELLOW.additive().linear_multiply(0.005), - Stroke::new(1.0, Color32::YELLOW.additive().linear_multiply(0.05)), - ); + if w.sense.focusable { + ctx.memory.interested_in_focus(w.id); + } + }); + } else { + // Don't remember invisible widgets } - self.interact_with_hovered( - layer_id, + if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() { + // Not interested or allowed input: + self.memory_mut(|mem| mem.surrender_focus(w.id)); + } + + if w.sense.interactive() || w.sense.focusable { + self.check_for_id_clash(w.id, w.rect, "widget"); + } + + #[allow(clippy::let_and_return)] + let res = self.get_response(w); + + #[cfg(feature = "accesskit")] + if w.sense.focusable { + // Make sure anything that can receive focus has an AccessKit node. + // TODO(mwcampbell): For nodes that are filled from widget info, + // some information is written to the node twice. + self.accesskit_node_builder(w.id, |builder| res.fill_accesskit_node_common(builder)); + } + + res + } + + /// Read the response of some widget, which may be called _before_ creating the widget (!). + /// + /// This is because widget interaction happens at the start of the frame, using the previous frame's widgets. + /// + /// If the widget was not visible the previous frame (or this frame), this will return `None`. + pub fn read_response(&self, id: Id) -> Option { + self.write(|ctx| { + let viewport = ctx.viewport(); + viewport + .widgets_this_frame + .by_id + .get(&id) + .or_else(|| viewport.widgets_prev_frame.by_id.get(&id)) + .copied() + }) + .map(|widget_rect| self.get_response(widget_rect)) + } + + /// Returns `true` if the widget with the given `Id` contains the pointer. + #[deprecated = "Use Response.contains_pointer or Context::read_response instead"] + pub fn widget_contains_pointer(&self, id: Id) -> bool { + self.read_response(id) + .map_or(false, |response| response.contains_pointer) + } + + /// Do all interaction for an existing widget, without (re-)registering it. + fn get_response(&self, widget_rect: WidgetRect) -> Response { + let WidgetRect { id, + layer_id, rect, interact_rect, sense, enabled, - contains_pointer, - ) - } + } = widget_rect; + + let highlighted = self.frame_state(|fs| fs.highlight_this_frame.contains(&id)); - /// You specify if a thing is hovered, and the function gives a [`Response`]. - #[allow(clippy::too_many_arguments)] - pub(crate) fn interact_with_hovered( - &self, - layer_id: LayerId, - id: Id, - rect: Rect, - interact_rect: Rect, - sense: Sense, - enabled: bool, - contains_pointer: bool, - ) -> Response { - // This is the start - we'll fill in the fields below: let mut res = Response { ctx: self.clone(), layer_id, @@ -1086,65 +1211,32 @@ impl Context { interact_rect, sense, enabled, - contains_pointer, - hovered: contains_pointer && enabled, - highlighted: self.frame_state(|fs| fs.highlight_this_frame.contains(&id)), + contains_pointer: false, + hovered: false, + highlighted, clicked: Default::default(), double_clicked: Default::default(), triple_clicked: Default::default(), drag_started: false, dragged: false, - drag_released: false, + drag_stopped: false, is_pointer_button_down_on: false, interact_pointer_pos: None, - changed: false, // must be set by the widget itself + changed: false, }; - if !enabled || !sense.focusable || !layer_id.allow_interaction() { - // Not interested or allowed input: - self.memory_mut(|mem| mem.surrender_focus(id)); - } - - if sense.interactive() || sense.focusable { - self.check_for_id_clash(id, rect, "widget"); - } - - #[cfg(feature = "accesskit")] - if sense.focusable { - // Make sure anything that can receive focus has an AccessKit node. - // TODO(mwcampbell): For nodes that are filled from widget info, - // some information is written to the node twice. - self.accesskit_node_builder(id, |builder| res.fill_accesskit_node_common(builder)); - } - let clicked_elsewhere = res.clicked_elsewhere(); + self.write(|ctx| { let viewport = ctx.viewports.entry(ctx.viewport_id()).or_default(); - // We need to remember this widget. - // `widget_contains_pointer` also does this, but in case of e.g. `Response::interact`, - // that won't be called. - // We add all widgets here, even non-interactive ones, - // because we need this list not only for checking for blocking widgets, - // but also to know when we have reached the widget we are checking for cover. - viewport.layer_rects_this_frame.insert( - layer_id, - WidgetRect { - id, - interact_rect, - sense, - }, - ); + res.contains_pointer = viewport.interact_widgets.contains_pointer.contains(&id); let input = &viewport.input; let memory = &mut ctx.memory; - if sense.focusable { - memory.interested_in_focus(id); - } - if sense.click - && memory.has_focus(res.id) + && memory.has_focus(id) && (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter)) { // Space/enter works like a primary click for e.g. selected buttons @@ -1152,99 +1244,42 @@ impl Context { } #[cfg(feature = "accesskit")] - if sense.click && input.has_accesskit_action_request(res.id, accesskit::Action::Default) - { + if sense.click && input.has_accesskit_action_request(id, accesskit::Action::Default) { res.clicked[PointerButton::Primary as usize] = true; } - if sense.click || sense.drag { - let interaction = memory.interaction_mut(); - - interaction.click_interest |= contains_pointer && sense.click; - interaction.drag_interest |= contains_pointer && sense.drag; - - res.is_pointer_button_down_on = - interaction.click_id == Some(id) || interaction.drag_id == Some(id); - - if sense.click && sense.drag { - // This widget is sensitive to both clicks and drags. - // When the mouse first is pressed, it could be either, - // so we postpone the decision until we know. - res.dragged = - interaction.drag_id == Some(id) && input.pointer.is_decidedly_dragging(); - res.drag_started = res.dragged && input.pointer.started_decidedly_dragging; - } else if sense.drag { - // We are just sensitive to drags, so we can mark ourself as dragged right away: - res.dragged = interaction.drag_id == Some(id); - // res.drag_started will be filled below if applicable - } + let interaction = memory.interaction(); - for pointer_event in &input.pointer.pointer_events { - match pointer_event { - PointerEvent::Moved(_) => {} - - PointerEvent::Pressed { .. } => { - if contains_pointer { - let interaction = memory.interaction_mut(); - - if sense.click && interaction.click_id.is_none() { - // potential start of a click - interaction.click_id = Some(id); - res.is_pointer_button_down_on = true; - } - - // HACK: windows have low priority on dragging. - // This is so that if you drag a slider in a window, - // the slider will steal the drag away from the window. - // This is needed because we do window interaction first (to prevent frame delay), - // and then do content layout. - if sense.drag - && (interaction.drag_id.is_none() || interaction.drag_is_window) - { - // potential start of a drag - interaction.drag_id = Some(id); - interaction.drag_is_window = false; - memory.set_window_interaction(None); // HACK: stop moving windows (if any) - - res.is_pointer_button_down_on = true; - - // Again, only if we are ONLY sensitive to drags can we decide that this is a drag now. - if sense.click { - res.dragged = false; - res.drag_started = false; - } else { - res.dragged = true; - res.drag_started = true; - } - } - } - } + res.is_pointer_button_down_on = interaction.potential_click_id == Some(id) + || interaction.potential_drag_id == Some(id); - PointerEvent::Released { click, button } => { - res.drag_released = res.dragged; - res.dragged = false; - - if sense.click && res.hovered && res.is_pointer_button_down_on { - if let Some(click) = click { - let clicked = res.hovered && res.is_pointer_button_down_on; - res.clicked[*button as usize] = clicked; - res.double_clicked[*button as usize] = - clicked && click.is_double(); - res.triple_clicked[*button as usize] = - clicked && click.is_triple(); - } - } + if res.enabled { + res.hovered = viewport.interact_widgets.hovered.contains(&id); + res.dragged = Some(id) == viewport.interact_widgets.dragged; + res.drag_started = Some(id) == viewport.interact_widgets.drag_started; + res.drag_stopped = Some(id) == viewport.interact_widgets.drag_stopped; + } + + let clicked = Some(id) == viewport.interact_widgets.clicked; - res.is_pointer_button_down_on = false; + for pointer_event in &input.pointer.pointer_events { + if let PointerEvent::Released { click, button } = pointer_event { + if sense.click && clicked { + if let Some(click) = click { + res.clicked[*button as usize] = true; + res.double_clicked[*button as usize] = click.is_double(); + res.triple_clicked[*button as usize] = click.is_triple(); } } + + res.is_pointer_button_down_on = false; + res.dragged = false; } } // is_pointer_button_down_on is false when released, but we want interact_pointer_pos // to still work. - let clicked = res.clicked.iter().any(|c| *c); - let is_interacted_with = res.is_pointer_button_down_on || clicked || res.drag_released; + let is_interacted_with = res.is_pointer_button_down_on || clicked || res.drag_stopped; if is_interacted_with { res.interact_pointer_pos = input.pointer.interact_pos(); if let (Some(transform), Some(pos)) = ( @@ -1260,11 +1295,11 @@ impl Context { res.hovered = false; } - if memory.has_focus(res.id) && clicked_elsewhere { + if clicked_elsewhere && memory.has_focus(id) { memory.surrender_focus(id); } - if res.dragged() && !memory.has_focus(res.id) { + if res.dragged() && !memory.has_focus(id) { // e.g.: remove focus from a widget when you drag something else memory.stop_text_input(); } @@ -1872,8 +1907,30 @@ impl Context { self.read(|ctx| ctx.plugins.clone()).on_end_frame(self); - if self.options(|o| o.debug_paint_interactive_widgets) { - let rects = self.write(|ctx| ctx.viewport().layer_rects_this_frame.clone()); + #[cfg(debug_assertions)] + self.debug_painting(); + + self.write(|ctx| ctx.end_frame()) + } + + #[cfg(debug_assertions)] + fn debug_painting(&self) { + let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| { + let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); + painter.debug_rect(widget.interact_rect, color, text); + }; + + let paint_widget_id = |id: Id, text: &str, color: Color32| { + if let Some(widget) = + self.write(|ctx| ctx.viewport().widgets_this_frame.by_id.get(&id).cloned()) + { + paint_widget(&widget, text, color); + } + }; + + if self.style().debug.show_interactive_widgets { + // Show all interactive widgets: + let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone()); for (layer_id, rects) in rects.by_layer { let painter = Painter::new(self.clone(), layer_id, Rect::EVERYTHING); for rect in rects { @@ -1885,15 +1942,65 @@ impl Context { } else if rect.sense.drag { (Color32::from_rgb(0, 0, 0x88), "drag") } else { - (Color32::from_rgb(0, 0, 0x88), "hover") + continue; + // (Color32::from_rgb(0, 0, 0x88), "hover") }; painter.debug_rect(rect.interact_rect, color, text); } } } + + // Show the ones actually interacted with: + { + let interact_widgets = self.write(|ctx| ctx.viewport().interact_widgets.clone()); + let InteractionSnapshot { + clicked, + drag_started: _, + dragged, + drag_stopped: _, + contains_pointer, + hovered, + } = interact_widgets; + + if false { + for widget in contains_pointer { + paint_widget_id(widget, "contains_pointer", Color32::BLUE); + } + } + if true { + for widget in hovered { + paint_widget_id(widget, "hovered", Color32::WHITE); + } + } + for &widget in &clicked { + paint_widget_id(widget, "clicked", Color32::RED); + } + for &widget in &dragged { + paint_widget_id(widget, "dragged", Color32::GREEN); + } + } } - self.write(|ctx| ctx.end_frame()) + if self.style().debug.show_widget_hits { + let hits = self.write(|ctx| ctx.viewport().hits.clone()); + let WidgetHits { + contains_pointer, + click, + drag, + } = hits; + + if false { + for widget in &contains_pointer { + paint_widget(widget, "contains_pointer", Color32::BLUE); + } + } + for widget in &click { + paint_widget(widget, "click", Color32::RED); + } + for widget in &drag { + paint_widget(widget, "drag", Color32::GREEN); + } + } } } @@ -1975,16 +2082,16 @@ impl ContextImpl { { if self.memory.options.repaint_on_widget_change { crate::profile_function!("compare-widget-rects"); - if viewport.layer_rects_prev_frame != viewport.layer_rects_this_frame { + if viewport.widgets_prev_frame != viewport.widgets_this_frame { repaint_needed = true; // Some widget has moved } } std::mem::swap( - &mut viewport.layer_rects_prev_frame, - &mut viewport.layer_rects_this_frame, + &mut viewport.widgets_prev_frame, + &mut viewport.widgets_this_frame, ); - viewport.layer_rects_this_frame.clear(); + viewport.widgets_this_frame.clear(); } if repaint_needed || viewport.input.wants_repaint() { @@ -2376,117 +2483,6 @@ impl Context { true } - /// Does the given widget contain the mouse pointer? - /// - /// Will return false if some other area is covering the given layer. - /// - /// If another widget is covering us and is listening for the same input (click and/or drag), - /// this will return false. - /// - /// The given rectangle is assumed to have been clipped by its parent clip rect. - /// - /// See also [`Response::contains_pointer`]. - pub fn widget_contains_pointer( - &self, - layer_id: LayerId, - id: Id, - sense: Sense, - interact_rect: Rect, - ) -> bool { - if !interact_rect.is_positive() { - return false; // don't even remember this widget - } - - let contains_pointer = self.rect_contains_pointer(layer_id, interact_rect); - - let mut blocking_widget = None; - - self.write(|ctx| { - let transform = ctx - .memory - .layer_transforms - .get(&layer_id) - .cloned() - .unwrap_or_default(); - let viewport = ctx.viewport(); - - // We add all widgets here, even non-interactive ones, - // because we need this list not only for checking for blocking widgets, - // but also to know when we have reached the widget we are checking for cover. - viewport.layer_rects_this_frame.insert( - layer_id, - WidgetRect { - id, - interact_rect, - sense, - }, - ); - - // Check if any other widget is covering us. - // Whichever widget is added LAST (=on top) gets the input. - if contains_pointer { - let pointer_pos = viewport.input.pointer.interact_pos(); - if let Some(pointer_pos) = pointer_pos { - // Apply the inverse transformation of this layer to the pointer pos. - let pointer_pos = transform.inverse() * pointer_pos; - if let Some(rects) = viewport.layer_rects_prev_frame.by_layer.get(&layer_id) { - // Iterate backwards, i.e. topmost widgets first. - for blocking in rects.iter().rev() { - if blocking.id == id { - // We've checked all widgets there were added after this one last frame, - // which means there are no widgets covering us. - break; - } - if !blocking.interact_rect.contains(pointer_pos) { - continue; - } - if sense.interactive() && !blocking.sense.interactive() { - // Only interactive widgets can block other interactive widgets. - continue; - } - - // The `prev` widget is covering us - do we care? - // We don't want a click-only button to block drag-events to a `ScrollArea`: - - let sense_only_drags = sense.drag && !sense.click; - if sense_only_drags && !blocking.sense.drag { - continue; - } - let sense_only_clicks = sense.click && !sense.drag; - if sense_only_clicks && !blocking.sense.click { - continue; - } - - if blocking.sense.interactive() { - // Another widget is covering us at the pointer position - blocking_widget = Some(blocking.interact_rect); - break; - } - } - } - } - } - }); - - #[cfg(debug_assertions)] - if let Some(blocking_rect) = blocking_widget { - if sense.interactive() && self.memory(|m| m.options.style.debug.show_blocking_widget) { - Self::layer_painter(self, LayerId::debug()).debug_rect( - interact_rect, - Color32::GREEN, - "Covered", - ); - Self::layer_painter(self, LayerId::debug()).debug_rect( - blocking_rect, - Color32::LIGHT_BLUE, - "On top", - ); - } - } - - contains_pointer && blocking_widget.is_none() - } - // --------------------------------------------------------------------- /// Whether or not to debug widget layout on hover. @@ -2669,6 +2665,13 @@ impl Context { crate::text_selection::LabelSelectionState::load(ui.ctx()) )); }); + + CollapsingHeader::new("Interaction") + .default_open(false) + .show(ui, |ui| { + let interact_widgets = self.write(|ctx| ctx.viewport().interact_widgets.clone()); + interact_widgets.ui(ui); + }); } /// Show stats about the allocated textures. @@ -3356,6 +3359,85 @@ impl Context { } } +/// ## Interaction +impl Context { + /// Read you what widgets are currently being interacted with. + pub fn interaction_snapshot(&self, reader: impl FnOnce(&InteractionSnapshot) -> R) -> R { + self.write(|w| reader(&w.viewport().interact_widgets)) + } + + /// The widget currently being dragged, if any. + /// + /// For widgets that sense both clicks and drags, this will + /// not be set until the mouse cursor has moved a certain distance. + /// + /// NOTE: if the widget was released this frame, this will be `None`. + /// Use [`Self::drag_stopped_id`] instead. + pub fn dragged_id(&self) -> Option { + self.interaction_snapshot(|i| i.dragged) + } + + /// Is this specific widget being dragged? + /// + /// A widget that sense both clicks and drags is only marked as "dragged" + /// when the mouse has moved a bit + /// + /// See also: [`crate::Response::dragged`]. + pub fn is_being_dragged(&self, id: Id) -> bool { + self.dragged_id() == Some(id) + } + + /// This widget just started being dragged this frame. + /// + /// The same widget should also be found in [`Self::dragged_id`]. + pub fn drag_started_id(&self) -> Option { + self.interaction_snapshot(|i| i.drag_started) + } + + /// This widget was being dragged, but was released this frame + pub fn drag_stopped_id(&self) -> Option { + self.interaction_snapshot(|i| i.drag_stopped) + } + + /// Set which widget is being dragged. + pub fn set_dragged_id(&self, id: Id) { + self.write(|ctx| { + let vp = ctx.viewport(); + let i = &mut vp.interact_widgets; + if i.dragged != Some(id) { + i.drag_stopped = i.dragged.or(i.drag_stopped); + i.dragged = Some(id); + i.drag_started = Some(id); + } + + ctx.memory.interaction_mut().potential_drag_id = Some(id); + }); + } + + /// Stop dragging any widget. + pub fn stop_dragging(&self) { + self.write(|ctx| { + let vp = ctx.viewport(); + let i = &mut vp.interact_widgets; + if i.dragged.is_some() { + i.drag_stopped = i.dragged; + i.dragged = None; + } + + ctx.memory.interaction_mut().potential_drag_id = None; + }); + } + + /// Is something else being dragged? + /// + /// Returns true if we are dragging something, but not the given widget. + #[inline(always)] + pub fn dragging_something_else(&self, not_this: Id) -> bool { + let dragged = self.dragged_id(); + dragged.is_some() && dragged != Some(not_this) + } +} + #[test] fn context_impl_send_sync() { fn assert_send_sync() {} diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs new file mode 100644 index 000000000000..fe8b085a3544 --- /dev/null +++ b/crates/egui/src/hit_test.rs @@ -0,0 +1,422 @@ +use ahash::HashMap; + +use emath::TSTransform; + +use crate::*; + +/// Result of a hit-test against [`WidgetRects`]. +/// +/// Answers the question "what is under the mouse pointer?". +/// +/// Note that this doesn't care if the mouse button is pressed or not, +/// or if we're currently already dragging something. +#[derive(Clone, Debug, Default)] +pub struct WidgetHits { + /// All widgets that contains the pointer, back-to-front. + /// + /// i.e. both a Window and the button in it can contain the pointer. + /// + /// Some of these may be widgets in a layer below the top-most layer. + pub contains_pointer: Vec, + + /// If the user would start a clicking now, this is what would be clicked. + /// + /// This is the top one under the pointer, or closest one of the top-most. + pub click: Option, + + /// If the user would start a dragging now, this is what would be dragged. + /// + /// This is the top one under the pointer, or closest one of the top-most. + pub drag: Option, +} + +/// Find the top or closest widgets to the given position, +/// none which is closer than `search_radius`. +pub fn hit_test( + widgets: &WidgetRects, + layer_order: &[LayerId], + layer_transforms: &HashMap, + pos: Pos2, + search_radius: f32, +) -> WidgetHits { + crate::profile_function!(); + + let search_radius_sq = search_radius * search_radius; + + // Transform the position into the local coordinate space of each layer: + let pos_in_layers: HashMap = layer_transforms + .iter() + .map(|(layer_id, t)| (*layer_id, t.inverse() * pos)) + .collect(); + + let mut closest_dist_sq = f32::INFINITY; + let mut closest_hit = None; + + // First pass: find the few widgets close to the given position, sorted back-to-front. + let mut close: Vec = layer_order + .iter() + .filter(|layer| layer.order.allow_interaction()) + .filter_map(|layer_id| widgets.by_layer.get(layer_id)) + .flatten() + .filter(|&w| { + let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos); + let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer); + + // In tie, pick last = topmost. + if dist_sq <= closest_dist_sq { + closest_dist_sq = dist_sq; + closest_hit = Some(w); + } + + dist_sq <= search_radius_sq + }) + .copied() + .collect(); + + // We need to pick one single layer for the interaction. + if let Some(closest_hit) = closest_hit { + // Select the top layer, and ignore widgets in any other layer: + let top_layer = closest_hit.layer_id; + close.retain(|w| w.layer_id == top_layer); + + let pos_in_layer = pos_in_layers.get(&top_layer).copied().unwrap_or(pos); + let hits = hit_test_on_close(&close, pos_in_layer); + + if let Some(drag) = hits.drag { + debug_assert!(drag.sense.drag); + } + if let Some(click) = hits.click { + debug_assert!(click.sense.click); + } + + hits + } else { + // No close widgets. + Default::default() + } +} + +fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { + #![allow(clippy::collapsible_else_if)] + + // Only those widgets directly under the `pos`. + let hits: Vec = close + .iter() + .filter(|widget| widget.interact_rect.contains(pos)) + .copied() + .collect(); + + let hit_click = hits.iter().copied().filter(|w| w.sense.click).last(); + let hit_drag = hits.iter().copied().filter(|w| w.sense.drag).last(); + + match (hit_click, hit_drag) { + (None, None) => { + // No direct hit on anything. Find the closest interactive widget. + + let closest = find_closest( + close + .iter() + .copied() + .filter(|w| w.sense.click || w.sense.drag), + pos, + ); + + if let Some(closest) = closest { + WidgetHits { + contains_pointer: hits, + click: closest.sense.click.then_some(closest), + drag: closest.sense.drag.then_some(closest), + } + } else { + // Found nothing + WidgetHits { + contains_pointer: hits, + click: None, + drag: None, + } + } + } + + (None, Some(hit_drag)) => { + // We have a perfect hit on a drag, but not on click. + + // We have a direct hit on something that implements drag. + // This could be a big background thing, like a `ScrollArea` background, + // or a moveable window. + // It could also be something small, like a slider, or panel resize handle. + + let closest_click = find_closest(close.iter().copied().filter(|w| w.sense.click), pos); + if let Some(closest_click) = closest_click { + if closest_click.sense.drag { + // We have something close that sense both clicks and drag. + // Should we use it over the direct drag-hit? + if hit_drag + .interact_rect + .contains_rect(closest_click.interact_rect) + { + // This is a smaller thing on a big background - help the user hit it, + // and ignore the big drag background. + WidgetHits { + contains_pointer: hits, + click: Some(closest_click), + drag: Some(closest_click), + } + } else { + // The drag wiudth is separate from the click wiudth, + // so return only the drag widget + WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(hit_drag), + } + } + } else { + // These is a close pure-click widget. + // However, we should be careful to only return two different widgets + // when it is absolutely not going to confuse the user. + if hit_drag + .interact_rect + .contains_rect(closest_click.interact_rect) + { + // The drag widget is a big background thing (scroll area), + // so returning a separate click widget should not be confusing + WidgetHits { + contains_pointer: hits, + click: Some(closest_click), + drag: Some(hit_drag), + } + } else { + // The two widgets are just two normal small widgets close to each other. + // Highlighting both would be very confusing. + WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(hit_drag), + } + } + } + } else { + // No close clicks. + // Maybe there is a close drag widget, that is a smaller + // widget floating on top of a big background? + // If so, it would be nice to help the user click that. + let closest_drag = find_closest( + close + .iter() + .copied() + .filter(|w| w.sense.drag && w.id != hit_drag.id), + pos, + ); + + if let Some(closest_drag) = closest_drag { + if hit_drag + .interact_rect + .contains_rect(closest_drag.interact_rect) + { + // `hit_drag` is a big background thing and `closest_drag` is something small on top of it. + // Be helpful and return the small things: + return WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(closest_drag), + }; + } + } + + WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(hit_drag), + } + } + } + + (Some(hit_click), None) => { + // We have a perfect hit on a click-widget, but not on a drag-widget. + // + // Note that we don't look for a close drag widget in this case, + // because I can't think of a case where that would be helpful. + // This is in contrast with the opposite case, + // where when hovering directly over a drag-widget (like a big ScrollArea), + // we look for close click-widgets (e.g. buttons). + // This is because big background drag-widgets (ScrollArea, Window) are common, + // but bit clickable things aren't. + // Even if they were, I think it would be confusing for a user if clicking + // a drag-only widget would click something _behind_ it. + + WidgetHits { + contains_pointer: hits, + click: Some(hit_click), + drag: None, + } + } + + (Some(hit_click), Some(hit_drag)) => { + // We have a perfect hit on both click and drag. Which is the topmost? + let click_idx = hits.iter().position(|w| *w == hit_click).unwrap(); + let drag_idx = hits.iter().position(|w| *w == hit_drag).unwrap(); + + let click_is_on_top_of_drag = drag_idx < click_idx; + if click_is_on_top_of_drag { + if hit_click.sense.drag { + // The top thing senses both clicks and drags. + WidgetHits { + contains_pointer: hits, + click: Some(hit_click), + drag: Some(hit_click), + } + } else { + // They are interested in different things, + // and click is on top. Report both hits, + // e.g. the top Button and the ScrollArea behind it. + WidgetHits { + contains_pointer: hits, + click: Some(hit_click), + drag: Some(hit_drag), + } + } + } else { + if hit_drag.sense.click { + // The top thing senses both clicks and drags. + WidgetHits { + contains_pointer: hits, + click: Some(hit_drag), + drag: Some(hit_drag), + } + } else { + // The top things senses only drags, + // so we ignore the click-widget, because it would be confusing + // if clicking a drag-widget would actually click something else below it. + WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(hit_drag), + } + } + } + } + } +} + +fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option { + let mut closest = None; + let mut closest_dist_sq = f32::INFINITY; + for widget in widgets { + let dist_sq = widget.interact_rect.distance_sq_to_pos(pos); + + // In case of a tie, take the last one = the one on top. + if dist_sq <= closest_dist_sq { + closest_dist_sq = dist_sq; + closest = Some(widget); + } + } + + closest +} + +#[cfg(test)] +mod tests { + use super::*; + + fn wr(id: Id, sense: Sense, rect: Rect) -> WidgetRect { + WidgetRect { + id, + layer_id: LayerId::background(), + rect, + interact_rect: rect, + sense, + enabled: true, + } + } + + #[test] + fn buttons_on_window() { + let widgets = vec![ + wr( + Id::new("bg-area"), + Sense::drag(), + Rect::from_min_size(pos2(0.0, 0.0), vec2(100.0, 100.0)), + ), + wr( + Id::new("click"), + Sense::click(), + Rect::from_min_size(pos2(10.0, 10.0), vec2(10.0, 10.0)), + ), + wr( + Id::new("click-and-drag"), + Sense::click_and_drag(), + Rect::from_min_size(pos2(100.0, 10.0), vec2(10.0, 10.0)), + ), + ]; + + // Perfect hit: + let hits = hit_test_on_close(&widgets, pos2(15.0, 15.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click")); + assert_eq!(hits.drag.unwrap().id, Id::new("bg-area")); + + // Close hit: + let hits = hit_test_on_close(&widgets, pos2(5.0, 5.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click")); + assert_eq!(hits.drag.unwrap().id, Id::new("bg-area")); + + // Perfect hit: + let hits = hit_test_on_close(&widgets, pos2(105.0, 15.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag")); + assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag")); + + // Close hit - should still ignore the drag-background so as not to confuse the userr: + let hits = hit_test_on_close(&widgets, pos2(105.0, 5.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag")); + assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag")); + } + + #[test] + fn thin_resize_handle_next_to_label() { + let widgets = vec![ + wr( + Id::new("bg-area"), + Sense::drag(), + Rect::from_min_size(pos2(0.0, 0.0), vec2(100.0, 100.0)), + ), + wr( + Id::new("bg-left-label"), + Sense::click_and_drag(), + Rect::from_min_size(pos2(0.0, 0.0), vec2(40.0, 100.0)), + ), + wr( + Id::new("thin-drag-handle"), + Sense::drag(), + Rect::from_min_size(pos2(30.0, 0.0), vec2(70.0, 100.0)), + ), + wr( + Id::new("fg-right-label"), + Sense::click_and_drag(), + Rect::from_min_size(pos2(60.0, 0.0), vec2(50.0, 100.0)), + ), + ]; + + for (i, w) in widgets.iter().enumerate() { + eprintln!("Widget {i}: {:?}", w.id); + } + + // In the middle of the bg-left-label: + let hits = hit_test_on_close(&widgets, pos2(25.0, 50.0)); + assert_eq!(hits.click.unwrap().id, Id::new("bg-left-label")); + assert_eq!(hits.drag.unwrap().id, Id::new("bg-left-label")); + + // On both the left click-and-drag and thin handle, but the thin handle is on top and should win: + let hits = hit_test_on_close(&widgets, pos2(35.0, 50.0)); + assert_eq!(hits.click, None); + assert_eq!(hits.drag.unwrap().id, Id::new("thin-drag-handle")); + + // Only on the thin-drag-handle: + let hits = hit_test_on_close(&widgets, pos2(50.0, 50.0)); + assert_eq!(hits.click, None); + assert_eq!(hits.drag.unwrap().id, Id::new("thin-drag-handle")); + + // On both the thin handle and right label. The label is on top and should win + let hits = hit_test_on_close(&widgets, pos2(65.0, 50.0)); + assert_eq!(hits.click.unwrap().id, Id::new("fg-right-label")); + assert_eq!(hits.drag.unwrap().id, Id::new("fg-right-label")); + } +} diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs new file mode 100644 index 000000000000..f1a3e2a32475 --- /dev/null +++ b/crates/egui/src/interaction.rs @@ -0,0 +1,230 @@ +//! How mouse and touch interzcts with widgets. + +use crate::*; + +use self::{hit_test::WidgetHits, id::IdSet, input_state::PointerEvent, memory::InteractionState}; + +/// Calculated at the start of each frame +/// based on: +/// * Widget rects from precious frame +/// * Mouse/touch input +/// * Current [`InteractionState`]. +#[derive(Clone, Default)] +pub struct InteractionSnapshot { + /// The widget that got clicked this frame. + pub clicked: Option, + + /// Drag started on this widget this frame. + /// + /// This will also be found in `dragged` this frame. + pub drag_started: Option, + + /// This widget is being dragged this frame. + /// + /// Set the same frame a drag starts, + /// but unset the frame a drag ends. + /// + /// NOTE: this may not have a corresponding [`WidgetRect`], + /// if this for instance is a drag-and-drop widget which + /// isn't painted whilst being dragged + pub dragged: Option, + + /// This widget was let go this frame, + /// after having been dragged. + /// + /// The widget will not be found in [`Self::dragged`] this frame. + pub drag_stopped: Option, + + /// A small set of widgets (usually 0-1) that the pointer is hovering over. + /// + /// Show these widgets as highlighted, if they are interactive. + /// + /// While dragging or clicking something, nothing else is hovered. + /// + /// Use [`Self::contains_pointer`] to find a drop-zone for drag-and-drop. + pub hovered: IdSet, + + /// All widgets that contain the pointer this frame, + /// regardless if the user is currently clicking or dragging. + /// + /// This is usually a larger set than [`Self::hovered`], + /// and can be used for e.g. drag-and-drop zones. + pub contains_pointer: IdSet, +} + +impl InteractionSnapshot { + pub fn ui(&self, ui: &mut crate::Ui) { + let Self { + clicked, + drag_started, + dragged, + drag_stopped, + hovered, + contains_pointer, + } = self; + + fn id_ui<'a>(ui: &mut crate::Ui, widgets: impl IntoIterator) { + for id in widgets { + ui.label(id.short_debug_format()); + } + } + + crate::Grid::new("interaction").show(ui, |ui| { + ui.label("clicked"); + id_ui(ui, clicked); + ui.end_row(); + + ui.label("drag_started"); + id_ui(ui, drag_started); + ui.end_row(); + + ui.label("dragged"); + id_ui(ui, dragged); + ui.end_row(); + + ui.label("drag_stopped"); + id_ui(ui, drag_stopped); + ui.end_row(); + + ui.label("hovered"); + id_ui(ui, hovered); + ui.end_row(); + + ui.label("contains_pointer"); + id_ui(ui, contains_pointer); + ui.end_row(); + }); + } +} + +pub(crate) fn interact( + prev_snapshot: &InteractionSnapshot, + widgets: &WidgetRects, + hits: &WidgetHits, + input: &InputState, + interaction: &mut InteractionState, +) -> InteractionSnapshot { + crate::profile_function!(); + + if let Some(id) = interaction.potential_click_id { + if !widgets.by_id.contains_key(&id) { + // The widget we were interested in clicking is gone. + interaction.potential_click_id = None; + } + } + if let Some(id) = interaction.potential_drag_id { + if !widgets.by_id.contains_key(&id) { + // The widget we were interested in dragging is gone. + // This is fine! This could be drag-and-drop, + // and the widget being dragged is now "in the air" and thus + // not registered in the new frame. + } + } + + let mut clicked = None; + let mut dragged = prev_snapshot.dragged; + + // Note: in the current code a press-release in the same frame is NOT considered a drag. + for pointer_event in &input.pointer.pointer_events { + match pointer_event { + PointerEvent::Moved(_) => {} + + PointerEvent::Pressed { .. } => { + // Maybe new click? + if interaction.potential_click_id.is_none() { + interaction.potential_click_id = hits.click.map(|w| w.id); + } + + // Maybe new drag? + if interaction.potential_drag_id.is_none() { + interaction.potential_drag_id = hits.drag.map(|w| w.id); + } + } + + PointerEvent::Released { click, button: _ } => { + if click.is_some() { + if let Some(widget) = interaction + .potential_click_id + .and_then(|id| widgets.by_id.get(&id)) + { + clicked = Some(widget.id); + } + } + + interaction.potential_drag_id = None; + interaction.potential_click_id = None; + dragged = None; + } + } + } + + if dragged.is_none() { + // Check if we started dragging something new: + if let Some(widget) = interaction + .potential_drag_id + .and_then(|id| widgets.by_id.get(&id)) + { + let is_dragged = if widget.sense.click && widget.sense.drag { + // This widget is sensitive to both clicks and drags. + // When the mouse first is pressed, it could be either, + // so we postpone the decision until we know. + input.pointer.is_decidedly_dragging() + } else { + // This widget is just sensitive to drags, so we can mark it as dragged right away: + widget.sense.drag + }; + + if is_dragged { + dragged = Some(widget.id); + } + } + } + + // ------------------------------------------------------------------------ + + let drag_changed = dragged != prev_snapshot.dragged; + let drag_stopped = drag_changed.then_some(prev_snapshot.dragged).flatten(); + let drag_started = drag_changed.then_some(dragged).flatten(); + + // if let Some(drag_started) = drag_started { + // eprintln!( + // "Started dragging {} {:?}", + // drag_started.id.short_debug_format(), + // drag_started.rect + // ); + // } + + let contains_pointer: IdSet = hits + .contains_pointer + .iter() + .chain(&hits.click) + .chain(&hits.drag) + .map(|w| w.id) + .collect(); + + let hovered = if clicked.is_some() || dragged.is_some() { + // If currently clicking or dragging, nothing else is hovered. + clicked.iter().chain(&dragged).copied().collect() + } else if hits.click.is_some() || hits.drag.is_some() { + // We are hovering over an interactive widget or two. + hits.click.iter().chain(&hits.drag).map(|w| w.id).collect() + } else { + // Whatever is topmost is what we are hovering. + // TODO: consider handle hovering over multiple top-most widgets? + // TODO: allow hovering close widgets? + hits.contains_pointer + .last() + .map(|w| w.id) + .into_iter() + .collect() + }; + + InteractionSnapshot { + clicked, + drag_started, + dragged, + drag_stopped, + contains_pointer, + hovered, + } +} diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 43b3a88b81fa..240ff6974a58 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -189,14 +189,17 @@ impl Widget for &mut epaint::TessellationOptions { } } -impl Widget for &memory::Interaction { +impl Widget for &memory::InteractionState { fn ui(self, ui: &mut Ui) -> Response { + let memory::InteractionState { + potential_click_id, + potential_drag_id, + focus: _, + } = self; + ui.vertical(|ui| { - ui.label(format!("click_id: {:?}", self.click_id)); - ui.label(format!("drag_id: {:?}", self.drag_id)); - ui.label(format!("drag_is_window: {:?}", self.drag_is_window)); - ui.label(format!("click_interest: {:?}", self.click_interest)); - ui.label(format!("drag_interest: {:?}", self.drag_interest)); + ui.label(format!("potential_click_id: {potential_click_id:?}")); + ui.label(format!("potential_drag_id: {potential_drag_id:?}")); }) .response } diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index ad931885ea8c..c715b859e6c1 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -267,6 +267,37 @@ //! } //! ``` //! +//! +//! ## Widget interaction +//! Each widget has a [`Sense`], which defines whether or not the widget +//! is sensitive to clickicking and/or drags. +//! +//! For instance, a [`Button`] only has a [`Sense::click`] (by default). +//! This means if you drag a button it will not respond with [`Response::dragged`]. +//! Instead, the drag will continue through the button to the first +//! widget behind it that is sensitive to dragging, which for instance could be +//! a [`ScrollArea`]. This lets you scroll by dragging a scroll area (important +//! on touch screens), just as long as you don't drag on a widget that is sensitive +//! to drags (e.g. a [`Slider`]). +//! +//! When widgets overlap it is the last added one +//! that is considered to be on top and which will get input priority. +//! +//! The widget interaction logic is run at the _start_ of each frame, +//! based on the output from the previous frame. +//! This means that when a new widget shows up you cannot click it in the same +//! frame (i.e. in the same fraction of a second), but unless the user +//! is spider-man, they wouldn't be fast enough to do so anyways. +//! +//! By running the interaction code early, egui can actually +//! tell you if a widget is being interacted with _before_ you add it, +//! as long as you know its [`Id`] before-hand (e.g. using [`Ui::next_auto_id`]), +//! by calling [`Context::read_response`]. +//! This can be useful in some circumstances in order to style a widget, +//! or to respond to interactions before adding the widget +//! (perhaps on top of other widgets). +//! +//! //! ## Auto-sizing panels and windows //! In egui, all panels and windows auto-shrink to fit the content. //! If the window or panel is also resizable, this can lead to a weird behavior @@ -352,8 +383,10 @@ mod drag_and_drop; mod frame_state; pub(crate) mod grid; pub mod gui_zoom; +mod hit_test; mod id; mod input_state; +mod interaction; pub mod introspection; pub mod layers; mod layout; diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index f222b300b7a9..fcfe0accb421 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -4,10 +4,8 @@ use ahash::HashMap; use epaint::emath::TSTransform; use crate::{ - area, vec2, - window::{self, WindowInteraction}, - EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, Rect, Style, Vec2, ViewportId, - ViewportIdMap, ViewportIdSet, + area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, Rect, Style, Vec2, + ViewportId, ViewportIdMap, ViewportIdSet, }; // ---------------------------------------------------------------------------- @@ -96,10 +94,7 @@ pub struct Memory { areas: ViewportIdMap, #[cfg_attr(feature = "persistence", serde(skip))] - pub(crate) interactions: ViewportIdMap, - - #[cfg_attr(feature = "persistence", serde(skip))] - window_interactions: ViewportIdMap, + pub(crate) interactions: ViewportIdMap, } impl Default for Memory { @@ -111,7 +106,6 @@ impl Default for Memory { new_font_definitions: Default::default(), interactions: Default::default(), viewport_id: Default::default(), - window_interactions: Default::default(), areas: Default::default(), layer_transforms: Default::default(), popup: Default::default(), @@ -161,6 +155,8 @@ impl FocusDirection { // ---------------------------------------------------------------------------- /// Some global options that you can read and write. +/// +/// See also [`crate::style::DebugOptions`]. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] @@ -221,9 +217,6 @@ pub struct Options { /// /// By default this is `true` in debug builds. pub warn_on_id_clash: bool, - - /// If true, paint all interactive widgets in the order they were added to each layer. - pub debug_paint_interactive_widgets: bool, } impl Default for Options { @@ -237,7 +230,6 @@ impl Default for Options { screen_reader: false, preload_font_glyphs: true, warn_on_id_clash: cfg!(debug_assertions), - debug_paint_interactive_widgets: false, } } } @@ -254,7 +246,6 @@ impl Options { screen_reader: _, // needs to come from the integration preload_font_glyphs: _, warn_on_id_clash, - debug_paint_interactive_widgets, } = self; use crate::Widget as _; @@ -273,8 +264,6 @@ impl Options { ); ui.checkbox(warn_on_id_clash, "Warn if two widgets have the same Id"); - - ui.checkbox(debug_paint_interactive_widgets, "Debug interactive widgets"); }); use crate::containers::*; @@ -297,6 +286,9 @@ impl Options { // ---------------------------------------------------------------------------- +/// The state of the interaction in egui, +/// i.e. what is being dragged. +/// /// Say there is a button in a scroll area. /// If the user clicks the button, the button should click. /// If the user drags the button we should scroll the scroll area. @@ -305,9 +297,9 @@ impl Options { /// If the user releases the button without moving the mouse we register it as a click on `click_id`. /// If the cursor moves too much we clear the `click_id` and start passing move events to `drag_id`. #[derive(Clone, Debug, Default)] -pub(crate) struct Interaction { +pub(crate) struct InteractionState { /// A widget interested in clicks that has a mouse press on it. - pub click_id: Option, + pub potential_click_id: Option, /// A widget interested in drags that has a mouse press on it. /// @@ -315,24 +307,9 @@ pub(crate) struct Interaction { /// so the widget may not yet be marked as "dragged", /// as that can only happen after the mouse has moved a bit /// (at least if the widget is interesated in both clicks and drags). - pub drag_id: Option, + pub potential_drag_id: Option, pub focus: Focus, - - /// HACK: windows have low priority on dragging. - /// This is so that if you drag a slider in a window, - /// the slider will steal the drag away from the window. - /// This is needed because we do window interaction first (to prevent frame delay), - /// and then do content layout. - pub drag_is_window: bool, - - /// Any interest in catching clicks this frame? - /// Cleared to false at start of each frame. - pub click_interest: bool, - - /// Any interest in catching clicks this frame? - /// Cleared to false at start of each frame. - pub drag_interest: bool, } /// Keeps tracks of what widget has keyboard focus @@ -380,10 +357,10 @@ impl FocusWidget { } } -impl Interaction { +impl InteractionState { /// Are we currently clicking or dragging an egui widget? pub fn is_using_pointer(&self) -> bool { - self.click_id.is_some() || self.drag_id.is_some() + self.potential_click_id.is_some() || self.potential_drag_id.is_some() } fn begin_frame( @@ -391,17 +368,14 @@ impl Interaction { prev_input: &crate::input_state::InputState, new_input: &crate::data::input::RawInput, ) { - self.click_interest = false; - self.drag_interest = false; - if !prev_input.pointer.could_any_button_be_click() { - self.click_id = None; + self.potential_click_id = None; } if !prev_input.pointer.any_down() || prev_input.pointer.latest_pos().is_none() { // pointer button was not down last frame - self.click_id = None; - self.drag_id = None; + self.potential_click_id = None; + self.potential_drag_id = None; } self.focus.begin_frame(new_input); @@ -640,8 +614,6 @@ impl Memory { // Cleanup self.interactions.retain(|id, _| viewports.contains(id)); self.areas.retain(|id, _| viewports.contains(id)); - self.window_interactions - .retain(|id, _| viewports.contains(id)); self.viewport_id = new_input.viewport_id; self.interactions @@ -649,10 +621,6 @@ impl Memory { .or_default() .begin_frame(prev_input, new_input); self.areas.entry(self.viewport_id).or_default(); - - if !prev_input.pointer.any_down() { - self.window_interactions.remove(&self.viewport_id); - } } pub(crate) fn end_frame(&mut self, used_ids: &IdMap) { @@ -770,9 +738,10 @@ impl Memory { } /// Is any widget being dragged? + #[deprecated = "Use `Context::dragged_id` instead"] #[inline(always)] pub fn is_anything_being_dragged(&self) -> bool { - self.interaction().drag_id.is_some() + self.interaction().potential_drag_id.is_some() } /// Is this specific widget being dragged? @@ -781,9 +750,10 @@ impl Memory { /// /// A widget that sense both clicks and drags is only marked as "dragged" /// when the mouse has moved a bit, but `is_being_dragged` will return true immediately. + #[deprecated = "Use `Context::is_being_dragged` instead"] #[inline(always)] pub fn is_being_dragged(&self, id: Id) -> bool { - self.interaction().drag_id == Some(id) + self.interaction().potential_drag_id == Some(id) } /// Get the id of the widget being dragged, if any. @@ -792,29 +762,33 @@ impl Memory { /// so the widget may not yet be marked as "dragged", /// as that can only happen after the mouse has moved a bit /// (at least if the widget is interesated in both clicks and drags). + #[deprecated = "Use `Context::dragged_id` instead"] #[inline(always)] pub fn dragged_id(&self) -> Option { - self.interaction().drag_id + self.interaction().potential_drag_id } /// Set which widget is being dragged. #[inline(always)] + #[deprecated = "Use `Context::set_dragged_id` instead"] pub fn set_dragged_id(&mut self, id: Id) { - self.interaction_mut().drag_id = Some(id); + self.interaction_mut().potential_drag_id = Some(id); } /// Stop dragging any widget. #[inline(always)] + #[deprecated = "Use `Context::stop_dragging` instead"] pub fn stop_dragging(&mut self) { - self.interaction_mut().drag_id = None; + self.interaction_mut().potential_drag_id = None; } /// Is something else being dragged? /// /// Returns true if we are dragging something, but not the given widget. #[inline(always)] + #[deprecated = "Use `Context::dragging_something_else` instead"] pub fn dragging_something_else(&self, not_this: Id) -> bool { - let drag_id = self.interaction().drag_id; + let drag_id = self.interaction().potential_drag_id; drag_id.is_some() && drag_id != Some(not_this) } @@ -831,25 +805,13 @@ impl Memory { self.areas().get(id.into()).map(|state| state.rect()) } - pub(crate) fn window_interaction(&self) -> Option { - self.window_interactions.get(&self.viewport_id).copied() - } - - pub(crate) fn set_window_interaction(&mut self, wi: Option) { - if let Some(wi) = wi { - self.window_interactions.insert(self.viewport_id, wi); - } else { - self.window_interactions.remove(&self.viewport_id); - } - } - - pub(crate) fn interaction(&self) -> &Interaction { + pub(crate) fn interaction(&self) -> &InteractionState { self.interactions .get(&self.viewport_id) .expect("Failed to get interaction") } - pub(crate) fn interaction_mut(&mut self) -> &mut Interaction { + pub(crate) fn interaction_mut(&mut self) -> &mut InteractionState { self.interactions.entry(self.viewport_id).or_default() } } diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index a6570014cfa8..6e791fc57729 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -2,7 +2,7 @@ use std::{any::Any, sync::Arc}; use crate::{ emath::{Align, Pos2, Rect, Vec2}, - menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText, + menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetRect, WidgetText, NUM_POINTER_BUTTONS, }; @@ -85,7 +85,7 @@ pub struct Response { /// The widget was being dragged, but now it has been released. #[doc(hidden)] - pub drag_released: bool, + pub drag_stopped: bool, /// Is the pointer button currently down on this widget? /// This is true if the pointer is pressing down or dragging a widget @@ -317,13 +317,26 @@ impl Response { /// The widget was being dragged, but now it has been released. #[inline] + pub fn drag_stopped(&self) -> bool { + self.drag_stopped + } + + /// The widget was being dragged by the button, but now it has been released. + pub fn drag_stopped_by(&self, button: PointerButton) -> bool { + self.drag_stopped() && self.ctx.input(|i| i.pointer.button_released(button)) + } + + /// The widget was being dragged, but now it has been released. + #[inline] + #[deprecated = "Renamed 'dragged_stopped'"] pub fn drag_released(&self) -> bool { - self.drag_released + self.drag_stopped } /// The widget was being dragged by the button, but now it has been released. + #[deprecated = "Renamed 'dragged_stopped_by'"] pub fn drag_released_by(&self, button: PointerButton) -> bool { - self.drag_released() && self.ctx.input(|i| i.pointer.button_released(button)) + self.drag_stopped_by(button) } /// If dragged, how many points were we dragged and in what direction? @@ -609,37 +622,45 @@ impl Response { self } - /// Check for more interactions (e.g. sense clicks on a [`Response`] returned from a label). + /// Sense more interactions (e.g. sense clicks on a [`Response`] returned from a label). + /// + /// The interaction will occur on the same plane as the original widget, + /// i.e. if the response was from a widget behind button, the interaction will also be behind that button. + /// egui gives priority to the _last_ added widget (the one on top gets clicked first). /// /// Note that this call will not add any hover-effects to the widget, so when possible /// it is better to give the widget a [`Sense`] instead, e.g. using [`crate::Label::sense`]. /// + /// Using this method on a `Response` that is the result of calling `union` on multiple `Response`s + /// is undefined behavior. + /// /// ``` /// # egui::__run_test_ui(|ui| { - /// let response = ui.label("hello"); - /// assert!(!response.clicked()); // labels don't sense clicks by default - /// let response = response.interact(egui::Sense::click()); - /// if response.clicked() { /* … */ } + /// let horiz_response = ui.horizontal(|ui| { + /// ui.label("hello"); + /// }).response; + /// assert!(!horiz_response.clicked()); // ui's don't sense clicks by default + /// let horiz_response = horiz_response.interact(egui::Sense::click()); + /// if horiz_response.clicked() { + /// // The background behind the label was clicked + /// } /// # }); /// ``` #[must_use] pub fn interact(&self, sense: Sense) -> Self { - // Test if we must sense something new compared to what we have already sensed. If not, then - // we can return early. This may avoid unnecessarily "masking" some widgets with unneeded - // interactions. if (self.sense | sense) == self.sense { + // Early-out: we already sense everything we need to sense. return self.clone(); } - self.ctx.interact_with_hovered( - self.layer_id, - self.id, - self.rect, - self.interact_rect, + self.ctx.create_widget(WidgetRect { + layer_id: self.layer_id, + id: self.id, + rect: self.rect, + interact_rect: self.interact_rect, sense, - self.enabled, - self.contains_pointer, - ) + enabled: self.enabled, + }) } /// Adjust the scroll position until this UI becomes visible. @@ -843,6 +864,8 @@ impl Response { /// For instance `a.union(b).hovered` means "was either a or b hovered?". /// /// The resulting [`Self::id`] will come from the first (`self`) argument. + /// + /// You may not call [`Self::interact`] on the resulting `Response`. pub fn union(&self, other: Self) -> Self { assert!(self.ctx == other.ctx); crate::egui_assert!( @@ -883,7 +906,7 @@ impl Response { ], drag_started: self.drag_started || other.drag_started, dragged: self.dragged || other.dragged, - drag_released: self.drag_released || other.drag_released, + drag_stopped: self.drag_stopped || other.drag_stopped, is_pointer_button_down_on: self.is_pointer_button_down_on || other.is_pointer_button_down_on, interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos), @@ -900,6 +923,8 @@ impl Response { } } +/// See [`Response::union`]. +/// /// To summarize the response from many widgets you can use this pattern: /// /// ``` @@ -918,6 +943,8 @@ impl std::ops::BitOr for Response { } } +/// See [`Response::union`]. +/// /// To summarize the response from many widgets you can use this pattern: /// /// ``` diff --git a/crates/egui/src/sense.rs b/crates/egui/src/sense.rs index 53baa3ec3c0d..ca216896aee9 100644 --- a/crates/egui/src/sense.rs +++ b/crates/egui/src/sense.rs @@ -59,6 +59,13 @@ impl Sense { } /// Sense both clicks, drags and hover (e.g. a slider or window). + /// + /// Note that this will introduce a latency when dragging, + /// because when the user starts a press egui can't know if this is the start + /// of a click or a drag, and it won't know until the cursor has + /// either moved a certain distance, or the user has released the mouse button. + /// + /// See [`crate::PointerState::is_decidedly_dragging`] for details. #[inline] pub fn click_and_drag() -> Self { Self { diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 645a7d188f29..e42d1996ca15 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -709,10 +709,16 @@ impl std::ops::Add for Margin { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Interaction { - /// Mouse must be this close to the side of a window to resize + /// How close a widget must be to the mouse to have a chance to register as a click or drag. + /// + /// If this is larger than zero, it gets easier to hit widgets, + /// which is important for e.g. touch screens. + pub interact_radius: f32, + + /// Radius of the interactive area of the side of a window during drag-to-resize. pub resize_grab_radius_side: f32, - /// Mouse must be this close to the corner of a window to resize + /// Radius of the interactive area of the corner of a window during drag-to-resize. pub resize_grab_radius_corner: f32, /// If `false`, tooltips will show up anytime you hover anything, even is mouse is still moving @@ -1041,8 +1047,8 @@ pub struct DebugOptions { /// Show an overlay on all interactive widgets. pub show_interactive_widgets: bool, - /// Show what widget blocks the interaction of another widget. - pub show_blocking_widget: bool, + /// Show interesting widgets under the mouse cursor. + pub show_widget_hits: bool, } #[cfg(debug_assertions)] @@ -1057,7 +1063,7 @@ impl Default for DebugOptions { show_expand_height: false, show_resize: false, show_interactive_widgets: false, - show_blocking_widget: false, + show_widget_hits: false, } } } @@ -1127,6 +1133,7 @@ impl Default for Interaction { Self { resize_grab_radius_side: 5.0, resize_grab_radius_corner: 10.0, + interact_radius: 5.0, show_tooltips_only_when_still: true, tooltip_delay: 0.3, selectable_labels: true, @@ -1592,6 +1599,7 @@ fn margin_ui(ui: &mut Ui, text: &str, margin: &mut Margin) { impl Interaction { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { + interact_radius, resize_grab_radius_side, resize_grab_radius_corner, show_tooltips_only_when_still, @@ -1599,6 +1607,8 @@ impl Interaction { selectable_labels, multi_widget_text_select, } = self; + ui.add(Slider::new(interact_radius, 0.0..=20.0).text("interact_radius")) + .on_hover_text("Interact with the closest widget within this radius."); ui.add(Slider::new(resize_grab_radius_side, 0.0..=20.0).text("resize_grab_radius_side")); ui.add( Slider::new(resize_grab_radius_corner, 0.0..=20.0).text("resize_grab_radius_corner"), @@ -1607,7 +1617,11 @@ impl Interaction { show_tooltips_only_when_still, "Only show tooltips if mouse is still", ); - ui.add(Slider::new(tooltip_delay, 0.0..=1.0).text("tooltip_delay")); + ui.add( + Slider::new(tooltip_delay, 0.0..=1.0) + .suffix(" s") + .text("tooltip_delay"), + ); ui.horizontal(|ui| { ui.checkbox(selectable_labels, "Selectable text in labels"); @@ -1866,7 +1880,7 @@ impl DebugOptions { show_expand_height, show_resize, show_interactive_widgets, - show_blocking_widget, + show_widget_hits, } = self; { @@ -1894,10 +1908,7 @@ impl DebugOptions { "Show an overlay on all interactive widgets", ); - ui.checkbox( - show_blocking_widget, - "Show which widget blocks the interaction of another widget", - ); + ui.checkbox(show_widget_hits, "Show widgets under mouse pointer"); ui.vertical_centered(|ui| reset_button(ui, self)); } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 4c5184e39f8d..182c5782f155 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -646,40 +646,26 @@ impl Ui { impl Ui { /// Check for clicks, drags and/or hover on a specific region of this [`Ui`]. pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> Response { - self.ctx().interact( - self.clip_rect(), - self.spacing().item_spacing, - self.layer_id(), + self.ctx().create_widget(WidgetRect { id, + layer_id: self.layer_id(), rect, + interact_rect: self.clip_rect().intersect(rect), sense, - self.enabled, - ) + enabled: self.enabled, + }) } - /// Check for clicks, and drags on a specific region that is hovered. - /// This can be used once you have checked that some shape you are painting has been hovered, - /// and want to check for clicks and drags on hovered items this frame. - /// - /// The given [`Rect`] should approximately be where the thing is, - /// as will be the rectangle for the returned [`Response::rect`]. + /// Deprecated: use [`Self::interact`] instead. + #[deprecated = "The contains_pointer argument is ignored. Use `ui.interact` instead."] pub fn interact_with_hovered( &self, rect: Rect, - contains_pointer: bool, + _contains_pointer: bool, id: Id, sense: Sense, ) -> Response { - let interact_rect = rect.intersect(self.clip_rect()); - self.ctx().interact_with_hovered( - self.layer_id(), - id, - rect, - interact_rect, - sense, - self.enabled, - contains_pointer, - ) + self.interact(rect, id, sense) } /// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]? @@ -2160,9 +2146,11 @@ impl Ui { where Payload: Any + Send + Sync, { - let is_being_dragged = self.memory(|mem| mem.is_being_dragged(id)); + let is_being_dragged = self.ctx().is_being_dragged(id); if is_being_dragged { + crate::DragAndDrop::set_payload(self.ctx(), payload); + // Paint the body to a new layer: let layer_id = LayerId::new(Order::Tooltip, id); let InnerResponse { inner, response } = self.with_layer_id(layer_id, add_contents); @@ -2185,9 +2173,9 @@ impl Ui { let InnerResponse { inner, response } = self.scope(add_contents); // Check for drags: - let dnd_response = self.interact(response.rect, id, Sense::drag()); - - dnd_response.dnd_set_drag_payload(payload); + let dnd_response = self + .interact(response.rect, id, Sense::drag()) + .on_hover_cursor(CursorIcon::Grab); InnerResponse::new(inner, dnd_response | response) } diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 09d3b9f3d7d1..f0875ba30553 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -374,7 +374,7 @@ impl<'a> Widget for DragValue<'a> { let shift = ui.input(|i| i.modifiers.shift_only()); // The widget has the same ID whether it's in edit or button mode. let id = ui.next_auto_id(); - let is_slow_speed = shift && ui.memory(|mem| mem.is_being_dragged(id)); + let is_slow_speed = shift && ui.ctx().is_being_dragged(id); // The following ensures that when a `DragValue` receives focus, // it is immediately rendered in edit mode, rather than being rendered diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 452d60c4cb77..22a5623f077c 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -551,7 +551,7 @@ impl<'t> TextEdit<'t> { paint_cursor(&painter, ui.visuals(), cursor_rect); } - let is_being_dragged = ui.ctx().memory(|m| m.is_being_dragged(response.id)); + let is_being_dragged = ui.ctx().is_being_dragged(response.id); let did_interact = state.cursor.pointer_interaction( ui, &response, diff --git a/crates/egui_demo_lib/src/demo/tests.rs b/crates/egui_demo_lib/src/demo/tests.rs index 4b0368cc50b3..2dd6ea64a864 100644 --- a/crates/egui_demo_lib/src/demo/tests.rs +++ b/crates/egui_demo_lib/src/demo/tests.rs @@ -475,8 +475,8 @@ fn response_summary(response: &egui::Response, show_hovers: bool) -> String { writeln!(new_info, "Clicked{button_suffix}").ok(); } - if response.drag_released_by(button) { - writeln!(new_info, "Drag ended{button_suffix}").ok(); + if response.drag_stopped_by(button) { + writeln!(new_info, "Drag stopped{button_suffix}").ok(); } if response.dragged_by(button) { writeln!(new_info, "Dragged{button_suffix}").ok(); diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index 9bce5ec4f35f..0876d9db07dc 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -203,6 +203,7 @@ impl<'l> StripLayout<'l> { } add_cell_contents(&mut child_ui); + child_ui.min_rect() } diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 0796edc292c7..692399e8bd3e 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -1052,7 +1052,7 @@ impl Plot { )); } // when the click is release perform the zoom - if response.drag_released() { + if response.drag_stopped() { let box_start_pos = mem.transform.value_from_position(box_start_pos); let box_end_pos = mem.transform.value_from_position(box_end_pos); let new_bounds = PlotBounds { diff --git a/examples/test_viewports/src/main.rs b/examples/test_viewports/src/main.rs index 791cf8e4efc6..b407ff5a12cf 100644 --- a/examples/test_viewports/src/main.rs +++ b/examples/test_viewports/src/main.rs @@ -386,7 +386,7 @@ fn drag_and_drop_test(ui: &mut egui::Ui) { for (id, value) in data.read().cols(container_id, col) { drag_source(ui, id, |ui| { ui.add(egui::Label::new(value).sense(egui::Sense::click())); - if ui.memory(|mem| mem.is_being_dragged(id)) { + if ui.ctx().is_being_dragged(id) { is_dragged = Some(id); } }); @@ -408,7 +408,7 @@ fn drag_source( id: egui::Id, body: impl FnOnce(&mut egui::Ui) -> R, ) -> InnerResponse { - let is_being_dragged = ui.memory(|mem| mem.is_being_dragged(id)); + let is_being_dragged = ui.ctx().is_being_dragged(id); if !is_being_dragged { let res = ui.scope(body); @@ -438,12 +438,12 @@ fn drag_source( } } -// This is taken from crates/egui_demo_lib/src/debo/drag_and_drop.rs +// TODO: Update to be more like `crates/egui_demo_lib/src/debo/drag_and_drop.rs` fn drop_target( ui: &mut egui::Ui, body: impl FnOnce(&mut egui::Ui) -> R, ) -> egui::InnerResponse { - let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged()); + let is_being_dragged = ui.ctx().dragged_id().is_some(); let margin = egui::Vec2::splat(ui.visuals().clip_rect_margin); // 3.0