Add anchors to windows and areas (#310)
This is so that you can put a window in e.g. the top right corner or the center of the screen.
This commit is contained in:
parent
5667e7eb51
commit
580d27e0d3
6 changed files with 220 additions and 56 deletions
|
@ -8,14 +8,15 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
|
|||
## Unreleased
|
||||
|
||||
### Added ⭐
|
||||
* Add anchors to windows and areas so you can put a window in e.g. the top right corner.
|
||||
* Make labels interactive with `Label::sense(Sense::click())`.
|
||||
* Add `Response::request_focus` and `Response::surrender_focus`.
|
||||
|
||||
### Changed 🔧
|
||||
* Make `Memory::has_focus` public (again)
|
||||
* Make `Memory::has_focus` public (again).
|
||||
|
||||
### Fixed 🐛
|
||||
* Fix [defocus-bug on touch screens](https://github.com/emilk/egui/issues/288)
|
||||
* Fix [defocus-bug on touch screens](https://github.com/emilk/egui/issues/288).
|
||||
|
||||
## 0.11.0 - 2021-04-05 - Optimization, screen reader & new layout logic
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ pub struct Area {
|
|||
enabled: bool,
|
||||
order: Order,
|
||||
default_pos: Option<Pos2>,
|
||||
anchor: Option<(Align2, Vec2)>,
|
||||
new_pos: Option<Pos2>,
|
||||
drag_bounds: Option<Rect>,
|
||||
}
|
||||
|
@ -62,6 +63,7 @@ impl Area {
|
|||
order: Order::Middle,
|
||||
default_pos: None,
|
||||
new_pos: None,
|
||||
anchor: None,
|
||||
drag_bounds: None,
|
||||
}
|
||||
}
|
||||
|
@ -120,24 +122,46 @@ impl Area {
|
|||
|
||||
/// Positions the window and prevents it from being moved
|
||||
pub fn fixed_pos(mut self, fixed_pos: impl Into<Pos2>) -> Self {
|
||||
let fixed_pos = fixed_pos.into();
|
||||
self.new_pos = Some(fixed_pos);
|
||||
self.new_pos = Some(fixed_pos.into());
|
||||
self.movable = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Positions the window but you can still move it.
|
||||
pub fn current_pos(mut self, current_pos: impl Into<Pos2>) -> Self {
|
||||
let current_pos = current_pos.into();
|
||||
self.new_pos = Some(current_pos);
|
||||
self.new_pos = Some(current_pos.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set anchor and distance.
|
||||
///
|
||||
/// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
|
||||
/// in the right-top corner of the screen".
|
||||
///
|
||||
/// The offset is added to the position, so e.g. an offset of `[-5.0, 5.0]`
|
||||
/// would move the window left and down from the given anchor.
|
||||
///
|
||||
/// Anchoring also makes the window immovable.
|
||||
///
|
||||
/// It is an error to set both an anchor and a position.
|
||||
pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
|
||||
self.anchor = Some((align, offset.into()));
|
||||
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 {
|
||||
if let Some((pivot, _)) = self.anchor {
|
||||
pivot
|
||||
} else {
|
||||
Align2::LEFT_TOP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Prepared {
|
||||
|
@ -158,18 +182,31 @@ impl Area {
|
|||
enabled,
|
||||
default_pos,
|
||||
new_pos,
|
||||
anchor,
|
||||
drag_bounds,
|
||||
} = self;
|
||||
|
||||
let layer_id = LayerId::new(order, id);
|
||||
|
||||
let state = ctx.memory().areas.get(id).cloned();
|
||||
let is_new = state.is_none();
|
||||
let mut state = state.unwrap_or_else(|| State {
|
||||
pos: default_pos.unwrap_or_else(|| automatic_area_position(ctx)),
|
||||
size: Vec2::ZERO,
|
||||
interactable,
|
||||
});
|
||||
state.pos = new_pos.unwrap_or(state.pos);
|
||||
|
||||
if let Some((anchor, offset)) = anchor {
|
||||
if is_new {
|
||||
// unknown size
|
||||
ctx.request_repaint()
|
||||
} else {
|
||||
let screen = ctx.available_rect();
|
||||
state.pos = anchor.align_size_within_rect(state.size, screen).min + offset;
|
||||
}
|
||||
}
|
||||
|
||||
state.pos = ctx.round_pos_to_pixels(state.pos);
|
||||
|
||||
Prepared {
|
||||
|
|
|
@ -318,17 +318,20 @@ use epaint::Stroke;
|
|||
|
||||
pub fn paint_resize_corner(ui: &mut Ui, response: &Response) {
|
||||
let stroke = ui.style().interact(response).fg_stroke;
|
||||
paint_resize_corner_with_style(ui, &response.rect, stroke);
|
||||
paint_resize_corner_with_style(ui, &response.rect, stroke, Align2::RIGHT_BOTTOM);
|
||||
}
|
||||
|
||||
pub fn paint_resize_corner_with_style(ui: &mut Ui, rect: &Rect, stroke: Stroke) {
|
||||
pub fn paint_resize_corner_with_style(ui: &mut Ui, rect: &Rect, stroke: Stroke, corner: Align2) {
|
||||
let painter = ui.painter();
|
||||
let corner = painter.round_pos_to_pixels(rect.right_bottom());
|
||||
let cp = painter.round_pos_to_pixels(corner.pos_in_rect(rect));
|
||||
let mut w = 2.0;
|
||||
|
||||
while w <= rect.width() && w <= rect.height() {
|
||||
painter.line_segment(
|
||||
[pos2(corner.x - w, corner.y), pos2(corner.x, corner.y - w)],
|
||||
[
|
||||
pos2(cp.x - w * corner.x().to_sign(), cp.y),
|
||||
pos2(cp.x, cp.y - w * corner.y().to_sign()),
|
||||
],
|
||||
stroke,
|
||||
);
|
||||
w += 4.0;
|
||||
|
|
|
@ -121,6 +121,22 @@ impl<'open> Window<'open> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set anchor and distance.
|
||||
///
|
||||
/// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window
|
||||
/// in the right-top corner of the screen".
|
||||
///
|
||||
/// The offset is added to the position, so e.g. an offset of `[-5.0, 5.0]`
|
||||
/// would move the window left and down from the given anchor.
|
||||
///
|
||||
/// Anchoring also makes the window immovable.
|
||||
///
|
||||
/// It is an error to set both an anchor and a position.
|
||||
pub fn anchor(mut self, align: Align2, offset: impl Into<Vec2>) -> Self {
|
||||
self.area = self.area.anchor(align, offset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set initial size of the window.
|
||||
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
|
||||
self.resize = self.resize.default_size(default_size);
|
||||
|
@ -248,12 +264,9 @@ impl<'open> Window<'open> {
|
|||
let resize_id = area_id.with("resize");
|
||||
let collapsing_id = area_id.with("collapsing");
|
||||
|
||||
let is_maximized = !with_title_bar
|
||||
|| collapsing_header::State::is_open(ctx, collapsing_id).unwrap_or_default();
|
||||
let possible = PossibleInteractions {
|
||||
movable: area.is_enabled() && area.is_movable(),
|
||||
resizable: area.is_enabled() && resize.is_resizable() && is_maximized,
|
||||
};
|
||||
let is_collapsed = with_title_bar
|
||||
&& !collapsing_header::State::is_open(ctx, collapsing_id).unwrap_or_default();
|
||||
let possible = PossibleInteractions::new(&area, &resize, is_collapsed);
|
||||
|
||||
let area = area.movable(false); // We move it manually
|
||||
let resize = resize.resizable(false); // We move it manually
|
||||
|
@ -265,7 +278,7 @@ impl<'open> Window<'open> {
|
|||
|
||||
// First interact (move etc) to avoid frame delay:
|
||||
let last_frame_outer_rect = area.state().rect();
|
||||
let interaction = if possible.movable || possible.resizable {
|
||||
let interaction = if possible.movable || possible.resizable() {
|
||||
window_interaction(
|
||||
ctx,
|
||||
possible,
|
||||
|
@ -344,10 +357,7 @@ impl<'open> Window<'open> {
|
|||
.map(|ir| ir.response);
|
||||
|
||||
let outer_rect = frame.end(&mut area_content_ui).rect;
|
||||
|
||||
if possible.resizable {
|
||||
paint_resize_corner(&mut area_content_ui, outer_rect, frame_stroke);
|
||||
}
|
||||
paint_resize_corner(&mut area_content_ui, &possible, outer_rect, frame_stroke);
|
||||
|
||||
// END FRAME --------------------------------
|
||||
|
||||
|
@ -391,12 +401,28 @@ impl<'open> Window<'open> {
|
|||
}
|
||||
}
|
||||
|
||||
fn paint_resize_corner(ui: &mut Ui, outer_rect: Rect, stroke: Stroke) {
|
||||
fn paint_resize_corner(
|
||||
ui: &mut Ui,
|
||||
possible: &PossibleInteractions,
|
||||
outer_rect: Rect,
|
||||
stroke: Stroke,
|
||||
) {
|
||||
let corner = if possible.resize_right && possible.resize_bottom {
|
||||
Align2::RIGHT_BOTTOM
|
||||
} else if possible.resize_left && possible.resize_bottom {
|
||||
Align2::LEFT_BOTTOM
|
||||
} else if possible.resize_left && possible.resize_top {
|
||||
Align2::LEFT_TOP
|
||||
} else if possible.resize_right && possible.resize_top {
|
||||
Align2::RIGHT_TOP
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
|
||||
let handle_offset = -Vec2::splat(2.0);
|
||||
let corner_rect =
|
||||
Rect::from_min_size(outer_rect.max - corner_size + handle_offset, corner_size);
|
||||
crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke);
|
||||
let corner_rect = corner.align_size_within_rect(corner_size, outer_rect);
|
||||
let corner_rect = corner_rect.translate(-2.0 * corner.to_sign()); // move away from corner
|
||||
crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke, corner);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -404,7 +430,30 @@ fn paint_resize_corner(ui: &mut Ui, outer_rect: Rect, stroke: Stroke) {
|
|||
#[derive(Clone, Copy, Debug)]
|
||||
struct PossibleInteractions {
|
||||
movable: bool,
|
||||
resizable: bool,
|
||||
// Which sized can we drag to resize?
|
||||
resize_left: bool,
|
||||
resize_right: bool,
|
||||
resize_top: bool,
|
||||
resize_bottom: bool,
|
||||
}
|
||||
|
||||
impl PossibleInteractions {
|
||||
fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self {
|
||||
let movable = area.is_enabled() && area.is_movable();
|
||||
let resizable = area.is_enabled() && resize.is_resizable() && !is_collapsed;
|
||||
let pivot = area.get_pivot();
|
||||
Self {
|
||||
movable,
|
||||
resize_left: resizable && (movable || pivot.x() != Align::LEFT),
|
||||
resize_right: resizable && (movable || pivot.x() != Align::RIGHT),
|
||||
resize_top: resizable && (movable || pivot.y() != Align::TOP),
|
||||
resize_bottom: resizable && (movable || pivot.y() != Align::BOTTOM),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resizable(&self) -> bool {
|
||||
self.resize_left || self.resize_right || self.resize_top || self.resize_bottom
|
||||
}
|
||||
}
|
||||
|
||||
/// Either a move or resize
|
||||
|
@ -550,13 +599,13 @@ fn resize_hover(
|
|||
area_layer_id: LayerId,
|
||||
rect: Rect,
|
||||
) -> Option<WindowInteraction> {
|
||||
let pointer_pos = ctx.input().pointer.interact_pos()?;
|
||||
let pointer = ctx.input().pointer.interact_pos()?;
|
||||
|
||||
if ctx.input().pointer.any_down() && !ctx.input().pointer.any_pressed() {
|
||||
return None; // already dragging (something)
|
||||
}
|
||||
|
||||
if let Some(top_layer_id) = ctx.layer_id_at(pointer_pos) {
|
||||
if let Some(top_layer_id) = ctx.layer_id_at(pointer) {
|
||||
if top_layer_id != area_layer_id && top_layer_id.order != Order::Background {
|
||||
return None; // Another window is on top here
|
||||
}
|
||||
|
@ -569,38 +618,45 @@ fn resize_hover(
|
|||
|
||||
let side_grab_radius = ctx.style().interaction.resize_grab_radius_side;
|
||||
let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner;
|
||||
if !rect.expand(side_grab_radius).contains(pointer_pos) {
|
||||
if !rect.expand(side_grab_radius).contains(pointer) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (mut left, mut right, mut top, mut bottom) = Default::default();
|
||||
if possible.resizable {
|
||||
right = (rect.right() - pointer_pos.x).abs() <= side_grab_radius;
|
||||
bottom = (rect.bottom() - pointer_pos.y).abs() <= side_grab_radius;
|
||||
let mut left = possible.resize_left && (rect.left() - pointer.x).abs() <= side_grab_radius;
|
||||
let mut right = possible.resize_right && (rect.right() - pointer.x).abs() <= side_grab_radius;
|
||||
let mut top = possible.resize_top && (rect.top() - pointer.y).abs() <= side_grab_radius;
|
||||
let mut bottom =
|
||||
possible.resize_bottom && (rect.bottom() - pointer.y).abs() <= side_grab_radius;
|
||||
|
||||
if rect.right_bottom().distance(pointer_pos) < corner_grab_radius {
|
||||
right = true;
|
||||
bottom = true;
|
||||
}
|
||||
|
||||
if possible.movable {
|
||||
left = (rect.left() - pointer_pos.x).abs() <= side_grab_radius;
|
||||
top = (rect.top() - pointer_pos.y).abs() <= side_grab_radius;
|
||||
|
||||
if rect.right_top().distance(pointer_pos) < corner_grab_radius {
|
||||
right = true;
|
||||
top = true;
|
||||
}
|
||||
if rect.left_top().distance(pointer_pos) < corner_grab_radius {
|
||||
left = true;
|
||||
top = true;
|
||||
}
|
||||
if rect.left_bottom().distance(pointer_pos) < corner_grab_radius {
|
||||
left = true;
|
||||
bottom = true;
|
||||
}
|
||||
}
|
||||
if possible.resize_right
|
||||
&& possible.resize_bottom
|
||||
&& rect.right_bottom().distance(pointer) < corner_grab_radius
|
||||
{
|
||||
right = true;
|
||||
bottom = true;
|
||||
}
|
||||
if possible.resize_right
|
||||
&& possible.resize_top
|
||||
&& rect.right_top().distance(pointer) < corner_grab_radius
|
||||
{
|
||||
right = true;
|
||||
top = true;
|
||||
}
|
||||
if possible.resize_left
|
||||
&& possible.resize_top
|
||||
&& rect.left_top().distance(pointer) < corner_grab_radius
|
||||
{
|
||||
left = true;
|
||||
top = true;
|
||||
}
|
||||
if possible.resize_left
|
||||
&& possible.resize_bottom
|
||||
&& rect.left_bottom().distance(pointer) < corner_grab_radius
|
||||
{
|
||||
left = true;
|
||||
bottom = true;
|
||||
}
|
||||
|
||||
let any_resize = left || right || top || bottom;
|
||||
|
||||
if !any_resize && !possible.movable {
|
||||
|
|
|
@ -8,6 +8,10 @@ pub struct WindowOptions {
|
|||
resizable: bool,
|
||||
scroll: bool,
|
||||
disabled_time: f64,
|
||||
|
||||
anchored: bool,
|
||||
anchor: egui::Align2,
|
||||
anchor_offset: egui::Vec2,
|
||||
}
|
||||
|
||||
impl Default for WindowOptions {
|
||||
|
@ -20,6 +24,9 @@ impl Default for WindowOptions {
|
|||
resizable: true,
|
||||
scroll: false,
|
||||
disabled_time: f64::NEG_INFINITY,
|
||||
anchored: false,
|
||||
anchor: egui::Align2::RIGHT_TOP,
|
||||
anchor_offset: egui::Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +45,9 @@ impl super::Demo for WindowOptions {
|
|||
resizable,
|
||||
scroll,
|
||||
disabled_time,
|
||||
anchored,
|
||||
anchor,
|
||||
anchor_offset,
|
||||
} = self.clone();
|
||||
|
||||
let enabled = ctx.input().time - disabled_time > 2.0;
|
||||
|
@ -56,6 +66,9 @@ impl super::Demo for WindowOptions {
|
|||
if closable {
|
||||
window = window.open(open);
|
||||
}
|
||||
if anchored {
|
||||
window = window.anchor(anchor, anchor_offset);
|
||||
}
|
||||
window.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +83,9 @@ impl super::View for WindowOptions {
|
|||
resizable,
|
||||
scroll,
|
||||
disabled_time,
|
||||
anchored,
|
||||
anchor,
|
||||
anchor_offset,
|
||||
} = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
|
@ -82,6 +98,28 @@ impl super::View for WindowOptions {
|
|||
ui.checkbox(resizable, "resizable");
|
||||
ui.checkbox(scroll, "scroll");
|
||||
|
||||
ui.group(|ui| {
|
||||
ui.checkbox(anchored, "anchored");
|
||||
ui.set_enabled(*anchored);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("x:");
|
||||
ui.selectable_value(&mut anchor.0[0], egui::Align::LEFT, "Left");
|
||||
ui.selectable_value(&mut anchor.0[0], egui::Align::Center, "Center");
|
||||
ui.selectable_value(&mut anchor.0[0], egui::Align::RIGHT, "Right");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("y:");
|
||||
ui.selectable_value(&mut anchor.0[1], egui::Align::TOP, "Top");
|
||||
ui.selectable_value(&mut anchor.0[1], egui::Align::Center, "Center");
|
||||
ui.selectable_value(&mut anchor.0[1], egui::Align::BOTTOM, "Bottom");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Offset:");
|
||||
ui.add(egui::DragValue::new(&mut anchor_offset.x));
|
||||
ui.add(egui::DragValue::new(&mut anchor_offset.y));
|
||||
});
|
||||
});
|
||||
|
||||
if ui.button("Disable for 2 seconds").clicked() {
|
||||
*disabled_time = ui.input().time;
|
||||
}
|
||||
|
|
|
@ -52,6 +52,15 @@ impl Align {
|
|||
Self::Max => 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert `Min => -1.0`, `Center => 0.0` or `Max => 1.0`.
|
||||
pub fn to_sign(&self) -> f32 {
|
||||
match self {
|
||||
Self::Min => -1.0,
|
||||
Self::Center => 0.0,
|
||||
Self::Max => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Align {
|
||||
|
@ -88,6 +97,11 @@ impl Align2 {
|
|||
self.0[1]
|
||||
}
|
||||
|
||||
/// -1, 0, or +1 for each axis
|
||||
pub fn to_sign(&self) -> Vec2 {
|
||||
vec2(self.x().to_sign(), self.y().to_sign())
|
||||
}
|
||||
|
||||
/// Used e.g. to anchor a piece of text to a part of the rectangle.
|
||||
/// Give a position within the rect, specified by the aligns
|
||||
pub fn anchor_rect(self, rect: Rect) -> Rect {
|
||||
|
@ -119,6 +133,21 @@ impl Align2 {
|
|||
|
||||
Rect::from_min_size(Pos2::new(x, y), size)
|
||||
}
|
||||
|
||||
pub fn pos_in_rect(self, frame: &Rect) -> Pos2 {
|
||||
let x = match self.x() {
|
||||
Align::Min => frame.left(),
|
||||
Align::Center => frame.center().x,
|
||||
Align::Max => frame.right(),
|
||||
};
|
||||
let y = match self.y() {
|
||||
Align::Min => frame.top(),
|
||||
Align::Center => frame.center().y,
|
||||
Align::Max => frame.bottom(),
|
||||
};
|
||||
|
||||
pos2(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_size_in_rect(size: Vec2, frame: Rect) -> Rect {
|
||||
|
|
Loading…
Reference in a new issue