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:
Emil Ernerfeldt 2021-04-18 10:01:41 +02:00 committed by GitHub
parent 5667e7eb51
commit 580d27e0d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 220 additions and 56 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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;

View file

@ -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 {

View file

@ -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;
}

View file

@ -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 {