Skip to content

Commit ca8c879

Browse files
authored
New widget interaction logic (#4026)
* 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.
1 parent 2eaaf5f commit ca8c879

File tree

20 files changed

+1513
-768
lines changed

20 files changed

+1513
-768
lines changed

crates/egui/src/containers/area.rs

+6-7
Original file line numberDiff line numberDiff line change
@@ -313,22 +313,21 @@ impl Area {
313313
let mut move_response = {
314314
let interact_id = layer_id.id.with("move");
315315
let sense = if movable {
316-
Sense::click_and_drag()
316+
Sense::drag()
317317
} else if interactable {
318318
Sense::click() // allow clicks to bring to front
319319
} else {
320320
Sense::hover()
321321
};
322322

323-
let move_response = ctx.interact(
324-
Rect::EVERYTHING,
325-
ctx.style().spacing.item_spacing,
323+
let move_response = ctx.create_widget(WidgetRect {
324+
id: interact_id,
326325
layer_id,
327-
interact_id,
328-
state.rect(),
326+
rect: state.rect(),
327+
interact_rect: state.rect(),
329328
sense,
330329
enabled,
331-
);
330+
});
332331

333332
if movable && move_response.dragged() {
334333
state.pivot_pos += move_response.drag_delta();

crates/egui/src/containers/panel.rs

+57-78
Original file line numberDiff line numberDiff line change
@@ -238,48 +238,22 @@ impl SidePanel {
238238
ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel");
239239
}
240240

241+
let resize_id = id.with("__resize");
241242
let mut resize_hover = false;
242243
let mut is_resizing = false;
243244
if resizable {
244-
let resize_id = id.with("__resize");
245-
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
246-
let we_are_on_top = ui
247-
.ctx()
248-
.layer_id_at(pointer)
249-
.map_or(true, |top_layer_id| top_layer_id == ui.layer_id());
250-
let pointer = if let Some(transform) = ui
251-
.ctx()
252-
.memory(|m| m.layer_transforms.get(&ui.layer_id()).cloned())
253-
{
254-
transform.inverse() * pointer
255-
} else {
256-
pointer
257-
};
258-
259-
let resize_x = side.opposite().side_x(panel_rect);
260-
let mouse_over_resize_line = we_are_on_top
261-
&& panel_rect.y_range().contains(pointer.y)
262-
&& (resize_x - pointer.x).abs()
263-
<= ui.style().interaction.resize_grab_radius_side;
264-
265-
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
266-
&& mouse_over_resize_line
267-
{
268-
ui.memory_mut(|mem| mem.set_dragged_id(resize_id));
269-
}
270-
is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id));
271-
if is_resizing {
272-
let width = (pointer.x - side.side_x(panel_rect)).abs();
273-
let width = clamp_to_range(width, width_range).at_most(available_rect.width());
274-
side.set_rect_width(&mut panel_rect, width);
275-
}
245+
// First we read the resize interaction results, to avoid frame latency in the resize:
246+
if let Some(resize_response) = ui.ctx().read_response(resize_id) {
247+
resize_hover = resize_response.hovered();
248+
is_resizing = resize_response.dragged();
276249

277-
let dragging_something_else =
278-
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
279-
resize_hover = mouse_over_resize_line && !dragging_something_else;
280-
281-
if resize_hover || is_resizing {
282-
ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
250+
if is_resizing {
251+
if let Some(pointer) = resize_response.interact_pointer_pos() {
252+
let width = (pointer.x - side.side_x(panel_rect)).abs();
253+
let width =
254+
clamp_to_range(width, width_range).at_most(available_rect.width());
255+
side.set_rect_width(&mut panel_rect, width);
256+
}
283257
}
284258
}
285259
}
@@ -309,6 +283,22 @@ impl SidePanel {
309283
}
310284
ui.expand_to_include_rect(rect);
311285

286+
if resizable {
287+
// Now we do the actual resize interaction, on top of all the contents.
288+
// Otherwise its input could be eaten by the contents, e.g. a
289+
// `ScrollArea` on either side of the panel boundary.
290+
let resize_x = side.opposite().side_x(panel_rect);
291+
let resize_rect = Rect::from_x_y_ranges(resize_x..=resize_x, panel_rect.y_range())
292+
.expand2(vec2(ui.style().interaction.resize_grab_radius_side, 0.0));
293+
let resize_response = ui.interact(resize_rect, resize_id, Sense::drag());
294+
resize_hover = resize_response.hovered();
295+
is_resizing = resize_response.dragged();
296+
}
297+
298+
if resize_hover || is_resizing {
299+
ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
300+
}
301+
312302
PanelState { rect }.store(ui.ctx(), id);
313303

314304
{
@@ -706,50 +696,22 @@ impl TopBottomPanel {
706696
.check_for_id_clash(id, panel_rect, "TopBottomPanel");
707697
}
708698

699+
let resize_id = id.with("__resize");
709700
let mut resize_hover = false;
710701
let mut is_resizing = false;
711702
if resizable {
712-
let resize_id = id.with("__resize");
713-
let latest_pos = ui.input(|i| i.pointer.latest_pos());
714-
if let Some(pointer) = latest_pos {
715-
let we_are_on_top = ui
716-
.ctx()
717-
.layer_id_at(pointer)
718-
.map_or(true, |top_layer_id| top_layer_id == ui.layer_id());
719-
let pointer = if let Some(transform) = ui
720-
.ctx()
721-
.memory(|m| m.layer_transforms.get(&ui.layer_id()).cloned())
722-
{
723-
transform.inverse() * pointer
724-
} else {
725-
pointer
726-
};
727-
728-
let resize_y = side.opposite().side_y(panel_rect);
729-
let mouse_over_resize_line = we_are_on_top
730-
&& panel_rect.x_range().contains(pointer.x)
731-
&& (resize_y - pointer.y).abs()
732-
<= ui.style().interaction.resize_grab_radius_side;
733-
734-
if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down())
735-
&& mouse_over_resize_line
736-
{
737-
ui.memory_mut(|mem| mem.set_dragged_id(resize_id));
738-
}
739-
is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id));
740-
if is_resizing {
741-
let height = (pointer.y - side.side_y(panel_rect)).abs();
742-
let height =
743-
clamp_to_range(height, height_range).at_most(available_rect.height());
744-
side.set_rect_height(&mut panel_rect, height);
745-
}
703+
// First we read the resize interaction results, to avoid frame latency in the resize:
704+
if let Some(resize_response) = ui.ctx().read_response(resize_id) {
705+
resize_hover = resize_response.hovered();
706+
is_resizing = resize_response.dragged();
746707

747-
let dragging_something_else =
748-
ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed());
749-
resize_hover = mouse_over_resize_line && !dragging_something_else;
750-
751-
if resize_hover || is_resizing {
752-
ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical);
708+
if is_resizing {
709+
if let Some(pointer) = resize_response.interact_pointer_pos() {
710+
let height = (pointer.y - side.side_y(panel_rect)).abs();
711+
let height =
712+
clamp_to_range(height, height_range).at_most(available_rect.height());
713+
side.set_rect_height(&mut panel_rect, height);
714+
}
753715
}
754716
}
755717
}
@@ -779,6 +741,23 @@ impl TopBottomPanel {
779741
}
780742
ui.expand_to_include_rect(rect);
781743

744+
if resizable {
745+
// Now we do the actual resize interaction, on top of all the contents.
746+
// Otherwise its input could be eaten by the contents, e.g. a
747+
// `ScrollArea` on either side of the panel boundary.
748+
749+
let resize_y = side.opposite().side_y(panel_rect);
750+
let resize_rect = Rect::from_x_y_ranges(panel_rect.x_range(), resize_y..=resize_y)
751+
.expand2(vec2(0.0, ui.style().interaction.resize_grab_radius_side));
752+
let resize_response = ui.interact(resize_rect, resize_id, Sense::drag());
753+
resize_hover = resize_response.hovered();
754+
is_resizing = resize_response.dragged();
755+
}
756+
757+
if resize_hover || is_resizing {
758+
ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical);
759+
}
760+
782761
PanelState { rect }.store(ui.ctx(), id);
783762

