diff --git a/CHANGELOG.md b/CHANGELOG.md index 55dd6aaa..bc471d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/egui/src/containers/area.rs b/egui/src/containers/area.rs index 8fa6300e..194e75d2 100644 --- a/egui/src/containers/area.rs +++ b/egui/src/containers/area.rs @@ -48,6 +48,7 @@ pub struct Area { enabled: bool, order: Order, default_pos: Option, + anchor: Option<(Align2, Vec2)>, new_pos: Option, drag_bounds: Option, } @@ -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) -> 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) -> 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) -> 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 { diff --git a/egui/src/containers/resize.rs b/egui/src/containers/resize.rs index 11b8054c..df1f05d0 100644 --- a/egui/src/containers/resize.rs +++ b/egui/src/containers/resize.rs @@ -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; diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index 3d5b31ab..7d87b28e 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -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) -> 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) -> 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 { - 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 { diff --git a/egui_demo_lib/src/apps/demo/window_options.rs b/egui_demo_lib/src/apps/demo/window_options.rs index 94ca11bf..90172b22 100644 --- a/egui_demo_lib/src/apps/demo/window_options.rs +++ b/egui_demo_lib/src/apps/demo/window_options.rs @@ -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; } diff --git a/emath/src/align.rs b/emath/src/align.rs index 991e57c2..38dc41dd 100644 --- a/emath/src/align.rs +++ b/emath/src/align.rs @@ -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 {