From ef931c406c47c64d71462886692787ebca4b7b04 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 13 Nov 2022 22:17:33 +0100 Subject: [PATCH] Add Window::pivot and position combo boxes better (#2303) * Paint ComboBox icon differently if opening upwards * Add Area::pivot and Window::pivot * Add Window::contrain * ComboBox: pop up above if it doesn't fit below * Add line to changelog --- CHANGELOG.md | 5 +- crates/egui/src/containers/area.rs | 34 ++++++++-- crates/egui/src/containers/combo_box.rs | 89 ++++++++++++++++++++----- crates/egui/src/containers/popup.rs | 32 +++++++-- crates/egui/src/containers/window.rs | 40 ++++++++--- 5 files changed, 163 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c530657d..dc4785b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,9 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Added `Key::Minus` and `Key::Equals` ([#2239](https://github.com/emilk/egui/pull/2239)). * Added `egui::gui_zoom` module with helpers for scaling the whole GUI of an app ([#2239](https://github.com/emilk/egui/pull/2239)). * You can now put one interactive widget on top of another, and only one will get interaction at a time ([#2244](https://github.com/emilk/egui/pull/2244)). -* Add `ui.centered`. -* Added `Area::constrain` which constrains area to the screen bounds. ([#2270](https://github.com/emilk/egui/pull/2270)). +* Added `ui.centered`. +* Added `Area::constrain` and `Window::constrain` which constrains area to the screen bounds. ([#2270](https://github.com/emilk/egui/pull/2270)). +* Added `Area::pivot` and `Window::pivot` which controls what part of the window to position. ([#2303](https://github.com/emilk/egui/pull/2303)). ### Changed 🔧 * Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)). diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 7ce2bab5..072af44e 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -49,6 +49,7 @@ pub struct Area { constrain: bool, order: Order, default_pos: Option, + pivot: Align2, anchor: Option<(Align2, Vec2)>, new_pos: Option, drag_bounds: Option, @@ -65,6 +66,7 @@ impl Area { order: Order::Middle, default_pos: None, new_pos: None, + pivot: Align2::LEFT_TOP, anchor: None, drag_bounds: None, } @@ -122,16 +124,28 @@ impl Area { self } + /// Positions the window and prevents it from being moved + pub fn fixed_pos(mut self, fixed_pos: impl Into) -> Self { + self.new_pos = Some(fixed_pos.into()); + self.movable = false; + self + } + /// Constrains this area to the screen bounds. pub fn constrain(mut self, constrain: bool) -> Self { self.constrain = constrain; self } - /// Positions the window and prevents it from being moved - pub fn fixed_pos(mut self, fixed_pos: impl Into) -> Self { - self.new_pos = Some(fixed_pos.into()); - self.movable = false; + /// Where the "root" of the area is. + /// + /// For instance, if you set this to [`Align2::RIGHT_TOP`] + /// then [`Self::fixed_pos`] will set the position of the right-top + /// corner of the area. + /// + /// Default: [`Align2::LEFT_TOP`]. + pub fn pivot(mut self, pivot: Align2) -> Self { + self.pivot = pivot; self } @@ -208,6 +222,7 @@ impl Area { enabled, default_pos, new_pos, + pivot, anchor, drag_bounds, constrain, @@ -229,9 +244,18 @@ impl Area { state.interactable = interactable; let mut temporarily_invisible = false; + if pivot != Align2::LEFT_TOP { + if is_new { + temporarily_invisible = true; // figure out the size first + } else { + state.pos.x -= pivot.x().to_factor() * state.size.x; + state.pos.y -= pivot.y().to_factor() * state.size.y; + } + } + if let Some((anchor, offset)) = anchor { if is_new { - temporarily_invisible = true; + temporarily_invisible = true; // figure out the size first } else { let screen = ctx.available_rect(); state.pos = anchor.align_size_within_rect(state.size, screen).min + offset; diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 49b9f8a1..9c33002f 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -1,8 +1,16 @@ -use crate::{style::WidgetVisuals, *}; use epaint::Shape; +use crate::{style::WidgetVisuals, *}; + +/// Indicate wether or not a popup will be shown above or below the box. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum AboveOrBelow { + Above, + Below, +} + /// A function that paints the [`ComboBox`] icon -pub type IconPainter = Box; +pub type IconPainter = Box; /// A drop-down selection menu with a descriptive label. /// @@ -89,6 +97,7 @@ impl ComboBox { /// rect: egui::Rect, /// visuals: &egui::style::WidgetVisuals, /// _is_open: bool, + /// _above_or_below: egui::AboveOrBelow, /// ) { /// let rect = egui::Rect::from_center_size( /// rect.center(), @@ -107,7 +116,10 @@ impl ComboBox { /// .show_ui(ui, |_ui| {}); /// # }); /// ``` - pub fn icon(mut self, icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool) + 'static) -> Self { + pub fn icon( + mut self, + icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static, + ) -> Self { self.icon = Some(Box::new(icon_fn)); self } @@ -213,6 +225,23 @@ fn combo_box_dyn<'c, R>( let popup_id = button_id.with("popup"); let is_popup_open = ui.memory().is_popup_open(popup_id); + + let popup_height = ui + .ctx() + .memory() + .areas + .get(popup_id) + .map_or(100.0, |state| state.size.y); + + let above_or_below = + if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height + < ui.ctx().input().screen_rect().bottom() + { + AboveOrBelow::Below + } else { + AboveOrBelow::Above + }; + let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| { // We don't want to change width when user selects something new let full_minimum_width = ui.spacing().slider_width; @@ -243,9 +272,15 @@ fn combo_box_dyn<'c, R>( icon_rect.expand(visuals.expansion), visuals, is_popup_open, + above_or_below, ); } else { - paint_default_icon(ui.painter(), icon_rect.expand(visuals.expansion), visuals); + paint_default_icon( + ui.painter(), + icon_rect.expand(visuals.expansion), + visuals, + above_or_below, + ); } let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect); @@ -256,12 +291,18 @@ fn combo_box_dyn<'c, R>( if button_response.clicked() { ui.memory().toggle_popup(popup_id); } - let inner = crate::popup::popup_below_widget(ui, popup_id, &button_response, |ui| { - ScrollArea::vertical() - .max_height(ui.spacing().combo_height) - .show(ui, menu_contents) - .inner - }); + let inner = crate::popup::popup_above_or_below_widget( + ui, + popup_id, + &button_response, + above_or_below, + |ui| { + ScrollArea::vertical() + .max_height(ui.spacing().combo_height) + .show(ui, menu_contents) + .inner + }, + ); InnerResponse { inner, @@ -316,13 +357,31 @@ fn button_frame( response } -fn paint_default_icon(painter: &Painter, rect: Rect, visuals: &WidgetVisuals) { +fn paint_default_icon( + painter: &Painter, + rect: Rect, + visuals: &WidgetVisuals, + above_or_below: AboveOrBelow, +) { let rect = Rect::from_center_size( rect.center(), vec2(rect.width() * 0.7, rect.height() * 0.45), ); - painter.add(Shape::closed_line( - vec![rect.left_top(), rect.right_top(), rect.center_bottom()], - visuals.fg_stroke, - )); + + match above_or_below { + AboveOrBelow::Above => { + // Upward pointing triangle + painter.add(Shape::closed_line( + vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()], + visuals.fg_stroke, + )); + } + AboveOrBelow::Below => { + // Downward pointing triangle + painter.add(Shape::closed_line( + vec![rect.left_top(), rect.right_top(), rect.center_bottom()], + visuals.fg_stroke, + )); + } + } } diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 992b6cf9..3ae49479 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -294,7 +294,23 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool { false } -/// Shows a popup below another widget. +/// Helper for [`popup_above_or_below_widget`]. +pub fn popup_below_widget( + ui: &Ui, + popup_id: Id, + widget_response: &Response, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> Option { + popup_above_or_below_widget( + ui, + popup_id, + widget_response, + AboveOrBelow::Below, + add_contents, + ) +} + +/// Shows a popup above or below another widget. /// /// Useful for drop-down menus (combo boxes) or suggestion menus under text fields. /// @@ -309,24 +325,32 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool { /// if response.clicked() { /// ui.memory().toggle_popup(popup_id); /// } -/// egui::popup::popup_below_widget(ui, popup_id, &response, |ui| { +/// let below = egui::AboveOrBelow::Below; +/// egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, |ui| { /// ui.set_min_width(200.0); // if you want to control the size /// ui.label("Some more info, or things you can select:"); /// ui.label("…"); /// }); /// # }); /// ``` -pub fn popup_below_widget( +pub fn popup_above_or_below_widget( ui: &Ui, popup_id: Id, widget_response: &Response, + above_or_below: AboveOrBelow, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { if ui.memory().is_popup_open(popup_id) { + let (pos, pivot) = match above_or_below { + AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM), + AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP), + }; + let inner = Area::new(popup_id) .order(Order::Foreground) .constrain(true) - .fixed_pos(widget_response.rect.left_bottom()) + .fixed_pos(pos) + .pivot(pivot) .show(ui.ctx(), |ui| { // Note: we use a separate clip-rect for this area, so the popup can be outside the parent. // See https://github.com/emilk/egui/issues/825 diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 6182b9f8..9d7032c5 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -122,6 +122,30 @@ impl<'open> Window<'open> { self } + /// Sets the window position and prevents it from being dragged around. + pub fn fixed_pos(mut self, pos: impl Into) -> Self { + self.area = self.area.fixed_pos(pos); + self + } + + /// Constrains this window to the screen bounds. + pub fn constrain(mut self, constrain: bool) -> Self { + self.area = self.area.constrain(constrain); + self + } + + /// Where the "root" of the window is. + /// + /// For instance, if you set this to [`Align2::RIGHT_TOP`] + /// then [`Self::fixed_pos`] will set the position of the right-top + /// corner of the window. + /// + /// Default: [`Align2::LEFT_TOP`]. + pub fn pivot(mut self, pivot: Align2) -> Self { + self.area = self.area.pivot(pivot); + self + } + /// Set anchor and distance. /// /// An anchor of `Align2::RIGHT_TOP` means "put the right-top corner of the window @@ -156,23 +180,17 @@ impl<'open> Window<'open> { self } - /// Set initial position and size of the window. - pub fn default_rect(self, rect: Rect) -> Self { - self.default_pos(rect.min).default_size(rect.size()) - } - - /// Sets the window position and prevents it from being dragged around. - pub fn fixed_pos(mut self, pos: impl Into) -> Self { - self.area = self.area.fixed_pos(pos); - self - } - /// Sets the window size and prevents it from being resized by dragging its edges. pub fn fixed_size(mut self, size: impl Into) -> Self { self.resize = self.resize.fixed_size(size); self } + /// Set initial position and size of the window. + pub fn default_rect(self, rect: Rect) -> Self { + self.default_pos(rect.min).default_size(rect.size()) + } + /// Sets the window pos and size and prevents it from being moved and resized by dragging its edges. pub fn fixed_rect(self, rect: Rect) -> Self { self.fixed_pos(rect.min).fixed_size(rect.size())