diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c34b27b3a..1f235c19bca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ## Unreleased * ⚠️ BREAKING: egui now expects integrations to do all color blending in gamma space ([#2071](https://github.com/emilk/egui/pull/2071)). +* ⚠️ BREAKING: if you have overlapping interactive widgets, only the top widget (last added) will be interactive ([#2244](https://github.com/emilk/egui/pull/2244)). ### Added ⭐ * Added helper functions for animating panels that collapse/expand ([#2190](https://github.com/emilk/egui/pull/2190)). @@ -15,7 +16,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Texture loading now takes a `TexureOptions` with minification and magnification filters ([#2224](https://github.com/emilk/egui/pull/2224)). * Added `Key::Minus` and `Key::Equals` ([#2239](https://github.com/emilk/egui/pull/2239)). * Added `egui::gui_zoom` module with helpers for scaling the whole GUI of an app ([#2239](https://github.com/emilk/egui/pull/2239)). -* Implemented `Debug` for `egui::Context` ([#2248](https://github.com/emilk/egui/pull/2248)). +* You can now put one interactive widget on top of another, and only one will get interaction at a time ([#2244](https://github.com/emilk/egui/pull/2244)). ### Fixed 🐛 * ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)). diff --git a/Cargo.toml b/Cargo.toml index 06ee751e683..eed3b6fc310 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'` [profile.dev] split-debuginfo = "unpacked" # faster debug builds on mac -opt-level = 1 # Make debug builds run faster +# opt-level = 1 # Make debug builds run faster # Optimize all dependencies even in debug builds (does not affect workspace packages): [profile.dev.package."*"] diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 01fd7df1cd4..f302c65f5be 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -169,7 +169,7 @@ impl Area { pub(crate) struct Prepared { layer_id: LayerId, state: State, - pub(crate) movable: bool, + move_response: Response, enabled: bool, drag_bounds: Option, /// Set the first frame of new windows with anchors. @@ -231,12 +231,53 @@ impl Area { } } + // interact right away to prevent frame-delay + let move_response = { + let interact_id = layer_id.id.with("move"); + let sense = if movable { + Sense::click_and_drag() + } else { + Sense::click() // allow clicks to bring to front + }; + + let move_response = ctx.interact( + Rect::EVERYTHING, + ctx.style().spacing.item_spacing, + layer_id, + interact_id, + state.rect(), + sense, + enabled, + ); + + // Important check - don't try to move e.g. a combobox popup! + if movable { + if move_response.dragged() { + state.pos += ctx.input().pointer.delta(); + } + + state.pos = ctx + .constrain_window_rect_to_area(state.rect(), drag_bounds) + .min; + } + + if (move_response.dragged() || move_response.clicked()) + || pointer_pressed_on_area(ctx, layer_id) + || !ctx.memory().areas.visible_last_frame(&layer_id) + { + ctx.memory().areas.move_to_top(layer_id); + ctx.request_repaint(); + } + + move_response + }; + state.pos = ctx.round_pos_to_pixels(state.pos); Prepared { layer_id, state, - movable, + move_response, enabled, drag_bounds, temporarily_invisible, @@ -330,49 +371,14 @@ impl Prepared { let Prepared { layer_id, mut state, - movable, - enabled, - drag_bounds, + move_response, + enabled: _, + drag_bounds: _, temporarily_invisible: _, } = self; state.size = content_ui.min_rect().size(); - let interact_id = layer_id.id.with("move"); - let sense = if movable { - Sense::click_and_drag() - } else { - Sense::click() // allow clicks to bring to front - }; - - let move_response = ctx.interact( - Rect::EVERYTHING, - ctx.style().spacing.item_spacing, - layer_id, - interact_id, - state.rect(), - sense, - enabled, - ); - - if move_response.dragged() && movable { - state.pos += ctx.input().pointer.delta(); - } - - // Important check - don't try to move e.g. a combobox popup! - if movable { - state.pos = ctx - .constrain_window_rect_to_area(state.rect(), drag_bounds) - .min; - } - - if (move_response.dragged() || move_response.clicked()) - || pointer_pressed_on_area(ctx, layer_id) - || !ctx.memory().areas.visible_last_frame(&layer_id) - { - ctx.memory().areas.move_to_top(layer_id); - ctx.request_repaint(); - } ctx.memory().areas.set_state(layer_id, state); move_response diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 6a5b760df1b..f777b8c4e4d 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -119,38 +119,45 @@ impl Frame { } impl Frame { + #[inline] pub fn fill(mut self, fill: Color32) -> Self { self.fill = fill; self } + #[inline] pub fn stroke(mut self, stroke: Stroke) -> Self { self.stroke = stroke; self } + #[inline] pub fn rounding(mut self, rounding: impl Into) -> Self { self.rounding = rounding.into(); self } /// Margin within the painted frame. + #[inline] pub fn inner_margin(mut self, inner_margin: impl Into) -> Self { self.inner_margin = inner_margin.into(); self } /// Margin outside the painted frame. + #[inline] pub fn outer_margin(mut self, outer_margin: impl Into) -> Self { self.outer_margin = outer_margin.into(); self } #[deprecated = "Renamed inner_margin in egui 0.18"] + #[inline] pub fn margin(self, margin: impl Into) -> Self { self.inner_margin(margin) } + #[inline] pub fn shadow(mut self, shadow: Shadow) -> Self { self.shadow = shadow; self @@ -164,6 +171,16 @@ impl Frame { } } +impl Frame { + /// inner margin plus outer margin. + #[inline] + pub fn total_margin(&self) -> Margin { + self.inner_margin + self.outer_margin + } +} + +// ---------------------------------------------------------------------------- + pub struct Prepared { pub frame: Frame, where_to_put_background: ShapeIdx, diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 9e35ba02e2b..f18919bb066 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -302,7 +302,7 @@ pub fn popup_below_widget( // Note: we use a separate clip-rect for this area, so the popup can be outside the parent. // See https://github.com/emilk/egui/issues/825 let frame = Frame::popup(ui.style()); - let frame_margin = frame.inner_margin + frame.outer_margin; + let frame_margin = frame.total_margin(); frame .show(ui, |ui| { ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 4fdc872e1a8..76fd212a0ad 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -14,8 +14,12 @@ pub struct State { /// Positive offset means scrolling down/right pub offset: Vec2, + /// Were the scroll bars visible last frame? show_scroll: [bool; 2], + /// The content were to large to fit large frame. + content_is_too_large: [bool; 2], + /// Momentum, used for kinetic scrolling #[cfg_attr(feature = "serde", serde(skip))] vel: Vec2, @@ -34,6 +38,7 @@ impl Default for State { Self { offset: Vec2::ZERO, show_scroll: [false; 2], + content_is_too_large: [false; 2], vel: Vec2::ZERO, scroll_start_offset_from_top_left: [None; 2], scroll_stuck_to_end: [true; 2], @@ -406,6 +411,40 @@ impl ScrollArea { let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size); + if scrolling_enabled && (state.content_is_too_large[0] || state.content_is_too_large[1]) { + // Drag contents to scroll (for touch screens mostly). + // We must do this BEFORE adding content to the `ScrollArea`, + // or we will steal input from the widgets we contain. + let content_response = ui.interact(inner_rect, id.with("area"), Sense::drag()); + + if content_response.dragged() { + for d in 0..2 { + if has_bar[d] { + state.offset[d] -= ui.input().pointer.delta()[d]; + state.vel[d] = ui.input().pointer.velocity()[d]; + state.scroll_stuck_to_end[d] = false; + } else { + state.vel[d] = 0.0; + } + } + } else { + let stop_speed = 20.0; // Pixels per second. + let friction_coeff = 1000.0; // Pixels per second squared. + let dt = ui.input().unstable_dt; + + let friction = friction_coeff * dt; + if friction > state.vel.length() || state.vel.length() < stop_speed { + state.vel = Vec2::ZERO; + } else { + state.vel -= friction * state.vel.normalized(); + // Offset has an inverted coordinate system compared to + // the velocity, so we subtract it instead of adding it + state.offset -= state.vel * dt; + ui.ctx().request_repaint(); + } + } + } + Prepared { id, state, @@ -606,43 +645,6 @@ impl Prepared { content_size.y > inner_rect.height(), ]; - if content_is_too_large[0] || content_is_too_large[1] { - // Drag contents to scroll (for touch screens mostly): - let sense = if self.scrolling_enabled { - Sense::drag() - } else { - Sense::hover() - }; - let content_response = ui.interact(inner_rect, id.with("area"), sense); - - if content_response.dragged() { - for d in 0..2 { - if has_bar[d] { - state.offset[d] -= ui.input().pointer.delta()[d]; - state.vel[d] = ui.input().pointer.velocity()[d]; - state.scroll_stuck_to_end[d] = false; - } else { - state.vel[d] = 0.0; - } - } - } else { - let stop_speed = 20.0; // Pixels per second. - let friction_coeff = 1000.0; // Pixels per second squared. - let dt = ui.input().unstable_dt; - - let friction = friction_coeff * dt; - if friction > state.vel.length() || state.vel.length() < stop_speed { - state.vel = Vec2::ZERO; - } else { - state.vel -= friction * state.vel.normalized(); - // Offset has an inverted coordinate system compared to - // the velocity, so we subtract it instead of adding it - state.offset -= state.vel * dt; - ui.ctx().request_repaint(); - } - } - } - let max_offset = content_size - inner_rect.size(); if scrolling_enabled && ui.rect_contains_pointer(outer_rect) { for d in 0..2 { @@ -837,6 +839,7 @@ impl Prepared { ]; state.show_scroll = show_scroll_this_frame; + state.content_is_too_large = content_is_too_large; state.store(ui.ctx(), id); diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 3712432aecc..ed2dc9a55e3 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -881,8 +881,11 @@ impl TitleBar { ui.painter().hline(outer_rect.x_range(), y, stroke); } + // Don't cover the close- and collapse buttons: + let double_click_rect = self.rect.shrink2(vec2(32.0, 0.0)); + if ui - .interact(self.rect, self.id, Sense::click()) + .interact(double_click_rect, self.id, Sense::click()) .double_clicked() && collapsible { diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 1df5ed45073..a4a92447083 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -62,6 +62,11 @@ struct ContextImpl { has_requested_repaint_this_frame: bool, requested_repaint_last_frame: bool, + + /// Written to during the frame. + layer_rects_this_frame: ahash::HashMap>, + /// Read + layer_rects_prev_frame: ahash::HashMap>, } impl ContextImpl { @@ -79,6 +84,8 @@ impl ContextImpl { new_raw_input.screen_rect = Some(rect); } + self.layer_rects_prev_frame = std::mem::take(&mut self.layer_rects_this_frame); + self.memory.begin_frame(&self.input, &new_raw_input); self.input = std::mem::take(&mut self.input) @@ -328,8 +335,66 @@ impl Context { (0.5 * item_spacing - Vec2::splat(gap)) .at_least(Vec2::splat(0.0)) .at_most(Vec2::splat(5.0)), - ); // make it easier to click - let hovered = self.rect_contains_pointer(layer_id, clip_rect.intersect(interact_rect)); + ); + + // Respect clip rectangle when interacting + let interact_rect = clip_rect.intersect(interact_rect); + let mut hovered = self.rect_contains_pointer(layer_id, interact_rect); + + // This solves the problem of overlapping widgets. + // Whichever widget is added LAST (=on top) gets the input: + if interact_rect.is_positive() && sense.interactive() { + if 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)), + ); + } + + let mut slf = self.write(); + + slf.layer_rects_this_frame + .entry(layer_id) + .or_default() + .push((id, interact_rect)); + + if hovered { + let pointer_pos = slf.input.pointer.interact_pos(); + if let Some(pointer_pos) = pointer_pos { + if let Some(rects) = slf.layer_rects_prev_frame.get(&layer_id) { + for &(prev_id, prev_rect) in rects.iter().rev() { + if prev_id == id { + break; // there is no other interactive widget covering us at the pointer position. + } + if prev_rect.contains(pointer_pos) { + // Another interactive widget is covering us at the pointer position, + // so we aren't hovered. + + if slf.memory.options.style.debug.show_blocking_widget { + drop(slf); + Self::layer_painter(self, LayerId::debug()).debug_rect( + interact_rect, + Color32::GREEN, + "Covered", + ); + Self::layer_painter(self, LayerId::debug()).debug_rect( + prev_rect, + Color32::LIGHT_BLUE, + "On top", + ); + } + + hovered = false; + break; + } + } + } + } + } + } + self.interact_with_hovered(layer_id, id, rect, sense, enabled, hovered) } @@ -1095,11 +1160,13 @@ impl Context { } pub(crate) fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { - let pointer_pos = self.input().pointer.interact_pos(); - if let Some(pointer_pos) = pointer_pos { - rect.contains(pointer_pos) && self.layer_id_at(pointer_pos) == Some(layer_id) - } else { - false + rect.is_positive() && { + let pointer_pos = self.input().pointer.interact_pos(); + if let Some(pointer_pos) = pointer_pos { + rect.contains(pointer_pos) && self.layer_id_at(pointer_pos) == Some(layer_id) + } else { + false + } } } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index c28d0948623..a92e723a737 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -592,11 +592,20 @@ impl WidgetVisuals { pub struct DebugOptions { /// However over widgets to see their rectangles pub debug_on_hover: bool, + /// Show which widgets make their parent wider pub show_expand_width: bool, + /// Show which widgets make their parent higher pub show_expand_height: bool, + pub show_resize: bool, + + /// 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, } // ---------------------------------------------------------------------------- @@ -1255,21 +1264,33 @@ impl DebugOptions { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { debug_on_hover, - show_expand_width: debug_expand_width, - show_expand_height: debug_expand_height, - show_resize: debug_resize, + show_expand_width, + show_expand_height, + show_resize, + show_interactive_widgets, + show_blocking_widget, } = self; ui.checkbox(debug_on_hover, "Show debug info on hover"); ui.checkbox( - debug_expand_width, + show_expand_width, "Show which widgets make their parent wider", ); ui.checkbox( - debug_expand_height, + show_expand_height, "Show which widgets make their parent higher", ); - ui.checkbox(debug_resize, "Debug Resize"); + ui.checkbox(show_resize, "Debug Resize"); + + ui.checkbox( + show_interactive_widgets, + "Show an overlay on all interactive widgets", + ); + + ui.checkbox( + show_blocking_widget, + "Show wha widget blocks the interaction of another widget", + ); ui.vertical_centered(|ui| reset_button(ui, self)); } diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index 9b4b0886bab..36231efedc9 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -354,7 +354,7 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res if ui.memory().is_popup_open(popup_id) { let area_response = Area::new(popup_id) .order(Order::Foreground) - .current_pos(button_response.rect.max) + .fixed_pos(button_response.rect.max) .show(ui.ctx(), |ui| { ui.spacing_mut().slider_width = 210.0; Frame::popup(ui.style()).show(ui, |ui| {