diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 4aff4edb3d0..e93217e58a6 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 705dfd733cd..11e69b8289c 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 9670ecb415e..22286368b7c 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 f63952641c3..8cf5475ebbd 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 440fe522c65..22741b237d5 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 00000000000..fe8b085a354 --- /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 00000000000..f1a3e2a3247 --- /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 43b3a88b81f..240ff6974a5 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 ad931885ea8..c715b859e6c 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 f222b300b7a..fcfe0accb42 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 a6570014cfa..6e791fc5772 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 53baa3ec3c0..ca216896aee 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 645a7d188f2..e42d1996ca1 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 4c5184e39f8..182c5782f15 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 09d3b9f3d7d..f0875ba3055 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 452d60c4cb7..22a5623f077 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 4b0368cc50b..2dd6ea64a86 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 9bce5ec4f35..0876d9db07d 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 0796edc292c..692399e8bd3 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 791cf8e4efc..b407ff5a12c 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