1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-28 07:23:13 -04:00
Files
egui/crates/egui_extras/src/layout.rs
Emil Ernerfeldt ca8c8792c2 New widget interaction logic (#4026)
* Closes https://github.com/emilk/egui/issues/3936
* Closes https://github.com/emilk/egui/issues/3923
* Closes https://github.com/emilk/egui/pull/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 https://github.com/emilk/egui/issues/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.
2024-02-17 15:48:23 +01:00

219 lines
6.5 KiB
Rust

use egui::{Pos2, Rect, Response, Sense, Ui};
#[derive(Clone, Copy)]
pub(crate) enum CellSize {
/// Absolute size in points
Absolute(f32),
/// Take all available space
Remainder,
}
/// Cells are positioned in two dimensions, cells go in one direction and form lines.
///
/// In a strip there's only one line which goes in the direction of the strip:
///
/// In a horizontal strip, a [`StripLayout`] with horizontal [`CellDirection`] is used.
/// Its cells go from left to right inside this [`StripLayout`].
///
/// In a table there's a [`StripLayout`] for each table row with a horizontal [`CellDirection`].
/// Its cells go from left to right. And the lines go from top to bottom.
pub(crate) enum CellDirection {
/// Cells go from left to right.
Horizontal,
/// Cells go from top to bottom.
Vertical,
}
/// Flags used by [`StripLayout::add`].
#[derive(Clone, Copy, Default)]
pub(crate) struct StripLayoutFlags {
pub(crate) clip: bool,
pub(crate) striped: bool,
pub(crate) hovered: bool,
pub(crate) selected: bool,
}
/// Positions cells in [`CellDirection`] and starts a new line on [`StripLayout::end_line`]
pub struct StripLayout<'l> {
pub(crate) ui: &'l mut Ui,
direction: CellDirection,
pub(crate) rect: Rect,
pub(crate) cursor: Pos2,
/// Keeps track of the max used position,
/// so we know how much space we used.
max: Pos2,
cell_layout: egui::Layout,
sense: Sense,
}
impl<'l> StripLayout<'l> {
pub(crate) fn new(
ui: &'l mut Ui,
direction: CellDirection,
cell_layout: egui::Layout,
sense: Sense,
) -> Self {
let rect = ui.available_rect_before_wrap();
let pos = rect.left_top();
Self {
ui,
direction,
rect,
cursor: pos,
max: pos,
cell_layout,
sense,
}
}
fn cell_rect(&self, width: &CellSize, height: &CellSize) -> Rect {
Rect {
min: self.cursor,
max: Pos2 {
x: match width {
CellSize::Absolute(width) => self.cursor.x + width,
CellSize::Remainder => self.rect.right(),
},
y: match height {
CellSize::Absolute(height) => self.cursor.y + height,
CellSize::Remainder => self.rect.bottom(),
},
},
}
}
fn set_pos(&mut self, rect: Rect) {
self.max.x = self.max.x.max(rect.right());
self.max.y = self.max.y.max(rect.bottom());
match self.direction {
CellDirection::Horizontal => {
self.cursor.x = rect.right() + self.ui.spacing().item_spacing.x;
}
CellDirection::Vertical => {
self.cursor.y = rect.bottom() + self.ui.spacing().item_spacing.y;
}
}
}
pub(crate) fn empty(&mut self, width: CellSize, height: CellSize) {
self.set_pos(self.cell_rect(&width, &height));
}
/// This is the innermost part of [`crate::Table`] and [`crate::Strip`].
///
/// Return the used space (`min_rect`) plus the [`Response`] of the whole cell.
pub(crate) fn add(
&mut self,
flags: StripLayoutFlags,
width: CellSize,
height: CellSize,
add_cell_contents: impl FnOnce(&mut Ui),
) -> (Rect, Response) {
let max_rect = self.cell_rect(&width, &height);
// Make sure we don't have a gap in the stripe/frame/selection background:
let item_spacing = self.ui.spacing().item_spacing;
let gapless_rect = max_rect.expand2(0.5 * item_spacing);
if flags.striped {
self.ui.painter().rect_filled(
gapless_rect,
egui::Rounding::ZERO,
self.ui.visuals().faint_bg_color,
);
}
if flags.selected {
self.ui.painter().rect_filled(
gapless_rect,
egui::Rounding::ZERO,
self.ui.visuals().selection.bg_fill,
);
}
if flags.hovered && !flags.selected && self.sense.interactive() {
self.ui.painter().rect_filled(
gapless_rect,
egui::Rounding::ZERO,
self.ui.visuals().widgets.hovered.bg_fill,
);
}
let used_rect = self.cell(flags, max_rect, add_cell_contents);
self.set_pos(max_rect);
let allocation_rect = if flags.clip {
max_rect
} else {
max_rect.union(used_rect)
};
let response = self.ui.allocate_rect(allocation_rect, self.sense);
(used_rect, response)
}
/// only needed for layouts with multiple lines, like [`Table`](crate::Table).
pub fn end_line(&mut self) {
match self.direction {
CellDirection::Horizontal => {
self.cursor.y = self.max.y + self.ui.spacing().item_spacing.y;
self.cursor.x = self.rect.left();
}
CellDirection::Vertical => {
self.cursor.x = self.max.x + self.ui.spacing().item_spacing.x;
self.cursor.y = self.rect.top();
}
}
}
/// Skip a lot of space.
pub(crate) fn skip_space(&mut self, delta: egui::Vec2) {
let before = self.cursor;
self.cursor += delta;
let rect = Rect::from_two_pos(before, self.cursor);
self.ui.allocate_rect(rect, Sense::hover());
}
fn cell(
&mut self,
flags: StripLayoutFlags,
rect: Rect,
add_cell_contents: impl FnOnce(&mut Ui),
) -> Rect {
let mut child_ui = self.ui.child_ui(rect, self.cell_layout);
if flags.clip {
let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin);
let margin = margin.min(0.5 * self.ui.spacing().item_spacing);
let clip_rect = rect.expand2(margin);
child_ui.set_clip_rect(clip_rect.intersect(child_ui.clip_rect()));
}
if flags.selected {
let stroke_color = child_ui.style().visuals.selection.stroke.color;
child_ui.style_mut().visuals.override_text_color = Some(stroke_color);
}
add_cell_contents(&mut child_ui);
child_ui.min_rect()
}
/// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size
pub fn allocate_rect(&mut self) -> Response {
let mut rect = self.rect;
rect.set_right(self.max.x);
rect.set_bottom(self.max.y);
self.ui.allocate_rect(rect, Sense::hover())
}
}