Skip to content

Commit

Permalink
Allow overlapping interactive widgets (#2244)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
emilk committed Nov 7, 2022
1 parent 8c76b8c commit d5eb877
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 95 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Expand Up @@ -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)).
Expand All @@ -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)).
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Expand Up @@ -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."*"]
Expand Down
86 changes: 46 additions & 40 deletions crates/egui/src/containers/area.rs
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions crates/egui/src/containers/frame.rs
Expand Up @@ -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
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/containers/popup.rs
Expand Up @@ -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| {
Expand Down
77 changes: 40 additions & 37 deletions crates/egui/src/containers/scroll_area.rs
Expand Up @@ -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,
Expand All @@ -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],
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);

Expand Down
5 changes: 4 additions & 1 deletion crates/egui/src/containers/window.rs
Expand Up @@ -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
{
Expand Down

0 comments on commit d5eb877

Please sign in to comment.