Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow overlapping interactive widgets #2244

Merged
merged 11 commits into from Nov 7, 2022
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