Allow overlapping interactive widgets (#2244)
* Turn off optimization for debug builds again * Optimize rect_contains_pointer * Fix for colorpicker: make popup immovable * Area: interact first * ScrollArea: do interaction first * Window: shrink double-clickable area of titelbar * Only the top-most (latest added) interactive widget gets `hovered=true` * Add Frame::total_margin * Update changelog * Add debug-options to visualize what widgets cover which other widget
This commit is contained in:
parent
8c76b8caff
commit
d5eb8779cb
10 changed files with 213 additions and 95 deletions
|
@ -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)).
|
||||
|
|
|
@ -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."*"]
|
||||
|
|
|
@ -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<Rect>,
|
||||
/// 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
|
||||
|
|
|
@ -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<Rounding>) -> Self {
|
||||
self.rounding = rounding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Margin within the painted frame.
|
||||
#[inline]
|
||||
pub fn inner_margin(mut self, inner_margin: impl Into<Margin>) -> Self {
|
||||
self.inner_margin = inner_margin.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Margin outside the painted frame.
|
||||
#[inline]
|
||||
pub fn outer_margin(mut self, outer_margin: impl Into<Margin>) -> Self {
|
||||
self.outer_margin = outer_margin.into();
|
||||
self
|
||||
}
|
||||
|
||||
#[deprecated = "Renamed inner_margin in egui 0.18"]
|
||||
#[inline]
|
||||
pub fn margin(self, margin: impl Into<Margin>) -> 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,
|
||||
|
|
|
@ -302,7 +302,7 @@ pub fn popup_below_widget<R>(
|
|||
// 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| {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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<LayerId, Vec<(Id, Rect)>>,
|
||||
/// Read
|
||||
layer_rects_prev_frame: ahash::HashMap<LayerId, Vec<(Id, Rect)>>,
|
||||
}
|
||||
|
||||
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,6 +1160,7 @@ impl Context {
|
|||
}
|
||||
|
||||
pub(crate) fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool {
|
||||
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)
|
||||
|
@ -1102,6 +1168,7 @@ impl Context {
|
|||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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| {
|
||||
|
|
Loading…
Reference in a new issue