784763
{

crates/egui/src/containers/resize.rs

+26-17
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@ impl Resize {
188188

189189
struct Prepared {
190190
id: Id,
191+
corner_id: Option<Id>,
191192
state: State,
192-
corner_response: Option<Response>,
193193
content_ui: Ui,
194194
}
195195

@@ -226,22 +226,17 @@ impl Resize {
226226

227227
let mut user_requested_size = state.requested_size.take();
228228

229-
let corner_response = if self.resizable {
230-
// Resize-corner:
231-
let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
232-
let corner_rect =
233-
Rect::from_min_size(position + state.desired_size - corner_size, corner_size);
234-
let corner_response = ui.interact(corner_rect, id.with("corner"), Sense::drag());
229+
let corner_id = self.resizable.then(|| id.with("__resize_corner"));
235230

236-
if let Some(pointer_pos) = corner_response.interact_pointer_pos() {
237-
user_requested_size =
238-
Some(pointer_pos - position + 0.5 * corner_response.rect.size());
231+
if let Some(corner_id) = corner_id {
232+
if let Some(corner_response) = ui.ctx().read_response(corner_id) {
233+
if let Some(pointer_pos) = corner_response.interact_pointer_pos() {
234+
// Respond to the interaction early to avoid frame delay.
235+
user_requested_size =
236+
Some(pointer_pos - position + 0.5 * corner_response.rect.size());
237+
}
239238
}
240-
241-
Some(corner_response)
242-
} else {
243-
None
244-
};
239+
}
245240

246241
if let Some(user_requested_size) = user_requested_size {
247242
state.desired_size = user_requested_size;
@@ -279,8 +274,8 @@ impl Resize {
279274

280275
Prepared {
281276
id,
277+
corner_id,
282278
state,
283-
corner_response,
284279
content_ui,
285280
}
286281
}
@@ -295,8 +290,8 @@ impl Resize {
295290
fn end(self, ui: &mut Ui, prepared: Prepared) {
296291
let Prepared {
297292
id,
293+
corner_id,
298294
mut state,
299-
corner_response,
300295
content_ui,
301296
} = prepared;
302297

@@ -320,6 +315,20 @@ impl Resize {
320315

321316
// ------------------------------
322317

318+
let corner_response = if let Some(corner_id) = corner_id {
319+
// We do the corner interaction last to place it on top of the content:
320+
let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
321+
let corner_rect = Rect::from_min_size(
322+
content_ui.min_rect().left_top() + size - corner_size,
323+
corner_size,
324+
);
325+
Some(ui.interact(corner_rect, corner_id, Sense::drag()))
326+
} else {
327+
None
328+
};
329+
330+
// ------------------------------
331+
323332
if self.with_stroke && corner_response.is_some() {
324333
let rect = Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size);
325334
let rect = rect.expand(2.0); // breathing room for content

0 commit comments

Comments
 (0)