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
This commit is contained in:
parent
f0f41d60e1
commit
ef931c406c
5 changed files with 163 additions and 37 deletions
|
@ -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)).
|
||||
|
|
|
@ -49,6 +49,7 @@ pub struct Area {
|
|||
constrain: bool,
|
||||
order: Order,
|
||||
default_pos: Option<Pos2>,
|
||||
pivot: Align2,
|
||||
anchor: Option<(Align2, Vec2)>,
|
||||
new_pos: Option<Pos2>,
|
||||
drag_bounds: Option<Rect>,
|
||||
|
@ -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<Pos2>) -> 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<Pos2>) -> 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;
|
||||
|
|
|
@ -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<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
|
||||
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow)>;
|
||||
|
||||
/// 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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<R>(
|
||||
ui: &Ui,
|
||||
popup_id: Id,
|
||||
widget_response: &Response,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
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<R>(
|
||||
pub fn popup_above_or_below_widget<R>(
|
||||
ui: &Ui,
|
||||
popup_id: Id,
|
||||
widget_response: &Response,
|
||||
above_or_below: AboveOrBelow,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
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
|
||||
|
|
|
@ -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<Pos2>) -> 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<Pos2>) -> 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<Vec2>) -> 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())
|
||||
|
|
Loading…
Reference in a new issue