mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Window: move only by dragging title bar (#8183)
* Part of https://github.com/emilk/egui/issues/8180 So far, you've been able to move any `egui::Window` by dragging anywhere on it. This makes sense on touch screens with thick fingers, but less so on non-touch-screens. With this PR, you can now control it with a new enum `WindowDrag`
This commit is contained in:
@@ -583,7 +583,7 @@ impl Area {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn round_area_position(ctx: &Context, pos: Pos2) -> Pos2 {
|
pub(crate) fn round_area_position(ctx: &Context, pos: Pos2) -> Pos2 {
|
||||||
// We round a lot of rendering to pixels, so we round the whole
|
// We round a lot of rendering to pixels, so we round the whole
|
||||||
// area positions to pixels too, so avoid widgets appearing to float
|
// area positions to pixels too, so avoid widgets appearing to float
|
||||||
// around independently of each other when the area is dragged.
|
// around independently of each other when the area is dragged.
|
||||||
|
|||||||
@@ -32,5 +32,5 @@ pub use {
|
|||||||
scroll_area::ScrollArea,
|
scroll_area::ScrollArea,
|
||||||
sides::Sides,
|
sides::Sides,
|
||||||
tooltip::*,
|
tooltip::*,
|
||||||
window::Window,
|
window::{Window, WindowDrag},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,54 @@ use crate::*;
|
|||||||
use super::scroll_area::{DragScroll, ScrollBarVisibility, ScrollSource};
|
use super::scroll_area::{DragScroll, ScrollBarVisibility, ScrollSource};
|
||||||
use super::{Area, Frame, Resize, ScrollArea, area, resize};
|
use super::{Area, Frame, Resize, ScrollArea, area, resize};
|
||||||
|
|
||||||
|
/// Where the user can drag to move a [`Window`].
|
||||||
|
///
|
||||||
|
/// See [`Window::drag_area`].
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub enum WindowDrag {
|
||||||
|
/// Window cannot be moved by dragging.
|
||||||
|
///
|
||||||
|
/// [`Window::movable(false)`](Window::movable) forces this regardless of
|
||||||
|
/// what was passed to [`Window::drag_area`].
|
||||||
|
Off,
|
||||||
|
|
||||||
|
/// The user can drag the window from anywhere on its surface.
|
||||||
|
///
|
||||||
|
/// Good for touch screens, but can interfere with selecting / dragging
|
||||||
|
/// content inside the window when used with a mouse.
|
||||||
|
Anywhere,
|
||||||
|
|
||||||
|
/// Only the title bar accepts the move-drag gesture.
|
||||||
|
///
|
||||||
|
/// Windows without a title bar (see [`Window::title_bar`]) silently fall
|
||||||
|
/// back to [`Self::Anywhere`] — otherwise they'd be unmovable.
|
||||||
|
TitleBar,
|
||||||
|
|
||||||
|
/// [`Self::Anywhere`] when a touch screen is detected (see
|
||||||
|
/// [`crate::InputState::has_touch_screen`]); [`Self::TitleBar`] otherwise.
|
||||||
|
/// The recommended default.
|
||||||
|
#[default]
|
||||||
|
OnTouch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowDrag {
|
||||||
|
/// Resolve [`Self::OnTouch`] to either [`Self::Anywhere`] or [`Self::TitleBar`]
|
||||||
|
/// based on whether a touch screen was detected.
|
||||||
|
fn resolve(self, ctx: &Context) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::OnTouch => {
|
||||||
|
if ctx.input(|i| i.has_touch_screen()) {
|
||||||
|
Self::Anywhere
|
||||||
|
} else {
|
||||||
|
Self::TitleBar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default).
|
/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default).
|
||||||
///
|
///
|
||||||
/// You can customize:
|
/// You can customize:
|
||||||
@@ -43,6 +91,7 @@ pub struct Window<'a> {
|
|||||||
with_title_bar: bool,
|
with_title_bar: bool,
|
||||||
fade_out: bool,
|
fade_out: bool,
|
||||||
auto_sized: bool,
|
auto_sized: bool,
|
||||||
|
drag_area: WindowDrag,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Window<'a> {
|
impl<'a> Window<'a> {
|
||||||
@@ -67,6 +116,7 @@ impl<'a> Window<'a> {
|
|||||||
with_title_bar: true,
|
with_title_bar: true,
|
||||||
fade_out: true,
|
fade_out: true,
|
||||||
auto_sized: false,
|
auto_sized: false,
|
||||||
|
drag_area: WindowDrag::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,12 +192,29 @@ impl<'a> Window<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// If `false` the window will be immovable.
|
/// If `false` the window will be immovable.
|
||||||
|
///
|
||||||
|
/// If `true`, you can move the window by dragging it.
|
||||||
|
/// Where you can drag to move the window is determined by [`Self::drag_area`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn movable(mut self, movable: bool) -> Self {
|
pub fn movable(mut self, movable: bool) -> Self {
|
||||||
self.area = self.area.movable(movable);
|
self.area = self.area.movable(movable);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where the user can grab the window to move it.
|
||||||
|
///
|
||||||
|
/// Defaults to [`WindowDrag::OnTouch`]: drag anywhere on touch screens,
|
||||||
|
/// title bar only otherwise. See [`WindowDrag`] for details.
|
||||||
|
///
|
||||||
|
/// [`Self::movable(false)`](Self::movable) forces [`WindowDrag::Off`]
|
||||||
|
/// regardless of this setting. Windows without a title bar (see
|
||||||
|
/// [`Self::title_bar`]) fall back to [`WindowDrag::Anywhere`].
|
||||||
|
#[inline]
|
||||||
|
pub fn drag_area(mut self, drag_area: WindowDrag) -> Self {
|
||||||
|
self.drag_area = drag_area;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// `order(Order::Foreground)` for a Window that should always be on top
|
/// `order(Order::Foreground)` for a Window that should always be on top
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn order(mut self, order: Order) -> Self {
|
pub fn order(mut self, order: Order) -> Self {
|
||||||
@@ -489,8 +556,64 @@ impl Window<'_> {
|
|||||||
with_title_bar,
|
with_title_bar,
|
||||||
fade_out,
|
fade_out,
|
||||||
auto_sized,
|
auto_sized,
|
||||||
|
drag_area: drag_area_setting,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
// `Window::movable(false)` (and `Area::movable(false)`) and
|
||||||
|
// `WindowDrag::Off` both mean "this window cannot be moved by
|
||||||
|
// dragging". Without a title bar, `TitleBar` mode would leave the
|
||||||
|
// window unmovable, so silently fall back to drag-anywhere instead.
|
||||||
|
let effective_drag = if !area.is_movable() || drag_area_setting == WindowDrag::Off {
|
||||||
|
WindowDrag::Off
|
||||||
|
} else if !with_title_bar {
|
||||||
|
WindowDrag::Anywhere
|
||||||
|
} else {
|
||||||
|
drag_area_setting.resolve(ctx)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make the area itself agree: keep its movable flag in sync with
|
||||||
|
// the resolved drag mode so resize behavior and `Area::begin`'s
|
||||||
|
// drag-from-anywhere handling don't disagree with the title-bar
|
||||||
|
// path. (Builder order shouldn't matter — `.drag_area(Off)` after
|
||||||
|
// `.movable(true)` and vice versa both end up here.)
|
||||||
|
let area = if effective_drag == WindowDrag::Off {
|
||||||
|
area.movable(false)
|
||||||
|
} else {
|
||||||
|
area
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply the previous frame's title-bar drag _before_ `Area::begin`
|
||||||
|
// loads the state. We can't apply it inside the content closure because
|
||||||
|
// `Area::end` writes the locally-captured `AreaState` back, overwriting
|
||||||
|
// any in-frame mutation.
|
||||||
|
//
|
||||||
|
// We deliberately leave `Area` with its normal `Sense::DRAG`: that way
|
||||||
|
// the area's widget still absorbs drag hit-tests over the body, so the
|
||||||
|
// resize-edge widgets aren't picked as the "closest drag" target when
|
||||||
|
// hovering anywhere in the window. The drag-from-anywhere move that
|
||||||
|
// `Area::begin` would then apply is undone right after `begin` for
|
||||||
|
// `WindowDrag::TitleBar`.
|
||||||
|
let title_drag_mode = effective_drag == WindowDrag::TitleBar;
|
||||||
|
let pivot_pos_before_begin = if title_drag_mode {
|
||||||
|
if let Some(resp) = ctx.read_response(area.id.with("__title_click"))
|
||||||
|
&& resp.dragged()
|
||||||
|
{
|
||||||
|
let delta = ctx.input(|i| i.pointer.delta());
|
||||||
|
if delta != Vec2::ZERO {
|
||||||
|
ctx.memory_mut(|mem| {
|
||||||
|
if let Some(state) = mem.areas_mut().get_mut(area.id)
|
||||||
|
&& let Some(pivot_pos) = state.pivot_pos.as_mut()
|
||||||
|
{
|
||||||
|
*pivot_pos += delta;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
area::AreaState::load(ctx, area.id).and_then(|s| s.pivot_pos)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let style = ctx.global_style();
|
let style = ctx.global_style();
|
||||||
|
|
||||||
let window_frame = frame.unwrap_or_else(|| Frame::window(&style));
|
let window_frame = frame.unwrap_or_else(|| Frame::window(&style));
|
||||||
@@ -525,6 +648,24 @@ impl Window<'_> {
|
|||||||
let on_top = Some(area_layer_id) == ctx.top_layer_id();
|
let on_top = Some(area_layer_id) == ctx.top_layer_id();
|
||||||
let mut area = area.begin(ctx);
|
let mut area = area.begin(ctx);
|
||||||
|
|
||||||
|
// Title-bar-drag mode: throw away any drag-from-anywhere movement
|
||||||
|
// `Area::begin` may have applied. The title-bar pre-begin step above
|
||||||
|
// already accounted for the title drag. We then re-run the same
|
||||||
|
// constrain+round step `Area::begin` does so the title-bar drag
|
||||||
|
// can't escape `constrain_rect` or reintroduce sub-pixel jitter.
|
||||||
|
if let Some(pre_begin_pivot) = pivot_pos_before_begin {
|
||||||
|
let constrain = area.constrain();
|
||||||
|
let constrain_rect = area.constrain_rect();
|
||||||
|
let state = area.state_mut();
|
||||||
|
state.pivot_pos = Some(pre_begin_pivot);
|
||||||
|
if constrain {
|
||||||
|
state.set_left_top_pos(
|
||||||
|
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
state.set_left_top_pos(area::round_area_position(ctx, state.left_top_pos()));
|
||||||
|
}
|
||||||
|
|
||||||
area.with_widget_info(|| {
|
area.with_widget_info(|| {
|
||||||
WidgetInfo::labeled(
|
WidgetInfo::labeled(
|
||||||
WidgetType::Window,
|
WidgetType::Window,
|
||||||
@@ -576,6 +717,8 @@ impl Window<'_> {
|
|||||||
on_top,
|
on_top,
|
||||||
open.as_deref_mut(),
|
open.as_deref_mut(),
|
||||||
auto_sized,
|
auto_sized,
|
||||||
|
effective_drag == WindowDrag::TitleBar,
|
||||||
|
area_id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
collapsing
|
collapsing
|
||||||
@@ -1142,6 +1285,8 @@ fn title_ui(
|
|||||||
active: bool,
|
active: bool,
|
||||||
open: Option<&mut bool>,
|
open: Option<&mut bool>,
|
||||||
auto_sized: bool,
|
auto_sized: bool,
|
||||||
|
drag_to_move: bool,
|
||||||
|
area_id: Id,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let shape_idx = ui.painter().add(Shape::Noop);
|
let shape_idx = ui.painter().add(Shape::Noop);
|
||||||
|
|
||||||
@@ -1247,17 +1392,22 @@ fn title_ui(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if collapsible
|
if collapsible || drag_to_move {
|
||||||
&& child_ui
|
// Single widget covers double-click-to-toggle (when collapsible) and
|
||||||
.interact(
|
// drag-to-move (in title-bar-drag mode). The move itself is applied in
|
||||||
title_click_rect,
|
// `Window::show_dyn` _before_ `Area::begin` next frame, since
|
||||||
child_ui.auto_id_with("window_title_click"),
|
// `Area::end` overwrites any in-frame mutation of `AreaState`.
|
||||||
Sense::click(),
|
let sense = if drag_to_move {
|
||||||
)
|
Sense::click_and_drag()
|
||||||
.double_clicked()
|
} else {
|
||||||
{
|
Sense::click()
|
||||||
|
};
|
||||||
|
let response = child_ui.interact(title_click_rect, area_id.with("__title_click"), sense);
|
||||||
|
|
||||||
|
if collapsible && response.double_clicked() {
|
||||||
collapsing.toggle(&child_ui);
|
collapsing.toggle(&child_ui);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut header_frame = frame.shadow(Shadow::NONE);
|
let mut header_frame = frame.shadow(Shadow::NONE);
|
||||||
|
|||||||
@@ -1171,6 +1171,10 @@ impl Areas {
|
|||||||
self.areas.get(&id)
|
self.areas.get(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_mut(&mut self, id: Id) -> Option<&mut area::AreaState> {
|
||||||
|
self.areas.get_mut(&id)
|
||||||
|
}
|
||||||
|
|
||||||
/// All layers back-to-front, top is last.
|
/// All layers back-to-front, top is last.
|
||||||
pub(crate) fn order(&self) -> &[LayerId] {
|
pub(crate) fn order(&self) -> &[LayerId] {
|
||||||
&self.order
|
&self.order
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use egui::{UiKind, Vec2b};
|
use egui::{UiKind, Vec2b, WindowDrag};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
@@ -7,6 +7,7 @@ pub struct WindowOptions {
|
|||||||
title_bar: bool,
|
title_bar: bool,
|
||||||
closable: bool,
|
closable: bool,
|
||||||
collapsible: bool,
|
collapsible: bool,
|
||||||
|
movable: bool,
|
||||||
resizable: bool,
|
resizable: bool,
|
||||||
constrain: bool,
|
constrain: bool,
|
||||||
scroll2: Vec2b,
|
scroll2: Vec2b,
|
||||||
@@ -15,6 +16,8 @@ pub struct WindowOptions {
|
|||||||
anchored: bool,
|
anchored: bool,
|
||||||
anchor: egui::Align2,
|
anchor: egui::Align2,
|
||||||
anchor_offset: egui::Vec2,
|
anchor_offset: egui::Vec2,
|
||||||
|
|
||||||
|
drag_area: WindowDrag,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WindowOptions {
|
impl Default for WindowOptions {
|
||||||
@@ -24,6 +27,7 @@ impl Default for WindowOptions {
|
|||||||
title_bar: true,
|
title_bar: true,
|
||||||
closable: true,
|
closable: true,
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
|
movable: true,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
constrain: true,
|
constrain: true,
|
||||||
scroll2: Vec2b::TRUE,
|
scroll2: Vec2b::TRUE,
|
||||||
@@ -31,6 +35,7 @@ impl Default for WindowOptions {
|
|||||||
anchored: false,
|
anchored: false,
|
||||||
anchor: egui::Align2::RIGHT_TOP,
|
anchor: egui::Align2::RIGHT_TOP,
|
||||||
anchor_offset: egui::Vec2::ZERO,
|
anchor_offset: egui::Vec2::ZERO,
|
||||||
|
drag_area: WindowDrag::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,6 +51,7 @@ impl crate::Demo for WindowOptions {
|
|||||||
title_bar,
|
title_bar,
|
||||||
closable,
|
closable,
|
||||||
collapsible,
|
collapsible,
|
||||||
|
movable,
|
||||||
resizable,
|
resizable,
|
||||||
constrain,
|
constrain,
|
||||||
scroll2,
|
scroll2,
|
||||||
@@ -53,6 +59,7 @@ impl crate::Demo for WindowOptions {
|
|||||||
anchored,
|
anchored,
|
||||||
anchor,
|
anchor,
|
||||||
anchor_offset,
|
anchor_offset,
|
||||||
|
drag_area,
|
||||||
} = self.clone();
|
} = self.clone();
|
||||||
|
|
||||||
let enabled = ui.input(|i| i.time) - disabled_time > 2.0;
|
let enabled = ui.input(|i| i.time) - disabled_time > 2.0;
|
||||||
@@ -66,7 +73,9 @@ impl crate::Demo for WindowOptions {
|
|||||||
.resizable(resizable)
|
.resizable(resizable)
|
||||||
.constrain(constrain)
|
.constrain(constrain)
|
||||||
.collapsible(collapsible)
|
.collapsible(collapsible)
|
||||||
|
.movable(movable)
|
||||||
.title_bar(title_bar)
|
.title_bar(title_bar)
|
||||||
|
.drag_area(drag_area)
|
||||||
.scroll(scroll2)
|
.scroll(scroll2)
|
||||||
.constrain_to(ui.available_rect_before_wrap())
|
.constrain_to(ui.available_rect_before_wrap())
|
||||||
.enabled(enabled);
|
.enabled(enabled);
|
||||||
@@ -87,6 +96,7 @@ impl crate::View for WindowOptions {
|
|||||||
title_bar,
|
title_bar,
|
||||||
closable,
|
closable,
|
||||||
collapsible,
|
collapsible,
|
||||||
|
movable,
|
||||||
resizable,
|
resizable,
|
||||||
constrain,
|
constrain,
|
||||||
scroll2,
|
scroll2,
|
||||||
@@ -94,6 +104,7 @@ impl crate::View for WindowOptions {
|
|||||||
anchored,
|
anchored,
|
||||||
anchor,
|
anchor,
|
||||||
anchor_offset,
|
anchor_offset,
|
||||||
|
drag_area,
|
||||||
} = self;
|
} = self;
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("title:");
|
ui.label("title:");
|
||||||
@@ -106,6 +117,8 @@ impl crate::View for WindowOptions {
|
|||||||
ui.checkbox(title_bar, "title_bar");
|
ui.checkbox(title_bar, "title_bar");
|
||||||
ui.checkbox(closable, "closable");
|
ui.checkbox(closable, "closable");
|
||||||
ui.checkbox(collapsible, "collapsible");
|
ui.checkbox(collapsible, "collapsible");
|
||||||
|
ui.checkbox(movable, "movable")
|
||||||
|
.on_hover_text("Can the window be moved by dragging?");
|
||||||
ui.checkbox(resizable, "resizable");
|
ui.checkbox(resizable, "resizable");
|
||||||
ui.checkbox(constrain, "constrain")
|
ui.checkbox(constrain, "constrain")
|
||||||
.on_hover_text("Constrain window to the screen");
|
.on_hover_text("Constrain window to the screen");
|
||||||
@@ -140,6 +153,19 @@ impl crate::View for WindowOptions {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Drag to move:")
|
||||||
|
.on_hover_text("Where the user can grab the window to move it");
|
||||||
|
ui.selectable_value(drag_area, WindowDrag::Off, "Off")
|
||||||
|
.on_hover_text("The window cannot be dragged to move it (same as movable = false)");
|
||||||
|
ui.selectable_value(drag_area, WindowDrag::OnTouch, "OnTouch")
|
||||||
|
.on_hover_text("Anywhere on touch screens, title-bar only otherwise (default)");
|
||||||
|
ui.selectable_value(drag_area, WindowDrag::TitleBar, "TitleBar")
|
||||||
|
.on_hover_text("Only the title bar moves the window");
|
||||||
|
ui.selectable_value(drag_area, WindowDrag::Anywhere, "Anywhere")
|
||||||
|
.on_hover_text("Drag anywhere on the window to move it");
|
||||||
|
});
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
let on_top = Some(ui.layer_id()) == ui.ctx().top_layer_id();
|
let on_top = Some(ui.layer_id()) == ui.ctx().top_layer_id();
|
||||||
ui.label(format!("This window is on top: {on_top}."));
|
ui.label(format!("This window is on top: {on_top}."));
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ab0a8201038b2b066c5aff1fd1a35bc7aed95e74cf50c293eee4a76d66623822
|
oid sha256:d27392f625445e82285c8eaeb532e11dcb02030f24c7a769356cb4c23d7c2067
|
||||||
size 35171
|
size 41516
|
||||||
|
|||||||
195
tests/egui_tests/tests/test_window_drag.rs
Normal file
195
tests/egui_tests/tests/test_window_drag.rs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
//! Tests for [`Window::drag_area`] and [`Window::movable`].
|
||||||
|
//!
|
||||||
|
//! Each test sets up a window with a particular drag configuration, drags
|
||||||
|
//! either inside or outside the title bar, and asserts on the area rect's
|
||||||
|
//! delta. `WindowDrag::OnTouch` is not exercised here since it just resolves
|
||||||
|
//! to `TitleBar` (no touch screen in headless tests).
|
||||||
|
|
||||||
|
use egui::{Id, Pos2, Sense, Vec2, Window, WindowDrag};
|
||||||
|
use egui_kittest::Harness;
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
drag_area: WindowDrag,
|
||||||
|
movable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(state: State) -> Harness<'static, State> {
|
||||||
|
let mut harness = Harness::builder()
|
||||||
|
.with_size(Vec2::new(500.0, 400.0))
|
||||||
|
.with_max_steps(40) // Area requests a repaint every frame while pressed.
|
||||||
|
.build_ui_state(
|
||||||
|
move |ui, state: &mut State| {
|
||||||
|
Window::new("test_win")
|
||||||
|
.id(Id::new("test_win"))
|
||||||
|
.drag_area(state.drag_area)
|
||||||
|
.movable(state.movable)
|
||||||
|
.default_pos([100.0, 80.0])
|
||||||
|
.default_size([180.0, 140.0])
|
||||||
|
.show(ui.ctx(), |ui| {
|
||||||
|
// A passive widget fills the body; it has no drag sense
|
||||||
|
// of its own, so the Area / title-bar widget is what
|
||||||
|
// decides whether a drag moves the window.
|
||||||
|
ui.allocate_response(ui.available_size(), Sense::hover());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
// Let the window settle (auto-position / size, then idle).
|
||||||
|
harness.run_steps(4);
|
||||||
|
harness
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_rect(harness: &Harness<'_, State>) -> egui::Rect {
|
||||||
|
egui::AreaState::load(&harness.ctx, Id::new("test_win"))
|
||||||
|
.expect("window area should be persisted after the first frame")
|
||||||
|
.rect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drag the pointer from `from` to `to` over multiple frames; release at the end.
|
||||||
|
fn drag(harness: &mut Harness<'_, State>, from: Pos2, to: Pos2) {
|
||||||
|
harness.drag_at(from);
|
||||||
|
harness.run_steps(4);
|
||||||
|
harness.hover_at(to);
|
||||||
|
harness.run_steps(4);
|
||||||
|
harness.drop_at(to);
|
||||||
|
harness.run_steps(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn titlebar_pos(rect: egui::Rect) -> Pos2 {
|
||||||
|
// Just inside the title bar:
|
||||||
|
Pos2::new(rect.center().x, rect.top() + 8.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_pos(rect: egui::Rect) -> Pos2 {
|
||||||
|
// Well below the title bar:
|
||||||
|
Pos2::new(rect.center().x, rect.bottom() - 30.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn title_bar_drag_on_titlebar_moves_window() {
|
||||||
|
let mut harness = build(State {
|
||||||
|
drag_area: WindowDrag::TitleBar,
|
||||||
|
movable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let before = window_rect(&harness);
|
||||||
|
let from = titlebar_pos(before);
|
||||||
|
let to = from + Vec2::new(60.0, 40.0);
|
||||||
|
|
||||||
|
drag(&mut harness, from, to);
|
||||||
|
|
||||||
|
let after = window_rect(&harness);
|
||||||
|
let moved = after.min - before.min;
|
||||||
|
assert!(
|
||||||
|
20.0 < moved.x && 20.0 < moved.y,
|
||||||
|
"TitleBar + drag on titlebar should move the window (delta = {moved:?})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn title_bar_drag_outside_titlebar_keeps_window_put() {
|
||||||
|
let mut harness = build(State {
|
||||||
|
drag_area: WindowDrag::TitleBar,
|
||||||
|
movable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let before = window_rect(&harness);
|
||||||
|
let from = body_pos(before);
|
||||||
|
let to = from + Vec2::new(60.0, -40.0);
|
||||||
|
|
||||||
|
drag(&mut harness, from, to);
|
||||||
|
|
||||||
|
let after = window_rect(&harness);
|
||||||
|
let moved = after.min - before.min;
|
||||||
|
assert!(
|
||||||
|
moved.length() < 1.0,
|
||||||
|
"TitleBar + drag in the body should NOT move the window (delta = {moved:?})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn anywhere_drag_in_body_moves_window() {
|
||||||
|
let mut harness = build(State {
|
||||||
|
drag_area: WindowDrag::Anywhere,
|
||||||
|
movable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let before = window_rect(&harness);
|
||||||
|
let from = body_pos(before);
|
||||||
|
let to = from + Vec2::new(60.0, -40.0);
|
||||||
|
|
||||||
|
drag(&mut harness, from, to);
|
||||||
|
|
||||||
|
let after = window_rect(&harness);
|
||||||
|
let moved = after.min - before.min;
|
||||||
|
assert!(
|
||||||
|
20.0 < moved.x && moved.y < -20.0,
|
||||||
|
"Anywhere + drag anywhere should move the window (delta = {moved:?})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn movable_false_keeps_window_put_even_on_titlebar() {
|
||||||
|
// Regression: a `movable(false)` window used to still move when the user
|
||||||
|
// dragged the title bar in `TitleBar` mode.
|
||||||
|
let mut harness = build(State {
|
||||||
|
drag_area: WindowDrag::TitleBar,
|
||||||
|
movable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let before = window_rect(&harness);
|
||||||
|
let from = titlebar_pos(before);
|
||||||
|
let to = from + Vec2::new(60.0, 40.0);
|
||||||
|
|
||||||
|
drag(&mut harness, from, to);
|
||||||
|
|
||||||
|
let after = window_rect(&harness);
|
||||||
|
let moved = after.min - before.min;
|
||||||
|
assert!(
|
||||||
|
moved.length() < 1.0,
|
||||||
|
"TitleBar + movable(false) should NOT move the window (delta = {moved:?})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn off_keeps_window_put_on_body_drag() {
|
||||||
|
// `WindowDrag::Off` should freeze the window regardless of `movable`.
|
||||||
|
let mut harness = build(State {
|
||||||
|
drag_area: WindowDrag::Off,
|
||||||
|
movable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let before = window_rect(&harness);
|
||||||
|
let from = body_pos(before);
|
||||||
|
let to = from + Vec2::new(60.0, -40.0);
|
||||||
|
|
||||||
|
drag(&mut harness, from, to);
|
||||||
|
|
||||||
|
let after = window_rect(&harness);
|
||||||
|
let moved = after.min - before.min;
|
||||||
|
assert!(
|
||||||
|
moved.length() < 1.0,
|
||||||
|
"Off + drag in the body should NOT move the window (delta = {moved:?})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn off_keeps_window_put_on_titlebar_drag() {
|
||||||
|
let mut harness = build(State {
|
||||||
|
drag_area: WindowDrag::Off,
|
||||||
|
movable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let before = window_rect(&harness);
|
||||||
|
let from = titlebar_pos(before);
|
||||||
|
let to = from + Vec2::new(60.0, 40.0);
|
||||||
|
|
||||||
|
drag(&mut harness, from, to);
|
||||||
|
|
||||||
|
let after = window_rect(&harness);
|
||||||
|
let moved = after.min - before.min;
|
||||||
|
assert!(
|
||||||
|
moved.length() < 1.0,
|
||||||
|
"Off + drag on titlebar should NOT move the window (delta = {moved:?})"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user