Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Window::pivot and position combo boxes better #2303

Merged
merged 5 commits into from
Nov 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
34 changes: 29 additions & 5 deletions crates/egui/src/containers/area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand All @@ -65,6 +66,7 @@ impl Area {
order: Order::Middle,
default_pos: None,
new_pos: None,
pivot: Align2::LEFT_TOP,
anchor: None,
drag_bounds: None,
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -208,6 +222,7 @@ impl Area {
enabled,
default_pos,
new_pos,
pivot,
anchor,
drag_bounds,
constrain,
Expand All @@ -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;
Expand Down
89 changes: 74 additions & 15 deletions crates/egui/src/containers/combo_box.rs
Original file line number Diff line number Diff line change
@@ -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.
///
Expand Down Expand Up @@ -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(),
Expand All @@ -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
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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,
));
}
}
}
32 changes: 28 additions & 4 deletions crates/egui/src/containers/popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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
Expand Down
40 changes: 29 additions & 11 deletions crates/egui/src/containers/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down