Skip to content

Commit

Permalink
New widget interaction logic (#4026)
Browse files Browse the repository at this point in the history
* Closes #3936
* Closes #3923
* Closes #4058

The interaction code is now done at the start of the frame, using stored
`WidgetRect`s from the previous frame.

The intention is that the new interaction code should be more accurate,
making it easier to hit widgets, and better respecting the rules of
overlapping widgets.

There is a new `style::Interaction::interact_radius` controlling how far
away from a widget the cursor can be and still hit it. This helps big
fat fingers hit small widgets on touch screens.

This PR adds a new `Context::read_response` which lets you read the
`Response` of a `Widget` _before_ you create the widget. This can be
used for styling, or for reading the result of an interaction early (to
prevent frame-delay) for a widget you add late (so it is on top of other
widgets).

# ⚠️ BREAKING CHANGES
`Memory::dragged_id`, `Memory::set_dragged_id` etc have been moved to
`Context`.
The semantics for `Context::dragged_id` is slightly different: a widget
is not considered dragged until egui it is sure this is not a
click-in-progress. For a widget that is only sensitive to drags, that is
right away, but for widgets sensitive to both clicks and drags it is not
until the mouse has moved a certain distance.

# TODO
* [x] Fix panel resizing
* [x] Fix scroll hover weirdness
* [x] Fix Resize widget
* [x] Fix drag-and-drop
* [x] Test all of egui_demo_app
* [x] Change `is_dragging` API
* [x] Consistent naming of start/stop or begin/end drag
* [x] Test `egui_tiles`
* [x] Test Rerun
* [x] Document
* [x] Document breaking changes in PR description
* [x] Test one final time

# Saving for a later PR
* [ ] Fix #4047
* [ ] Specify what the response order for e.g. `ui.horizontal` is

I think both these can be fixed if each `Ui` registers themselves as a
`WidgetRect`, with the possibility to interact with it later, as if the
interaction was under all widgets on top of it.
  • Loading branch information
emilk authored Feb 17, 2024
1 parent 2eaaf5f commit ca8c879
Show file tree
Hide file tree
Showing 20 changed files with 1,513 additions and 768 deletions.
13 changes: 6 additions & 7 deletions crates/egui/src/containers/area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
135 changes: 57 additions & 78 deletions crates/egui/src/containers/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}
Expand Down Expand Up @@ -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);

{
Expand Down Expand Up @@ -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);
}
}
}
}
Expand Down Expand Up @@ -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);

{
Expand Down
43 changes: 26 additions & 17 deletions crates/egui/src/containers/resize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ impl Resize {

struct Prepared {
id: Id,
corner_id: Option<Id>,
state: State,
corner_response: Option<Response>,
content_ui: Ui,
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -279,8 +274,8 @@ impl Resize {

Prepared {
id,
corner_id,
state,
corner_response,
content_ui,
}
}
Expand All @@ -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;

Expand All @@ -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
Expand Down
Loading

0 comments on commit ca8c879

Please sign in to comment.