mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 12:50:04 -04:00
Merge branch 'main' into theme_plugin
# Conflicts: # crates/egui/src/widget_style.rs
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
name = "egui_tests"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::ScrollArea;
|
||||
use egui::accesskit::Role;
|
||||
#[cfg(debug_assertions)]
|
||||
use egui::epaint::Shape;
|
||||
use egui::style::ScrollAnimation;
|
||||
use egui::text::{LayoutJob, TextWrapping};
|
||||
use egui::{
|
||||
Align, Button, Color32, FontFamily, FontId, Image, Label, Layout, RichText, Sense, TextBuffer,
|
||||
TextFormat, TextWrapMode, Ui, include_image, vec2,
|
||||
Align, Button, Color32, FontFamily, FontId, Image, Label, Layout, Rect, RichText, Sense,
|
||||
TextBuffer, TextFormat, TextWrapMode, Ui, Vec2, include_image, vec2,
|
||||
};
|
||||
use egui::{Pos2, ScrollArea};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::{NodeT as _, Queryable as _};
|
||||
|
||||
@@ -316,7 +316,7 @@ fn warn_if_rect_changes_id() {
|
||||
#[test]
|
||||
#[cfg(debug_assertions)]
|
||||
fn warn_if_rect_changes_id_false_positive_parent_shift() {
|
||||
use std::cell::Cell;
|
||||
use core::cell::Cell;
|
||||
|
||||
let counter = Cell::new(0);
|
||||
let button_rect = egui::Rect::from_min_size(egui::pos2(10.0, 10.0), egui::vec2(100.0, 30.0));
|
||||
@@ -481,3 +481,197 @@ fn animated_scroll_beats_sticky_bottom() {
|
||||
"animated explicit scroll should leave the sticky bottom"
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that tooltips are shown correctly for buttons that are only shown on hover.
|
||||
///
|
||||
/// Basically, this tests that a tooltip overlapping the mouse cursor does not interfere with a
|
||||
/// buttons hover state.
|
||||
#[test]
|
||||
fn tooltip_should_work_for_hover_button() {
|
||||
let button_rect = Rect::from_min_size(Pos2::new(4.0, 4.0), Vec2::new(80.0, 20.0));
|
||||
let mut harness = Harness::builder().with_size((320.0, 80.0)).build_ui(|ui| {
|
||||
if ui.rect_contains_pointer(button_rect) {
|
||||
ui.button("A tooltip should be shown")
|
||||
.on_hover_text("My tooltip");
|
||||
}
|
||||
});
|
||||
|
||||
harness.hover_at(button_rect.center());
|
||||
|
||||
harness.run();
|
||||
|
||||
harness.snapshot("test_tooltip_hover_regression");
|
||||
}
|
||||
|
||||
/// Ensure that hovering close to a widget doesn't cause a tooltip feedback loop (due to a
|
||||
/// difference between `hovered` and `contains_pointer` caused by the interact radius).
|
||||
#[test]
|
||||
fn tooltip_covering_button_should_not_cause_feedback_loop() {
|
||||
let mut harness = Harness::builder().with_size((200.0, 30.0)).build_ui(|ui| {
|
||||
ui.button("A tooltip should be shown")
|
||||
.on_hover_text("This tooltip is larger than the button");
|
||||
});
|
||||
|
||||
harness.hover_at(
|
||||
harness
|
||||
.get_by_label("A tooltip should be shown")
|
||||
.rect()
|
||||
.left_center()
|
||||
- Vec2::X,
|
||||
);
|
||||
|
||||
harness.run();
|
||||
|
||||
harness.snapshot("tooltip_covering_button_should_not_cause_feedback_loop");
|
||||
}
|
||||
|
||||
/// Tests that a tooltip closes when the pointer moves onto a neighboring widget,
|
||||
/// so that the neighbor can show its own tooltip.
|
||||
///
|
||||
/// The two buttons are only `item_spacing.y` (3 pt) apart, which is less than the
|
||||
/// hit-test `interact_radius` (5 pt), so the first button is still close enough to
|
||||
/// interact with when the pointer is on the second one.
|
||||
#[test]
|
||||
fn tooltip_should_hand_over_to_neighboring_widget() {
|
||||
let mut harness = Harness::builder().with_size((300.0, 200.0)).build_ui(|ui| {
|
||||
ui.button("Button A").on_hover_text("Tooltip A");
|
||||
ui.button("Button B").on_hover_text("Tooltip B");
|
||||
});
|
||||
|
||||
let a_rect = harness.get_by_label("Button A").rect();
|
||||
let b_rect = harness.get_by_label("Button B").rect();
|
||||
|
||||
harness.hover_at(a_rect.center_bottom() - Vec2::Y);
|
||||
harness.run();
|
||||
assert!(
|
||||
harness.query_by_label("Tooltip A").is_some(),
|
||||
"Tooltip A should be shown when hovering Button A"
|
||||
);
|
||||
|
||||
harness.hover_at(b_rect.center_top() + Vec2::Y);
|
||||
harness.run();
|
||||
assert!(
|
||||
harness.query_by_label("Tooltip B").is_some(),
|
||||
"Tooltip B should be shown when hovering Button B"
|
||||
);
|
||||
assert!(
|
||||
harness.query_by_label("Tooltip A").is_none(),
|
||||
"Tooltip A should be hidden when hovering Button B"
|
||||
);
|
||||
}
|
||||
|
||||
/// When a window is minimized or occluded, the integration runs no pass at all,
|
||||
/// and instead ticks the app logic with [`egui::Context::run_logic`].
|
||||
///
|
||||
/// Such a tick must leave all ui state alone. Otherwise areas think they were hidden and
|
||||
/// replay their fade-in, popups close, focus is lost, and child viewports pop back up.
|
||||
/// See <https://github.com/emilk/egui/issues/8266>.
|
||||
#[test]
|
||||
fn run_logic_should_not_disturb_ui_state() {
|
||||
const MENU: &str = "My menu";
|
||||
const MENU_ITEM: &str = "Button in my menu";
|
||||
const FOCUSED_BUTTON: &str = "Click me";
|
||||
|
||||
let child_viewport = egui::ViewportId::from_hash_of("My child viewport");
|
||||
let area_id = egui::Id::new("My area");
|
||||
let area_layer = egui::LayerId::new(egui::Order::Middle, area_id);
|
||||
|
||||
let mut harness = Harness::builder()
|
||||
.with_size(Vec2::new(400.0, 300.0))
|
||||
.build_ui(move |ui| {
|
||||
// A backend that can open real windows, like eframe:
|
||||
ui.ctx().set_embed_viewports(false);
|
||||
|
||||
ui.ctx()
|
||||
.show_viewport_deferred(child_viewport, Default::default(), |_ui, _class| {});
|
||||
|
||||
ui.menu_button(MENU, |ui| {
|
||||
_ = ui.button(MENU_ITEM);
|
||||
});
|
||||
|
||||
egui::Area::new(area_id)
|
||||
.fixed_pos((150.0, 120.0))
|
||||
.show(ui.ctx(), |ui| {
|
||||
_ = ui.button(FOCUSED_BUTTON);
|
||||
});
|
||||
});
|
||||
|
||||
harness.get_by_label(MENU).click();
|
||||
harness.run();
|
||||
// Nothing asks for focus again, so the test fails if egui ever loses it:
|
||||
harness.get_by_label(FOCUSED_BUTTON).focus();
|
||||
harness.run();
|
||||
|
||||
let assert_state = |harness: &Harness<'_>| {
|
||||
assert!(
|
||||
harness
|
||||
.get_by_label(FOCUSED_BUTTON)
|
||||
.accesskit_node()
|
||||
.is_focused(),
|
||||
"The button lost focus"
|
||||
);
|
||||
harness.get_by_label(MENU_ITEM); // Panics if the menu closed
|
||||
assert!(
|
||||
harness
|
||||
.ctx
|
||||
.memory(|m| m.areas().visible_last_frame(&area_layer)),
|
||||
"Area state was reset"
|
||||
);
|
||||
assert!(
|
||||
harness
|
||||
.ctx
|
||||
.viewport_for(child_viewport, |viewport| viewport.class)
|
||||
== egui::ViewportClass::Deferred,
|
||||
"The child viewport was closed"
|
||||
);
|
||||
};
|
||||
|
||||
assert_state(&harness);
|
||||
|
||||
// The window is now occluded, so the integration runs no pass,
|
||||
// and only ticks the app logic:
|
||||
for i in 0..2 {
|
||||
let time = 100.0 + f64::from(i);
|
||||
let mut raw_input = egui::RawInput {
|
||||
time: Some(time),
|
||||
..Default::default()
|
||||
};
|
||||
raw_input
|
||||
.viewports
|
||||
.entry(egui::ViewportId::ROOT)
|
||||
.or_default()
|
||||
.occluded = Some(true);
|
||||
|
||||
let output = harness.ctx.run_logic(&raw_input, |ctx| {
|
||||
assert_eq!(
|
||||
ctx.input(|i| i.viewport().occluded),
|
||||
Some(true),
|
||||
"App logic should be able to tell that the window is occluded"
|
||||
);
|
||||
assert!(
|
||||
ctx.input(|i| i.time) != time,
|
||||
"The ui input should not be interpreted: it is for the next pass"
|
||||
);
|
||||
|
||||
// The app asks to be shown again:
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
output
|
||||
.viewport_commands
|
||||
.into_values()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>(),
|
||||
vec![egui::ViewportCommand::Focus],
|
||||
"The integration should receive the command, even though there was no pass"
|
||||
);
|
||||
|
||||
assert_state(&harness);
|
||||
}
|
||||
|
||||
// The window is visible again, and everything should be where we left it:
|
||||
harness.run();
|
||||
|
||||
assert_state(&harness);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eba6e690937fbbd22c8edfce14078f50998e968324d8073d5db5829493d957e0
|
||||
oid sha256:9a21708315ae4514c1ff2c71075ccce2bef4627720808e4eebc9d7922388a17f
|
||||
size 5292
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:84e6030760561e308a190d2eb9781f53b896fbea5d11a3548b73d68c49f4d525
|
||||
oid sha256:a0c8d12d6f3741d33b993b5390fba5b845778a1f4dcf6d661513e65243978e18
|
||||
size 65797
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:84e6030760561e308a190d2eb9781f53b896fbea5d11a3548b73d68c49f4d525
|
||||
oid sha256:a0c8d12d6f3741d33b993b5390fba5b845778a1f4dcf6d661513e65243978e18
|
||||
size 65797
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:71c6c7299aad708fd16b945bcc9c82f529a276486eb1b2042f2ebaa932935a54
|
||||
size 3516
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bc0393c0b85df389314d15161b14803fde138eb66ef2b055335aef275ac03d1c
|
||||
size 3538
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2bbf9c826403a00937f9e37cccc76b16dc67db8aef3536bf076922fdd2d564d5
|
||||
size 4881
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cf09470a6628e62421bfa754ce238dd13820ba0bf8146ae835e539874d39a150
|
||||
size 4882
|
||||
oid sha256:054c9005865ff2d6c1ee6c76539453fbad6005d1c62693de5653c4bb00164322
|
||||
size 4881
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bbe8eda2e6162220ff8baa6e84156ea18f0bb15ffc46ebf9d60b6d3eeb8861d4
|
||||
size 9016
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:547316973b563bd92841f428838848323d656688cd03e8babcbc24626a389419
|
||||
size 7195
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b18774a40c1c1065645be7b59c90e65bb6fb40d4931ab37ccf40126e2767b709
|
||||
size 9019
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0e9ffcb9f14f86155a4a8279d694ae3e43c6bb8df59fa681673199c01ff68f96
|
||||
size 7206
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:59bef0e3593896985988c03171c710d8ed31ed8bb89c7a3f63c060c573e4fb74
|
||||
size 6510
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed2f80921ab864b0df3303834a8ab943a3aa5e10456896991c2dca81be0c4968
|
||||
size 3968
|
||||
@@ -225,8 +225,8 @@ fn test_atom_selectable_senses_click_and_drag() {
|
||||
/// See <https://github.com/emilk/egui/issues/8217>.
|
||||
#[test]
|
||||
fn test_atom_selectable_text_can_be_copied() {
|
||||
use core::cell::Cell;
|
||||
use egui::{AtomLayout, Event, Modifiers, OutputCommand, PointerButton, Pos2, Rect};
|
||||
use std::cell::Cell;
|
||||
|
||||
fn copied_text(selectable: bool) -> Option<String> {
|
||||
let rect_cell = Cell::new(Rect::NOTHING);
|
||||
|
||||
234
tests/egui_tests/tests/test_click_or_drag.rs
Normal file
234
tests/egui_tests/tests/test_click_or_drag.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
//! Tests for how egui decides whether a press on a click-and-drag widget
|
||||
//! is a click or a drag.
|
||||
|
||||
use egui::{Id, InputOptions, Pos2, Rect, Sense, Style, Vec2};
|
||||
use egui_kittest::Harness;
|
||||
|
||||
/// How far the pointer may move before a press is decidedly a drag.
|
||||
fn max_click_dist() -> f32 {
|
||||
InputOptions::default().max_click_dist
|
||||
}
|
||||
|
||||
/// How far outside its rect a widget can still be hit.
|
||||
fn interact_radius() -> f32 {
|
||||
Style::default().interaction.interact_radius
|
||||
}
|
||||
|
||||
fn widget_id() -> Id {
|
||||
Id::new("click_and_drag")
|
||||
}
|
||||
|
||||
/// A harness with one click-and-drag widget of the given size at the top-left.
|
||||
///
|
||||
/// If `with_background`, a second click-and-drag widget covers the whole area
|
||||
/// _beneath_ it. That one matters: without something under the pointer to take
|
||||
/// over the hover, the first widget keeps it even after the pointer leaves.
|
||||
///
|
||||
/// Steps at 60Hz. The default `step_dt` of 0.25s would blow past
|
||||
/// `max_click_duration` within a couple of frames, turning every press into a
|
||||
/// drag before the distance rules get a chance to matter.
|
||||
fn harness_with_widget(size: Vec2, with_background: bool) -> Harness<'static, ()> {
|
||||
Harness::builder()
|
||||
.with_step_dt(1.0 / 60.0)
|
||||
.with_size(Vec2::new(300.0, 200.0))
|
||||
.build_ui(move |ui| {
|
||||
if with_background {
|
||||
// Allocated first, so it ends up _behind_ the widget under test.
|
||||
ui.interact(
|
||||
ui.max_rect(),
|
||||
Id::new("background"),
|
||||
Sense::click_and_drag(),
|
||||
);
|
||||
}
|
||||
let rect = Rect::from_min_size(ui.max_rect().min, size);
|
||||
ui.interact(rect, widget_id(), Sense::click_and_drag());
|
||||
})
|
||||
}
|
||||
|
||||
/// The widget's `(hovered, dragged)` as of the last completed pass.
|
||||
fn widget_state(harness: &Harness<'_, ()>) -> (bool, bool) {
|
||||
harness
|
||||
.ctx
|
||||
.read_response(widget_id())
|
||||
.map(|r| (r.hovered(), r.dragged()))
|
||||
.expect("the widget should have been registered")
|
||||
}
|
||||
|
||||
/// The widget's rect as of the last completed pass.
|
||||
fn widget_rect(harness: &Harness<'_, ()>) -> Rect {
|
||||
harness
|
||||
.ctx
|
||||
.read_response(widget_id())
|
||||
.expect("the widget should have been registered")
|
||||
.rect
|
||||
}
|
||||
|
||||
/// Press the primary button at `pos`, without releasing it.
|
||||
fn press_at(harness: &mut Harness<'_, ()>, pos: Pos2) {
|
||||
harness.hover_at(pos);
|
||||
harness.step();
|
||||
harness.drag_at(pos);
|
||||
harness.step();
|
||||
}
|
||||
|
||||
/// Once a release can no longer land on the widget, the press can no longer become
|
||||
/// a click, so it counts as a drag right away — without waiting for `max_click_dist`.
|
||||
///
|
||||
/// This matters for widgets thinner than `max_click_dist` (panel resize handles,
|
||||
/// say): waiting would leave them neither hovered nor dragged for a few frames,
|
||||
/// which shows up as a flickering highlight.
|
||||
#[test]
|
||||
fn press_that_leaves_a_thin_widget_becomes_a_drag_immediately() {
|
||||
let width = max_click_dist() / 2.0; // thinner than `max_click_dist`
|
||||
let mut harness = harness_with_widget(Vec2::new(width, 100.0), true);
|
||||
harness.step();
|
||||
|
||||
let grab = widget_rect(&harness).center();
|
||||
press_at(&mut harness, grab);
|
||||
|
||||
let (hovered, dragged) = widget_state(&harness);
|
||||
assert!(hovered && !dragged, "the press starts out undecided");
|
||||
|
||||
// Creep outward in 1px steps, never reaching `max_click_dist` —
|
||||
// if we did, `is_decidedly_dragging` would explain the drag on its own
|
||||
// and the test would prove nothing.
|
||||
let mut saw_drag = false;
|
||||
for step in 1..max_click_dist().ceil() as i32 {
|
||||
let offset = step as f32;
|
||||
harness.hover_at(Pos2::new(grab.x + offset, grab.y));
|
||||
harness.step();
|
||||
|
||||
let (hovered, dragged) = widget_state(&harness);
|
||||
saw_drag |= dragged;
|
||||
assert!(
|
||||
hovered || dragged,
|
||||
"at +{offset}px the widget was neither hovered nor dragged, \
|
||||
so anything highlighting on `hovered || dragged` would blink out"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
saw_drag,
|
||||
"leaving the widget should have started a drag, even within max_click_dist"
|
||||
);
|
||||
}
|
||||
|
||||
/// While the pointer is still on the widget, a press stays undecided: hovered,
|
||||
/// but not yet dragged, so it can still become a click.
|
||||
#[test]
|
||||
fn press_inside_a_wide_widget_stays_undecided() {
|
||||
let mut harness = harness_with_widget(Vec2::new(100.0, 100.0), true);
|
||||
harness.step();
|
||||
|
||||
let grab = widget_rect(&harness).center();
|
||||
press_at(&mut harness, grab);
|
||||
|
||||
// A small twitch: inside the widget, and inside `max_click_dist`.
|
||||
harness.hover_at(Pos2::new(grab.x + max_click_dist() / 2.0, grab.y));
|
||||
harness.step();
|
||||
|
||||
let (hovered, dragged) = widget_state(&harness);
|
||||
assert!(hovered, "the pointer is still over the widget");
|
||||
assert!(
|
||||
!dragged,
|
||||
"a small twitch inside the widget should still be able to become a click"
|
||||
);
|
||||
}
|
||||
|
||||
/// A press just _outside_ the widget still hits it, thanks to `interact_radius`.
|
||||
/// The pointer hasn't moved at all, so this must not count as leaving the widget.
|
||||
#[test]
|
||||
fn press_just_outside_a_widget_stays_undecided() {
|
||||
// No background: we want the widget to win the hit-test from a distance.
|
||||
let mut harness = harness_with_widget(Vec2::new(100.0, 100.0), false);
|
||||
harness.step();
|
||||
|
||||
let rect = widget_rect(&harness);
|
||||
let offset = interact_radius() - 1.0;
|
||||
let grab = Pos2::new(rect.right() + offset, rect.center().y);
|
||||
press_at(&mut harness, grab);
|
||||
|
||||
let (hovered, dragged) = widget_state(&harness);
|
||||
assert!(
|
||||
hovered,
|
||||
"a press within interact_radius still hits the widget"
|
||||
);
|
||||
assert!(
|
||||
!dragged,
|
||||
"the pointer never moved, so this press must still be able to become a click"
|
||||
);
|
||||
}
|
||||
|
||||
/// A press and release inside the widget is still a click, not a drag.
|
||||
#[test]
|
||||
fn click_inside_a_widget_still_clicks() {
|
||||
let mut harness = harness_with_widget(Vec2::new(100.0, 100.0), true);
|
||||
harness.step();
|
||||
|
||||
let grab = widget_rect(&harness).center();
|
||||
press_at(&mut harness, grab);
|
||||
// Release without `drop_at`, which would also fire `PointerGone` and so
|
||||
// discard the click.
|
||||
harness.event(egui::Event::PointerButton {
|
||||
pos: grab,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: false,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
});
|
||||
harness.step();
|
||||
|
||||
assert!(
|
||||
harness
|
||||
.ctx
|
||||
.read_response(widget_id())
|
||||
.is_some_and(|r| r.clicked()),
|
||||
"press and release without moving should be a click"
|
||||
);
|
||||
}
|
||||
|
||||
/// A button inside a draggable row takes the click hit, because it is on top.
|
||||
/// The pointer is still inside the row though, so the press must stay undecided —
|
||||
/// otherwise the row starts dragging the moment the user touches the button.
|
||||
#[test]
|
||||
fn press_on_a_button_inside_a_draggable_row_stays_undecided() {
|
||||
let button_id = Id::new("button");
|
||||
let button_size = Vec2::new(50.0, 20.0);
|
||||
|
||||
let mut harness = Harness::builder()
|
||||
.with_step_dt(1.0 / 60.0)
|
||||
.with_size(Vec2::new(300.0, 200.0))
|
||||
.build_ui(move |ui| {
|
||||
let row_rect = ui.max_rect();
|
||||
ui.interact(row_rect, widget_id(), Sense::click_and_drag());
|
||||
|
||||
// Allocated after the row, so it ends up _on top_ of it.
|
||||
let button_rect = Rect::from_min_size(row_rect.min, button_size);
|
||||
ui.interact(button_rect, button_id, Sense::click());
|
||||
});
|
||||
harness.step();
|
||||
|
||||
let grab = Rect::from_min_size(widget_rect(&harness).min, button_size).center();
|
||||
press_at(&mut harness, grab);
|
||||
|
||||
let (_hovered, dragged) = widget_state(&harness);
|
||||
assert!(
|
||||
!dragged,
|
||||
"pressing a button inside the row must not start dragging the row"
|
||||
);
|
||||
|
||||
harness.event(egui::Event::PointerButton {
|
||||
pos: grab,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: false,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
});
|
||||
harness.step();
|
||||
|
||||
assert!(
|
||||
harness
|
||||
.ctx
|
||||
.read_response(button_id)
|
||||
.is_some_and(|r| r.clicked()),
|
||||
"the button should have been clicked"
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
//!
|
||||
//! Covers:
|
||||
//! * [`Panel::show_collapsible`] — drag-to-close on a `Left` panel.
|
||||
//! * [`Panel::show_collapsible`] — drag-to-open via the grab handle a fully
|
||||
//! collapsed panel leaves behind, plus [`Panel::drag_to_open`] opting out of it.
|
||||
//! * [`Panel::show_switched`] — drag-to-close on the expanded panel
|
||||
//! followed by drag-to-expand on the collapsed panel, both via the shared
|
||||
//! resize handle.
|
||||
@@ -13,6 +15,15 @@ use egui_kittest::{Harness, SnapshotResults};
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
is_expanded: bool,
|
||||
|
||||
/// The panel's live _outer_ width, recorded each pass.
|
||||
///
|
||||
/// `None` while the panel is fully collapsed.
|
||||
///
|
||||
/// We can't read this back from [`egui::PanelState`], because a panel
|
||||
/// deliberately doesn't persist its size while its resize handle is being
|
||||
/// dragged — which is exactly when these tests need to observe it.
|
||||
panel_width: Option<f32>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -34,7 +45,10 @@ fn drag_to_close_animated_inside() {
|
||||
ui.label("Central");
|
||||
});
|
||||
},
|
||||
State { is_expanded: true },
|
||||
State {
|
||||
is_expanded: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
harness.run();
|
||||
@@ -65,6 +79,156 @@ fn drag_to_close_animated_inside() {
|
||||
results.add(harness.try_snapshot("panel_drag/inside_closed"));
|
||||
}
|
||||
|
||||
/// The size range of the collapsible left panel used by the drag-to-open tests.
|
||||
const MIN_SIZE: f32 = 60.0;
|
||||
const DEFAULT_SIZE: f32 = 80.0;
|
||||
|
||||
/// A harness with a single collapsible, resizable left panel.
|
||||
///
|
||||
/// `drag_to_open` is passed straight through to [`Panel::drag_to_open`].
|
||||
fn collapsible_left_panel_harness(drag_to_open: bool) -> Harness<'static, State> {
|
||||
Harness::builder()
|
||||
.with_size(Vec2::new(400.0, 200.0))
|
||||
.build_ui_state(
|
||||
move |ui, state: &mut State| {
|
||||
let response = Panel::left("test_left_panel")
|
||||
.resizable(true)
|
||||
.drag_to_open(drag_to_open)
|
||||
.default_size(DEFAULT_SIZE)
|
||||
.min_size(MIN_SIZE)
|
||||
.show_collapsible(ui, &mut state.is_expanded, |ui| {
|
||||
ui.label("Left panel content");
|
||||
// Without this the frame shrinks to fit the label, and the
|
||||
// panel's rect would report the content width instead of
|
||||
// the width the panel was resized to.
|
||||
ui.take_available_space();
|
||||
});
|
||||
state.panel_width = response.map(|response| response.response.rect.width());
|
||||
egui::CentralPanel::default().show(ui, |ui| {
|
||||
ui.label("Central");
|
||||
});
|
||||
},
|
||||
State {
|
||||
is_expanded: true,
|
||||
panel_width: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// The panel's live _outer_ width, as of the last completed pass.
|
||||
fn panel_width(harness: &Harness<'_, State>) -> f32 {
|
||||
harness
|
||||
.state()
|
||||
.panel_width
|
||||
.expect("the panel should be showing")
|
||||
}
|
||||
|
||||
/// Collapse the panel by dragging its resize edge past `min_size`, and return the
|
||||
/// panel's fixed (left) edge — where the grab handle it leaves behind sits.
|
||||
fn collapse_by_drag(harness: &mut Harness<'_, State>) -> Pos2 {
|
||||
harness.run();
|
||||
assert!(harness.state().is_expanded, "should start expanded");
|
||||
|
||||
// Query the actual resize edge from PanelState (avoids assumptions about
|
||||
// Frame margins and the harness's ui padding).
|
||||
let panel_state = egui::PanelState::load(&harness.ctx, egui::Id::new("test_left_panel"))
|
||||
.expect("PanelState should be persisted after the first frame");
|
||||
let fixed_edge = Pos2::new(
|
||||
panel_state.outer_rect.left(),
|
||||
panel_state.outer_rect.center().y,
|
||||
);
|
||||
|
||||
let drag_start = Pos2::new(panel_state.outer_rect.right(), fixed_edge.y);
|
||||
let drag_end = Pos2::new(drag_start.x - 200.0, fixed_edge.y);
|
||||
|
||||
harness.drag_at(drag_start);
|
||||
harness.run();
|
||||
harness.hover_at(drag_end);
|
||||
harness.run();
|
||||
harness.drop_at(drag_end);
|
||||
harness.run();
|
||||
|
||||
assert!(
|
||||
!harness.state().is_expanded,
|
||||
"drag past min_size should have closed the panel"
|
||||
);
|
||||
|
||||
// Move the pointer away so the handle isn't left hovered.
|
||||
harness.hover_at(Pos2::new(300.0, fixed_edge.y));
|
||||
harness.run();
|
||||
|
||||
fixed_edge
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drag_to_open_collapsed_panel() {
|
||||
let mut results = SnapshotResults::new();
|
||||
|
||||
let mut harness = collapsible_left_panel_harness(true);
|
||||
let fixed_edge = collapse_by_drag(&mut harness);
|
||||
// Grab just inside the fixed edge, where the handle is.
|
||||
let handle_pos = fixed_edge + Vec2::new(1.0, 0.0);
|
||||
|
||||
// The handle is invisible until hovered:
|
||||
results.add(harness.try_snapshot("panel_drag/collapsed_handle_idle"));
|
||||
|
||||
harness.hover_at(handle_pos);
|
||||
harness.run();
|
||||
results.add(harness.try_snapshot("panel_drag/collapsed_handle_hovered"));
|
||||
|
||||
// Dragging out but not as far as `min_size` must not reopen the panel.
|
||||
harness.drag_at(handle_pos);
|
||||
harness.run();
|
||||
let short_of_min = Pos2::new(fixed_edge.x + MIN_SIZE - 10.0, fixed_edge.y);
|
||||
harness.hover_at(short_of_min);
|
||||
harness.run();
|
||||
assert!(
|
||||
!harness.state().is_expanded,
|
||||
"dragging out less than min_size should not reopen the panel"
|
||||
);
|
||||
|
||||
// …but continuing past `min_size` should, without releasing the drag. The
|
||||
// panel opens at the size the pointer is already at, so it never jumps ahead.
|
||||
let past_min = Pos2::new(fixed_edge.x + MIN_SIZE + 20.0, fixed_edge.y);
|
||||
harness.hover_at(past_min);
|
||||
harness.run();
|
||||
assert!(
|
||||
harness.state().is_expanded,
|
||||
"dragging out past min_size should have reopened the panel"
|
||||
);
|
||||
assert_eq!(
|
||||
panel_width(&harness),
|
||||
past_min.x - fixed_edge.x,
|
||||
"the reopened panel's edge should sit under the pointer"
|
||||
);
|
||||
|
||||
harness.drop_at(past_min);
|
||||
harness.run();
|
||||
assert!(
|
||||
harness.state().is_expanded,
|
||||
"the panel should stay open after the drag is released"
|
||||
);
|
||||
results.add(harness.try_snapshot("panel_drag/collapsed_handle_reopened"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drag_to_open_can_be_opted_out_of() {
|
||||
let mut harness = collapsible_left_panel_harness(false);
|
||||
let handle_pos = collapse_by_drag(&mut harness) + Vec2::new(1.0, 0.0);
|
||||
|
||||
harness.drag_at(handle_pos);
|
||||
harness.run();
|
||||
harness.hover_at(Pos2::new(handle_pos.x + 150.0, handle_pos.y));
|
||||
harness.run();
|
||||
harness.drop_at(Pos2::new(handle_pos.x + 150.0, handle_pos.y));
|
||||
harness.run();
|
||||
|
||||
assert!(
|
||||
!harness.state().is_expanded,
|
||||
"with `drag_to_open(false)` there should be no grab handle to reopen the panel with"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drag_to_close_and_reopen_animated_between() {
|
||||
let mut results = SnapshotResults::new();
|
||||
@@ -108,7 +272,10 @@ fn drag_to_close_and_reopen_animated_between() {
|
||||
ui.label("Central");
|
||||
});
|
||||
},
|
||||
State { is_expanded: true },
|
||||
State {
|
||||
is_expanded: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
harness.run();
|
||||
@@ -155,3 +322,174 @@ fn drag_to_close_and_reopen_animated_between() {
|
||||
);
|
||||
results.add(harness.try_snapshot("panel_drag/between_reopened"));
|
||||
}
|
||||
|
||||
/// State for the animated-close test: records the panel's live top edge.
|
||||
#[derive(Default)]
|
||||
struct SwitchedState {
|
||||
is_expanded: bool,
|
||||
|
||||
/// Bottom of whatever space is left after the panel — i.e. the top edge of
|
||||
/// the panel that is currently showing.
|
||||
///
|
||||
/// Read from the ui rather than [`egui::PanelState`], which a panel doesn't
|
||||
/// persist while its resize handle is held.
|
||||
panel_top: f32,
|
||||
}
|
||||
|
||||
/// The sizes a `show_switched` bottom panel moves between in these tests.
|
||||
///
|
||||
/// The expanded minimum sits well above the collapsed size, so the gap between
|
||||
/// the two shows up in the panel's edge.
|
||||
const SWITCHED_COLLAPSED_SIZE: f32 = 20.0;
|
||||
const SWITCHED_EXPANDED_MIN: f32 = 80.0;
|
||||
|
||||
fn switched_bottom_panel_harness(start_expanded: bool) -> Harness<'static, SwitchedState> {
|
||||
let mut harness = Harness::builder()
|
||||
.with_size(Vec2::new(400.0, 300.0))
|
||||
.with_step_dt(1.0 / 60.0)
|
||||
.build_ui_state(
|
||||
move |ui, state: &mut SwitchedState| {
|
||||
Panel::show_switched(
|
||||
ui,
|
||||
&mut state.is_expanded,
|
||||
Panel::bottom("switched_collapsed")
|
||||
.resizable(true)
|
||||
.exact_size(SWITCHED_COLLAPSED_SIZE),
|
||||
Panel::bottom("switched_expanded")
|
||||
.resizable(true)
|
||||
.default_size(160.0)
|
||||
.min_size(SWITCHED_EXPANDED_MIN)
|
||||
.max_size(250.0),
|
||||
|ui, _expanded| ui.take_available_space(),
|
||||
);
|
||||
state.panel_top = ui.available_rect_before_wrap().bottom();
|
||||
egui::CentralPanel::default().show(ui, |_ui| {});
|
||||
},
|
||||
SwitchedState {
|
||||
is_expanded: start_expanded,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
// kittest disables animations by default, and these tests are about one.
|
||||
harness
|
||||
.ctx
|
||||
.all_styles_mut(|style| style.animation_time = 0.25);
|
||||
for _ in 0..4 {
|
||||
harness.step();
|
||||
}
|
||||
harness
|
||||
}
|
||||
|
||||
/// Assert that the panel edge crossed `gap` gradually, rather than in one frame.
|
||||
fn assert_crossed_gradually(tops: &[f32], gap: core::ops::Range<f32>) {
|
||||
let frames_in_gap = tops.iter().filter(|top| gap.contains(top)).count();
|
||||
assert!(
|
||||
3 <= frames_in_gap,
|
||||
"expected the panel to be animated across the gap between the collapsed \
|
||||
size and the expanded min_size, but only {frames_in_gap} frame(s) landed \
|
||||
inside {gap:?}: {tops:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Dragging the expanded panel shut animates it the rest of the way, rather than
|
||||
/// snapping, even while the drag is still held.
|
||||
///
|
||||
/// The expanded panel can't shrink past its own `min_size`, so a drag that goes
|
||||
/// below that leaves a gap between where the panel is stuck and the collapsed
|
||||
/// panel's size. That gap has to be animated, or the panel jumps.
|
||||
#[test]
|
||||
fn drag_to_close_switched_animates_while_held() {
|
||||
let collapsed_size = SWITCHED_COLLAPSED_SIZE;
|
||||
let expanded_min = SWITCHED_EXPANDED_MIN;
|
||||
|
||||
let mut harness = switched_bottom_panel_harness(true);
|
||||
|
||||
let expanded = egui::PanelState::load(&harness.ctx, egui::Id::new("switched_expanded"))
|
||||
.expect("PanelState should be persisted after the first frame");
|
||||
let (x, bottom) = (expanded.outer_rect.center().x, expanded.outer_rect.bottom());
|
||||
let collapsed_top = bottom - collapsed_size;
|
||||
|
||||
// Drag the top edge down well past the collapsed size, and keep holding.
|
||||
harness.drag_at(Pos2::new(x, expanded.outer_rect.top()));
|
||||
harness.step();
|
||||
harness.hover_at(Pos2::new(x, bottom - 10.0));
|
||||
harness.step();
|
||||
|
||||
assert!(
|
||||
!harness.state().is_expanded,
|
||||
"dragging past the collapsed size should have collapsed the panel"
|
||||
);
|
||||
let top_at_collapse = harness.state().panel_top;
|
||||
assert_eq!(
|
||||
top_at_collapse,
|
||||
bottom - expanded_min,
|
||||
"the expanded panel should be stuck at its min_size when the collapse fires"
|
||||
);
|
||||
|
||||
// Follow the close, still holding the drag.
|
||||
let mut tops = vec![top_at_collapse];
|
||||
for _ in 0..40 {
|
||||
harness.step();
|
||||
tops.push(harness.state().panel_top);
|
||||
}
|
||||
|
||||
assert!(
|
||||
tops.windows(2).all(|w| w[0] <= w[1]),
|
||||
"the panel should only ever move towards being shut, never jump back open: {tops:?}"
|
||||
);
|
||||
assert!(
|
||||
(tops.last().copied().unwrap_or_default() - collapsed_top).abs() < 1.0,
|
||||
"the close should end at the collapsed panel's size, got {:?}",
|
||||
tops.last()
|
||||
);
|
||||
|
||||
// The gap between min_size and the collapsed size must be crossed over
|
||||
// several frames, not in one jump.
|
||||
assert_crossed_gradually(&tops, (top_at_collapse + 1.0)..(collapsed_top - 1.0));
|
||||
}
|
||||
|
||||
/// The mirror image: dragging the collapsed panel open animates across the same
|
||||
/// gap, instead of snapping straight out to the expanded panel's `min_size`.
|
||||
#[test]
|
||||
fn drag_to_open_switched_animates_while_held() {
|
||||
let mut harness = switched_bottom_panel_harness(false);
|
||||
|
||||
let collapsed = egui::PanelState::load(&harness.ctx, egui::Id::new("switched_collapsed"))
|
||||
.expect("PanelState should be persisted after the first frame");
|
||||
let (x, collapsed_top, bottom) = (
|
||||
collapsed.outer_rect.center().x,
|
||||
collapsed.outer_rect.top(),
|
||||
collapsed.outer_rect.bottom(),
|
||||
);
|
||||
let expanded_min_top = bottom - SWITCHED_EXPANDED_MIN;
|
||||
|
||||
// Nudge the collapsed panel's top edge out past its `exact_size` cap, and keep
|
||||
// holding. The pointer stays far short of the expanded panel's `min_size`.
|
||||
harness.drag_at(Pos2::new(x, collapsed_top));
|
||||
harness.step();
|
||||
harness.hover_at(Pos2::new(x, collapsed_top - 10.0));
|
||||
harness.step();
|
||||
|
||||
assert!(
|
||||
harness.state().is_expanded,
|
||||
"a small outward drag past the collapsed panel's cap should expand it"
|
||||
);
|
||||
|
||||
let mut tops = vec![harness.state().panel_top];
|
||||
for _ in 0..40 {
|
||||
harness.step();
|
||||
tops.push(harness.state().panel_top);
|
||||
}
|
||||
|
||||
assert!(
|
||||
tops.windows(2).all(|w| w[1] <= w[0]),
|
||||
"the panel should only ever grow, never jump back shut: {tops:?}"
|
||||
);
|
||||
assert!(
|
||||
(tops.last().copied().unwrap_or_default() - expanded_min_top).abs() < 1.0,
|
||||
"the panel should settle at the expanded min_size (top {expanded_min_top}), \
|
||||
since the pointer never got further out than that, got {:?}",
|
||||
tops.last()
|
||||
);
|
||||
assert_crossed_gradually(&tops, (expanded_min_top + 1.0)..(collapsed_top - 1.0));
|
||||
}
|
||||
|
||||
105
tests/egui_tests/tests/test_panel_separator_line.rs
Normal file
105
tests/egui_tests/tests/test_panel_separator_line.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
//! Snapshot tests for where a [`Panel`] puts its separator line, and how much room it reserves.
|
||||
//!
|
||||
//! Going outwards from the panel contents, the order is:
|
||||
//!
|
||||
//! contents | `Frame::inner_margin` | `Frame::stroke` | separator line | `Frame::outer_margin`
|
||||
//!
|
||||
//! i.e. the line is painted _outside_ the frame's outline, in room the panel reserves for it in the
|
||||
//! frame's outer margin. A panel that opted out of the separator line must not reserve that room,
|
||||
//! or it ends up with a permanently visible gap along that edge — even though it is `resizable` and
|
||||
//! therefore still shows a line while hovered or dragged.
|
||||
//!
|
||||
//! The snapshots span `show_separator_line` on/off × resize handle hovered/not. The panel uses a
|
||||
//! garish frame outline and separator colors so both are unmistakable, and its only content is a
|
||||
//! [`egui::SelectableLabel`] vertically centered in the panel: if the panel reserves room it
|
||||
//! shouldn't, the label drifts off center.
|
||||
|
||||
use egui::{Color32, CornerRadius, Frame, Margin, Panel, Pos2, Stroke, Vec2};
|
||||
use egui_kittest::{Harness, SnapshotResults};
|
||||
|
||||
/// [`Frame::fill`] of the test panel.
|
||||
const FILL: Color32 = Color32::from_rgb(20, 20, 40);
|
||||
|
||||
/// [`Frame::stroke`] color of the test panel.
|
||||
const OUTLINE: Color32 = Color32::from_rgb(255, 0, 255);
|
||||
|
||||
/// The dim, always-visible separator line (`noninteractive.bg_stroke`).
|
||||
const SEPARATOR: Color32 = Color32::from_rgb(0, 255, 0);
|
||||
|
||||
/// The bright separator line shown while the resize handle is hovered (`hovered.fg_stroke`).
|
||||
const HOVERED_SEPARATOR: Color32 = Color32::from_rgb(255, 255, 0);
|
||||
|
||||
const PANEL_ID: &str = "test_panel";
|
||||
|
||||
fn build_harness(show_separator_line: bool) -> Harness<'static> {
|
||||
let mut harness = Harness::builder()
|
||||
.with_size(Vec2::new(200.0, 120.0))
|
||||
// So the thin lines are legible to a human reviewing the snapshots:
|
||||
.with_pixels_per_point(2.0)
|
||||
.build_ui(move |ui| {
|
||||
// Loud, distinguishable colors, so we can tell the separator line, the frame outline
|
||||
// and the frame fill apart.
|
||||
let widgets = &mut ui.visuals_mut().widgets;
|
||||
widgets.noninteractive.bg_stroke = Stroke::new(1.0, SEPARATOR);
|
||||
widgets.hovered.fg_stroke = Stroke::new(1.0, HOVERED_SEPARATOR);
|
||||
|
||||
let frame = Frame::new()
|
||||
.fill(FILL)
|
||||
.stroke(Stroke::new(2.0, OUTLINE))
|
||||
.corner_radius(CornerRadius::ZERO)
|
||||
.inner_margin(Margin::same(4))
|
||||
.outer_margin(Margin::same(2));
|
||||
|
||||
Panel::top(PANEL_ID)
|
||||
.frame(frame)
|
||||
.resizable(true)
|
||||
.default_size(60.0)
|
||||
.show_separator_line(show_separator_line)
|
||||
.show(ui, |ui| {
|
||||
// Vertically centered in whatever room the panel gave us.
|
||||
ui.horizontal_centered(|ui| {
|
||||
let _ = ui.selectable_label(true, "Centered");
|
||||
});
|
||||
});
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(Frame::default().fill(Color32::GRAY))
|
||||
.show(ui, |ui| {
|
||||
ui.label("CentralPanel");
|
||||
});
|
||||
});
|
||||
harness.run();
|
||||
harness
|
||||
}
|
||||
|
||||
fn hover_resize_handle(harness: &mut Harness<'_>) {
|
||||
let outer = egui::PanelState::load(&harness.ctx, egui::Id::new(PANEL_ID))
|
||||
.expect("PanelState should be persisted after the first frame")
|
||||
.outer_rect;
|
||||
|
||||
// Hover just _inside_ the panel's inner edge, but still well within the resize grab radius:
|
||||
// the `CentralPanel` and its label start exactly at that edge, and would otherwise take the
|
||||
// hover from the resize handle.
|
||||
harness.hover_at(Pos2::new(outer.center().x, outer.bottom() - 1.0));
|
||||
harness.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn separator_line_matrix() {
|
||||
let mut results = SnapshotResults::new();
|
||||
|
||||
for show_separator_line in [false, true] {
|
||||
let suffix = if show_separator_line { "on" } else { "off" };
|
||||
|
||||
// Not hovered: the line is dim (`show_separator_line`) or absent.
|
||||
let mut harness = build_harness(show_separator_line);
|
||||
results.add(harness.try_snapshot(format!("panel_separator_line/separator_{suffix}_idle")));
|
||||
|
||||
// Hovered: a `resizable` panel shows a bright line regardless of `show_separator_line`,
|
||||
// and must not shift its contents to make room for it.
|
||||
let mut harness = build_harness(show_separator_line);
|
||||
hover_resize_handle(&mut harness);
|
||||
results
|
||||
.add(harness.try_snapshot(format!("panel_separator_line/separator_{suffix}_hovered")));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
#![expect(rustdoc::missing_crate_level_docs)]
|
||||
#![allow(clippy::print_stderr)]
|
||||
|
||||
use std::time::Duration;
|
||||
use core::time::Duration;
|
||||
|
||||
use eframe::egui::{self, ViewportInfo};
|
||||
|
||||
@@ -56,7 +56,7 @@ fn viewport_info(ctx: &egui::Context) -> String {
|
||||
];
|
||||
for (name, value) in flags {
|
||||
if let Some(value) = value {
|
||||
use std::fmt::Write as _;
|
||||
use core::fmt::Write as _;
|
||||
write!(s, " {name}={value}").ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
use eframe::egui;
|
||||
use eframe::glow;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn main() -> Result<(), Box<dyn core::error::Error>> {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
let options = eframe::NativeOptions {
|
||||
renderer: eframe::Renderer::Glow,
|
||||
|
||||
@@ -451,17 +451,13 @@ fn drop_target<R>(
|
||||
) -> egui::InnerResponse<R> {
|
||||
let is_being_dragged = ui.ctx().dragged_id().is_some();
|
||||
|
||||
let margin = egui::Vec2::splat(ui.visuals().clip_rect_margin); // 3.0
|
||||
|
||||
let background_id = ui.painter().add(egui::Shape::Noop);
|
||||
|
||||
let available_rect = ui.available_rect_before_wrap();
|
||||
let inner_rect = available_rect.shrink2(margin);
|
||||
let mut content_ui = ui.new_child(UiBuilder::new().max_rect(inner_rect));
|
||||
let mut content_ui = ui.new_child(UiBuilder::new().max_rect(available_rect));
|
||||
let ret = body(&mut content_ui);
|
||||
|
||||
let outer_rect =
|
||||
egui::Rect::from_min_max(available_rect.min, content_ui.min_rect().max + margin);
|
||||
let outer_rect = egui::Rect::from_min_max(available_rect.min, content_ui.min_rect().max);
|
||||
let (rect, response) = ui.allocate_at_least(outer_rect.size(), egui::Sense::hover());
|
||||
|
||||
let style = if is_being_dragged && response.hovered() {
|
||||
|
||||
Reference in New Issue
Block a user