Add `Area::constrain_to` and `Window::constrain_to` (#3396)

* Rename `drag_bounds` to `constrain_to`

and allow turning off constraining for `Window`s

* Constrain DatePicker popup to screen

* Fix typo
This commit is contained in:
Emil Ernerfeldt 2023-09-27 08:40:24 +02:00 committed by GitHub
parent 1911248ade
commit 1b18f8e266
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 72 additions and 41 deletions

View File

@ -65,12 +65,12 @@ pub struct Area {
interactable: bool, interactable: bool,
enabled: bool, enabled: bool,
constrain: bool, constrain: bool,
constrain_rect: Option<Rect>,
order: Order, order: Order,
default_pos: Option<Pos2>, default_pos: Option<Pos2>,
pivot: Align2, pivot: Align2,
anchor: Option<(Align2, Vec2)>, anchor: Option<(Align2, Vec2)>,
new_pos: Option<Pos2>, new_pos: Option<Pos2>,
drag_bounds: Option<Rect>,
} }
impl Area { impl Area {
@ -80,13 +80,13 @@ impl Area {
movable: true, movable: true,
interactable: true, interactable: true,
constrain: false, constrain: false,
constrain_rect: None,
enabled: true, enabled: true,
order: Order::Middle, order: Order::Middle,
default_pos: None, default_pos: None,
new_pos: None, new_pos: None,
pivot: Align2::LEFT_TOP, pivot: Align2::LEFT_TOP,
anchor: None, anchor: None,
drag_bounds: None,
} }
} }
@ -155,6 +155,21 @@ impl Area {
self self
} }
/// Constraint the movement of the window to the given rectangle.
///
/// For instance: `.constrain_to(ctx.screen_rect())`.
pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
self.constrain = true;
self.constrain_rect = Some(constrain_rect);
self
}
#[deprecated = "Use `constrain_to` instead"]
pub fn drag_bounds(mut self, constrain_rect: Rect) -> Self {
self.constrain_rect = Some(constrain_rect);
self
}
/// Where the "root" of the area is. /// Where the "root" of the area is.
/// ///
/// For instance, if you set this to [`Align2::RIGHT_TOP`] /// For instance, if you set this to [`Align2::RIGHT_TOP`]
@ -189,12 +204,6 @@ impl Area {
self.movable(false) self.movable(false)
} }
/// Constrain the area up to which the window can be dragged.
pub fn drag_bounds(mut self, bounds: Rect) -> Self {
self.drag_bounds = Some(bounds);
self
}
pub(crate) fn get_pivot(&self) -> Align2 { pub(crate) fn get_pivot(&self) -> Align2 {
if let Some((pivot, _)) = self.anchor { if let Some((pivot, _)) = self.anchor {
pivot pivot
@ -209,7 +218,8 @@ pub(crate) struct Prepared {
state: State, state: State,
move_response: Response, move_response: Response,
enabled: bool, enabled: bool,
drag_bounds: Option<Rect>, constrain: bool,
constrain_rect: Option<Rect>,
/// We always make windows invisible the first frame to hide "first-frame-jitters". /// We always make windows invisible the first frame to hide "first-frame-jitters".
/// ///
@ -243,8 +253,8 @@ impl Area {
new_pos, new_pos,
pivot, pivot,
anchor, anchor,
drag_bounds,
constrain, constrain,
constrain_rect,
} = self; } = self;
let layer_id = LayerId::new(order, id); let layer_id = LayerId::new(order, id);
@ -308,7 +318,7 @@ impl Area {
if constrain { if constrain {
state.set_left_top_pos( state.set_left_top_pos(
ctx.constrain_window_rect_to_area(state.rect(), drag_bounds) ctx.constrain_window_rect_to_area(state.rect(), constrain_rect)
.min, .min,
); );
} }
@ -323,7 +333,8 @@ impl Area {
state, state,
move_response, move_response,
enabled, enabled,
drag_bounds, constrain,
constrain_rect,
temporarily_invisible: is_new, temporarily_invisible: is_new,
} }
} }
@ -366,15 +377,19 @@ impl Prepared {
&mut self.state &mut self.state
} }
pub(crate) fn drag_bounds(&self) -> Option<Rect> { pub(crate) fn constrain(&self) -> bool {
self.drag_bounds self.constrain
}
pub(crate) fn constrain_rect(&self) -> Option<Rect> {
self.constrain_rect
} }
pub(crate) fn content_ui(&self, ctx: &Context) -> Ui { pub(crate) fn content_ui(&self, ctx: &Context) -> Ui {
let screen_rect = ctx.screen_rect(); let screen_rect = ctx.screen_rect();
let bounds = if let Some(bounds) = self.drag_bounds { let constrain_rect = if let Some(constrain_rect) = self.constrain_rect {
bounds.intersect(screen_rect) // protect against infinite bounds constrain_rect.intersect(screen_rect) // protect against infinite bounds
} else { } else {
let central_area = ctx.available_rect(); let central_area = ctx.available_rect();
@ -388,7 +403,7 @@ impl Prepared {
let max_rect = Rect::from_min_max( let max_rect = Rect::from_min_max(
self.state.left_top_pos(), self.state.left_top_pos(),
bounds constrain_rect
.max .max
.at_least(self.state.left_top_pos() + Vec2::splat(32.0)), .at_least(self.state.left_top_pos() + Vec2::splat(32.0)),
); );
@ -396,9 +411,9 @@ impl Prepared {
let shadow_radius = ctx.style().visuals.window_shadow.extrusion; // hacky let shadow_radius = ctx.style().visuals.window_shadow.extrusion; // hacky
let clip_rect_margin = ctx.style().visuals.clip_rect_margin.max(shadow_radius); let clip_rect_margin = ctx.style().visuals.clip_rect_margin.max(shadow_radius);
let clip_rect = Rect::from_min_max(self.state.left_top_pos(), bounds.max) let clip_rect = Rect::from_min_max(self.state.left_top_pos(), constrain_rect.max)
.expand(clip_rect_margin) .expand(clip_rect_margin)
.intersect(bounds); .intersect(constrain_rect);
let mut ui = Ui::new( let mut ui = Ui::new(
ctx.clone(), ctx.clone(),
@ -419,7 +434,8 @@ impl Prepared {
mut state, mut state,
move_response, move_response,
enabled: _, enabled: _,
drag_bounds: _, constrain: _,
constrain_rect: _,
temporarily_invisible: _, temporarily_invisible: _,
} = self; } = self;

View File

@ -260,9 +260,8 @@ fn show_tooltip_area_dyn<'c, R>(
Area::new(area_id) Area::new(area_id)
.order(Order::Tooltip) .order(Order::Tooltip)
.fixed_pos(window_pos) .fixed_pos(window_pos)
.constrain(true) .constrain_to(ctx.screen_rect())
.interactable(false) .interactable(false)
.drag_bounds(ctx.screen_rect())
.show(ctx, |ui| { .show(ctx, |ui| {
Frame::popup(&ctx.style()) Frame::popup(&ctx.style())
.show(ui, |ui| { .show(ui, |ui| {

View File

@ -43,7 +43,7 @@ impl<'open> Window<'open> {
/// If you need a changing title, you must call `window.id(…)` with a fixed id. /// If you need a changing title, you must call `window.id(…)` with a fixed id.
pub fn new(title: impl Into<WidgetText>) -> Self { pub fn new(title: impl Into<WidgetText>) -> Self {
let title = title.into().fallback_text_style(TextStyle::Heading); let title = title.into().fallback_text_style(TextStyle::Heading);
let area = Area::new(Id::new(title.text())); let area = Area::new(Id::new(title.text())).constrain(true);
Self { Self {
title, title,
open: None, open: None,
@ -146,11 +146,31 @@ impl<'open> Window<'open> {
} }
/// Constrains this window to the screen bounds. /// Constrains this window to the screen bounds.
///
/// To change the area to constrain to, use [`Self::constraint_to`].
///
/// Default: `true`.
pub fn constrain(mut self, constrain: bool) -> Self { pub fn constrain(mut self, constrain: bool) -> Self {
self.area = self.area.constrain(constrain); self.area = self.area.constrain(constrain);
self self
} }
/// Constraint the movement of the window to the given rectangle.
///
/// For instance: `.constrain_to(ctx.screen_rect())`.
pub fn constraint_to(mut self, constrain_rect: Rect) -> Self {
self.area = self.area.constrain_to(constrain_rect);
self
}
#[deprecated = "Use `constrain_to` instead"]
pub fn drag_bounds(mut self, constrain_rect: Rect) -> Self {
#![allow(deprecated)]
self.area = self.area.drag_bounds(constrain_rect);
self
}
/// Where the "root" of the window is. /// Where the "root" of the window is.
/// ///
/// For instance, if you set this to [`Align2::RIGHT_TOP`] /// For instance, if you set this to [`Align2::RIGHT_TOP`]
@ -276,12 +296,6 @@ impl<'open> Window<'open> {
self.scroll = self.scroll.drag_to_scroll(drag_to_scroll); self.scroll = self.scroll.drag_to_scroll(drag_to_scroll);
self self
} }
/// Constrain the area up to which the window can be dragged.
pub fn drag_bounds(mut self, bounds: Rect) -> Self {
self.area = self.area.drag_bounds(bounds);
self
}
} }
impl<'open> Window<'open> { impl<'open> Window<'open> {
@ -452,13 +466,6 @@ impl<'open> Window<'open> {
content_inner content_inner
}; };
{
let pos = ctx
.constrain_window_rect_to_area(area.state().rect(), area.drag_bounds())
.left_top();
area.state_mut().set_left_top_pos(pos);
}
let full_response = area.end(ctx, area_content_ui); let full_response = area.end(ctx, area_content_ui);
let inner_response = InnerResponse { let inner_response = InnerResponse {
@ -562,9 +569,11 @@ fn interact(
resize_id: Id, resize_id: Id,
) -> Option<WindowInteraction> { ) -> Option<WindowInteraction> {
let new_rect = move_and_resize_window(ctx, &window_interaction)?; let new_rect = move_and_resize_window(ctx, &window_interaction)?;
let new_rect = ctx.round_rect_to_pixels(new_rect); let mut new_rect = ctx.round_rect_to_pixels(new_rect);
let new_rect = ctx.constrain_window_rect_to_area(new_rect, area.drag_bounds()); if area.constrain() {
new_rect = ctx.constrain_window_rect_to_area(new_rect, area.constrain_rect());
}
// TODO(emilk): add this to a Window state instead as a command "move here next frame" // TODO(emilk): add this to a Window state instead as a command "move here next frame"
area.state_mut().set_left_top_pos(new_rect.left_top()); area.state_mut().set_left_top_pos(new_rect.left_top());

View File

@ -146,10 +146,9 @@ pub(crate) fn menu_ui<'c, R>(
let area = Area::new(menu_id) let area = Area::new(menu_id)
.order(Order::Foreground) .order(Order::Foreground)
.constrain(true)
.fixed_pos(pos) .fixed_pos(pos)
.interactable(true) .constrain_to(ctx.screen_rect())
.drag_bounds(ctx.screen_rect()); .interactable(true);
area.show(ctx, |ui| { area.show(ctx, |ui| {
set_menu_style(ui.style_mut()); set_menu_style(ui.style_mut());

View File

@ -6,6 +6,7 @@ pub struct WindowOptions {
closable: bool, closable: bool,
collapsible: bool, collapsible: bool,
resizable: bool, resizable: bool,
constrain: bool,
scroll2: [bool; 2], scroll2: [bool; 2],
disabled_time: f64, disabled_time: f64,
@ -22,6 +23,7 @@ impl Default for WindowOptions {
closable: true, closable: true,
collapsible: true, collapsible: true,
resizable: true, resizable: true,
constrain: true,
scroll2: [true; 2], scroll2: [true; 2],
disabled_time: f64::NEG_INFINITY, disabled_time: f64::NEG_INFINITY,
anchored: false, anchored: false,
@ -43,6 +45,7 @@ impl super::Demo for WindowOptions {
closable, closable,
collapsible, collapsible,
resizable, resizable,
constrain,
scroll2, scroll2,
disabled_time, disabled_time,
anchored, anchored,
@ -59,6 +62,7 @@ impl super::Demo for WindowOptions {
let mut window = egui::Window::new(title) let mut window = egui::Window::new(title)
.id(egui::Id::new("demo_window_options")) // required since we change the title .id(egui::Id::new("demo_window_options")) // required since we change the title
.resizable(resizable) .resizable(resizable)
.constrain(constrain)
.collapsible(collapsible) .collapsible(collapsible)
.title_bar(title_bar) .title_bar(title_bar)
.scroll2(scroll2) .scroll2(scroll2)
@ -81,6 +85,7 @@ impl super::View for WindowOptions {
closable, closable,
collapsible, collapsible,
resizable, resizable,
constrain,
scroll2, scroll2,
disabled_time: _, disabled_time: _,
anchored, anchored,
@ -99,6 +104,8 @@ impl super::View for WindowOptions {
ui.checkbox(closable, "closable"); ui.checkbox(closable, "closable");
ui.checkbox(collapsible, "collapsible"); ui.checkbox(collapsible, "collapsible");
ui.checkbox(resizable, "resizable"); ui.checkbox(resizable, "resizable");
ui.checkbox(constrain, "constrain")
.on_hover_text("Constrain window to the screen");
ui.checkbox(&mut scroll2[0], "hscroll"); ui.checkbox(&mut scroll2[0], "hscroll");
ui.checkbox(&mut scroll2[1], "vscroll"); ui.checkbox(&mut scroll2[1], "vscroll");
}); });

View File

@ -117,6 +117,7 @@ impl<'a> Widget for DatePickerButton<'a> {
} = Area::new(ui.make_persistent_id(self.id_source)) } = Area::new(ui.make_persistent_id(self.id_source))
.order(Order::Foreground) .order(Order::Foreground)
.fixed_pos(pos) .fixed_pos(pos)
.constrain_to(ui.ctx().screen_rect())
.show(ui.ctx(), |ui| { .show(ui.ctx(), |ui| {
let frame = Frame::popup(ui.style()); let frame = Frame::popup(ui.style());
frame frame