Add Ui functions for doing manual layout ("put this widget here")

This commit is contained in:
Emil Ernerfeldt 2021-02-07 13:48:55 +01:00
parent bca722ddf8
commit df4c0257c0
8 changed files with 231 additions and 65 deletions

View file

@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* Add support for secondary and middle mouse buttons.
* Add `Label` methods for code, strong, strikethrough, underline and italics.
* Add `ui.group(|ui| { … })` to visually group some widgets within a frame.
* Add `Ui` helpers for doing manual layout (`ui.put`, `ui.allocate_ui_at_rect` and more).
* Add `ui.set_enabled(false)` to disable all widgets in a `Ui` (grayed out and non-interactive).
* Add `TextEdit::hint_text` for showing a weak hint text when empty.
* `egui::popup::popup_below_widget`: show a popup area below another widget.

View file

@ -136,7 +136,7 @@ impl GridLayout {
Align2::LEFT_CENTER.align_size_within_rect(size, frame)
}
pub(crate) fn justify_or_align(&self, frame: Rect, size: Vec2) -> Rect {
pub(crate) fn justify_and_align(&self, frame: Rect, size: Vec2) -> Rect {
self.align_size_within_rect(size, frame)
}

View file

@ -115,6 +115,12 @@ pub struct Layout {
/// wrap to a new row when we reach the right side of the `max_rect`.
main_wrap: bool,
/// How to align things on the main axis.
main_align: Align,
/// Justify the main axis?
main_justify: bool,
/// How to align things on the cross axis.
/// For vertical layouts: put things to left, center or right?
/// For horizontal layouts: put things to top, center or bottom?
@ -133,6 +139,8 @@ impl Default for Layout {
Self {
main_dir: Direction::TopDown,
main_wrap: false,
main_align: Align::TOP,
main_justify: false,
cross_align: Align::LEFT,
cross_justify: false,
}
@ -145,6 +153,8 @@ impl Layout {
Self {
main_dir: Direction::LeftToRight,
main_wrap: false,
main_align: Align::Center, // looks best to e.g. center text within a button
main_justify: false,
cross_align: Align::Center,
cross_justify: false,
}
@ -154,6 +164,8 @@ impl Layout {
Self {
main_dir: Direction::RightToLeft,
main_wrap: false,
main_align: Align::Center, // looks best to e.g. center text within a button
main_justify: false,
cross_align: Align::Center,
cross_justify: false,
}
@ -163,6 +175,8 @@ impl Layout {
Self {
main_dir: Direction::TopDown,
main_wrap: false,
main_align: Align::Center, // looks best to e.g. center text within a button
main_justify: false,
cross_align,
cross_justify: false,
}
@ -177,6 +191,8 @@ impl Layout {
Self {
main_dir: Direction::BottomUp,
main_wrap: false,
main_align: Align::Center, // looks best to e.g. center text within a button
main_justify: false,
cross_align,
cross_justify: false,
}
@ -186,11 +202,24 @@ impl Layout {
Self {
main_dir,
main_wrap: false,
main_align: Align::Center, // looks best to e.g. center text within a button
main_justify: false,
cross_align,
cross_justify: false,
}
}
pub fn centered_and_justified(main_dir: Direction) -> Self {
Self {
main_dir,
main_wrap: false,
main_align: Align::Center,
main_justify: true,
cross_align: Align::Center,
cross_justify: true,
}
}
#[deprecated = "Use `top_down`"]
pub fn vertical(cross_align: Align) -> Self {
Self::top_down(cross_align)
@ -252,22 +281,38 @@ impl Layout {
}
fn horizontal_align(&self) -> Align {
match self.main_dir {
// Direction::LeftToRight => Align::LEFT,
// Direction::RightToLeft => Align::right(),
Direction::LeftToRight | Direction::RightToLeft => Align::Center, // looks better to e.g. center text within a button
Direction::TopDown | Direction::BottomUp => self.cross_align,
if self.is_horizontal() {
self.main_align
} else {
self.cross_align
}
}
fn vertical_align(&self) -> Align {
match self.main_dir {
// Direction::TopDown => Align::TOP,
// Direction::BottomUp => Align::BOTTOM,
Direction::TopDown | Direction::BottomUp => Align::Center, // looks better to e.g. center text within a button
if self.is_vertical() {
self.main_align
} else {
self.cross_align
}
}
Direction::LeftToRight | Direction::RightToLeft => self.cross_align,
fn align2(&self) -> Align2 {
Align2([self.horizontal_align(), self.vertical_align()])
}
fn horizontal_justify(&self) -> bool {
if self.is_horizontal() {
self.main_justify
} else {
self.cross_justify
}
}
fn vertical_justify(&self) -> bool {
if self.is_vertical() {
self.main_justify
} else {
self.cross_justify
}
}
}
@ -275,18 +320,7 @@ impl Layout {
/// ## Doing layout
impl Layout {
pub fn align_size_within_rect(&self, size: Vec2, outer: Rect) -> Rect {
let x = match self.horizontal_align() {
Align::Min => outer.left(),
Align::Center => outer.center().x - size.x / 2.0,
Align::Max => outer.right() - size.x,
};
let y = match self.vertical_align() {
Align::Min => outer.top(),
Align::Center => outer.center().y - size.y / 2.0,
Align::Max => outer.bottom() - size.y,
};
Rect::from_min_size(Pos2::new(x, y), size)
self.align2().align_size_within_rect(size, outer)
}
fn initial_cursor(&self, max_rect: Rect) -> Pos2 {
@ -369,7 +403,7 @@ impl Layout {
/// Returns where to put the next widget that is of the given size.
/// The returned `frame_rect` `Rect` will always be justified along the cross axis.
/// This is what you then pass to `advance_after_rects`.
/// Use `justify_or_align` to get the inner `widget_rect`.
/// Use `justify_and_align` to get the inner `widget_rect`.
#[allow(clippy::collapsible_if)]
pub(crate) fn next_space(
&self,
@ -422,12 +456,12 @@ impl Layout {
}
let available_size = self.available_size_before_wrap_finite(region);
if self.main_dir.is_horizontal() {
// Fill full height
child_size.y = child_size.y.max(available_size.y);
} else {
// Fill full width
child_size.x = child_size.x.max(available_size.x);
if self.is_vertical() || self.horizontal_justify() {
child_size.x = child_size.x.at_least(available_size.x); // fill full width
}
if self.is_horizontal() || self.vertical_justify() {
child_size.y = child_size.y.at_least(available_size.y); // fill full height
}
let child_pos = match self.main_dir {
@ -440,30 +474,15 @@ impl Layout {
Rect::from_min_size(child_pos, child_size)
}
/// Apply justify or alignment after calling `next_space`.
pub(crate) fn justify_or_align(&self, rect: Rect, mut child_size: Vec2) -> Rect {
if self.cross_justify {
if self.main_dir.is_horizontal() {
child_size.y = rect.height(); // fill full height
} else {
child_size.x = rect.width(); // fill full width
}
/// Apply justify (fill width/height) and/or alignment after calling `next_space`.
pub(crate) fn justify_and_align(&self, rect: Rect, mut child_size: Vec2) -> Rect {
if self.horizontal_justify() {
child_size.x = child_size.x.at_least(rect.width()); // fill full width
}
match self.main_dir {
Direction::LeftToRight => {
Align2([Align::Min, self.cross_align]).align_size_within_rect(child_size, rect)
}
Direction::RightToLeft => {
Align2([Align::Max, self.cross_align]).align_size_within_rect(child_size, rect)
}
Direction::TopDown => {
Align2([self.cross_align, Align::Min]).align_size_within_rect(child_size, rect)
}
Direction::BottomUp => {
Align2([self.cross_align, Align::Max]).align_size_within_rect(child_size, rect)
}
if self.vertical_justify() {
child_size.y = child_size.y.at_least(rect.height()); // fill full height
}
self.align_size_within_rect(child_size, rect)
}
/// Advance the cursor by this many points.

View file

@ -102,7 +102,7 @@ impl Placer {
/// Returns where to put the next widget that is of the given size.
/// The returned `frame_rect` will always be justified along the cross axis.
/// This is what you then pass to `advance_after_rects`.
/// Use `justify_or_align` to get the inner `widget_rect`.
/// Use `justify_and_align` to get the inner `widget_rect`.
pub(crate) fn next_space(&self, child_size: Vec2, item_spacing: Vec2) -> Rect {
if let Some(grid) = &self.grid {
grid.next_cell(self.region.cursor, child_size)
@ -113,11 +113,11 @@ impl Placer {
}
/// Apply justify or alignment after calling `next_space`.
pub(crate) fn justify_or_align(&self, rect: Rect, child_size: Vec2) -> Rect {
pub(crate) fn justify_and_align(&self, rect: Rect, child_size: Vec2) -> Rect {
if let Some(grid) = &self.grid {
grid.justify_or_align(rect, child_size)
grid.justify_and_align(rect, child_size)
} else {
self.layout.justify_or_align(rect, child_size)
self.layout.justify_and_align(rect, child_size)
}
}

View file

@ -17,7 +17,9 @@ use crate::{
/// ui.label("A shorter and more convenient way to add a label.");
/// ui.horizontal(|ui| {
/// ui.label("Add widgets");
/// ui.button("on the same row!");
/// if ui.button("on the same row!").clicked() {
/// /* … */
/// }
/// });
/// ```
pub struct Ui {
@ -274,7 +276,7 @@ impl Ui {
// ------------------------------------------------------------------------
/// ## Sizes etc
/// # Sizes etc
impl Ui {
/// Where and how large the `Ui` is already.
/// All widgets that have been added ot this `Ui` fits within this rectangle.
@ -517,7 +519,10 @@ impl Ui {
pub fn advance_cursor(&mut self, amount: f32) {
self.placer.advance_cursor(amount);
}
}
/// # Allocating space: where do I put my widgets?
impl Ui {
/// Allocate space for a widget and check for interaction in the space.
/// Returns a `Response` which contains a rectangle, id, and interaction info.
///
@ -629,7 +634,7 @@ impl Ui {
fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect {
let item_spacing = self.spacing().item_spacing;
let frame_rect = self.placer.next_space(desired_size, item_spacing);
let widget_rect = self.placer.justify_or_align(frame_rect, desired_size);
let widget_rect = self.placer.justify_and_align(frame_rect, desired_size);
self.placer
.advance_after_rects(frame_rect, widget_rect, item_spacing);
@ -637,7 +642,8 @@ impl Ui {
widget_rect
}
/// Allocate a specific part of the ui.
/// Allocate a specific part of the `Ui.
/// Ignore the layout of the `Ui: just put my widget here!
pub(crate) fn allocate_rect(&mut self, rect: Rect, sense: Sense) -> Response {
let id = self.advance_cursor_after_rect(rect);
self.interact(rect, id, sense)
@ -666,7 +672,9 @@ impl Ui {
) -> (R, Response) {
let item_spacing = self.spacing().item_spacing;
let outer_child_rect = self.placer.next_space(desired_size, item_spacing);
let inner_child_rect = self.placer.justify_or_align(outer_child_rect, desired_size);
let inner_child_rect = self
.placer
.justify_and_align(outer_child_rect, desired_size);
let mut child_ui = self.child_ui(inner_child_rect, *self.layout());
let ret = add_contents(&mut child_ui);
@ -682,6 +690,29 @@ impl Ui {
(ret, response)
}
/// Allocated the given rectangle and then adds content to that rectangle.
/// If the contents overflow, more space will be allocated.
/// When finished, the amount of space actually used (`min_rect`) will be allocated.
/// So you can request a lot of space and then use less.
pub fn allocate_ui_at_rect<R>(
&mut self,
max_rect: Rect,
add_contents: impl FnOnce(&mut Self) -> R,
) -> (R, Response) {
let mut child_ui = self.child_ui(max_rect, *self.layout());
let ret = add_contents(&mut child_ui);
let final_child_rect = child_ui.min_rect();
self.placer.advance_after_rects(
final_child_rect,
final_child_rect,
self.spacing().item_spacing,
);
let response = self.interact(final_child_rect, child_ui.id, Sense::hover());
(ret, response)
}
/// Convenience function to get a region to paint on
pub fn allocate_painter(&mut self, desired_size: Vec2, sense: Sense) -> (Response, Painter) {
let response = self.allocate_response(desired_size, sense);
@ -729,6 +760,22 @@ impl Ui {
widget.ui(self)
}
/// Add a widget to this `Ui` with a given max size.
pub fn add_sized(&mut self, max_size: Vec2, widget: impl Widget) -> Response {
self.allocate_ui(max_size, |ui| {
ui.centered_and_justified(|ui| ui.add(widget)).0
})
.0
}
/// Add a widget to this `Ui` at a specific location (manual layout).
pub fn put(&mut self, max_rect: Rect, widget: impl Widget) -> Response {
self.allocate_ui_at_rect(max_rect, |ui| {
ui.centered_and_justified(|ui| ui.add(widget)).0
})
.0
}
/// Shortcut for `add(Label::new(text))`
pub fn label(&mut self, label: impl Into<Label>) -> Response {
self.add(label.into())
@ -1256,6 +1303,17 @@ impl Ui {
(ret, self.interact(rect, child_ui.id, Sense::hover()))
}
/// This will make the next added widget centered and justified in the available space.
pub fn centered_and_justified<R>(
&mut self,
add_contents: impl FnOnce(&mut Self) -> R,
) -> (R, Response) {
self.with_layout(
Layout::centered_and_justified(Direction::TopDown),
add_contents,
)
}
pub(crate) fn set_grid(&mut self, grid: grid::GridLayout) {
self.placer.set_grid(grid);
}
@ -1332,7 +1390,7 @@ impl Ui {
// ----------------------------------------------------------------------------
/// ## Debug stuff
/// # Debug stuff
impl Ui {
/// Shows where the next widget is going to be placed
pub fn debug_paint_cursor(&self) {

View file

@ -22,10 +22,11 @@ impl Default for Demos {
Box::new(super::widget_gallery::WidgetGallery::default()),
Box::new(super::window_options::WindowOptions::default()),
// Tests:
Box::new(super::layout_test::LayoutTest::default()),
Box::new(super::tests::IdTest::default()),
Box::new(super::tests::TableTest::default()),
Box::new(super::tests::InputTest::default()),
Box::new(super::layout_test::LayoutTest::default()),
Box::new(super::tests::ManualLayoutTest::default()),
Box::new(super::tests::TableTest::default()),
];
Self {
open: vec![false; demos.len()],

View file

@ -52,6 +52,91 @@ impl super::View for IdTest {
// ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug, PartialEq)]
enum WidgetType {
Label,
Button,
TextEdit,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ManualLayoutTest {
widget_offset: egui::Vec2,
widget_size: egui::Vec2,
widget_type: WidgetType,
text_edit_contents: String,
}
impl Default for ManualLayoutTest {
fn default() -> Self {
Self {
widget_offset: egui::Vec2::splat(150.0),
widget_size: egui::Vec2::new(200.0, 100.0),
widget_type: WidgetType::Button,
text_edit_contents: crate::LOREM_IPSUM.to_owned(),
}
}
}
impl super::Demo for ManualLayoutTest {
fn name(&self) -> &str {
"Manual Layout Test"
}
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
use super::View;
self.ui(ui);
});
}
}
impl super::View for ManualLayoutTest {
fn ui(&mut self, ui: &mut egui::Ui) {
use egui::*;
reset_button(ui, self);
let Self {
widget_offset,
widget_size,
widget_type,
text_edit_contents,
} = self;
ui.horizontal(|ui| {
ui.label("Test widget:");
ui.radio_value(widget_type, WidgetType::Button, "Button");
ui.radio_value(widget_type, WidgetType::Label, "Label");
ui.radio_value(widget_type, WidgetType::TextEdit, "TextEdit");
});
Grid::new("pos_size").show(ui, |ui| {
ui.label("Widget position:");
ui.add(Slider::f32(&mut widget_offset.x, 0.0..=400.0));
ui.add(Slider::f32(&mut widget_offset.y, 0.0..=400.0));
ui.end_row();
ui.label("Widget size:");
ui.add(Slider::f32(&mut widget_size.x, 0.0..=400.0));
ui.add(Slider::f32(&mut widget_size.y, 0.0..=400.0));
ui.end_row();
});
let widget_rect = Rect::from_min_size(ui.min_rect().min + *widget_offset, *widget_size);
// Showing how to place a widget anywhere in the `Ui`:
match *widget_type {
WidgetType::Button => {
ui.put(widget_rect, Button::new("Example button"));
}
WidgetType::Label => {
ui.put(widget_rect, Label::new("Example label"));
}
WidgetType::TextEdit => {
ui.put(widget_rect, TextEdit::multiline(text_edit_contents));
}
}
}
}
// ----------------------------------------------------------------------------
pub struct TableTest {
num_cols: usize,
num_rows: usize,

View file

@ -62,6 +62,8 @@ impl super::View for WidgetGallery {
});
ui.set_enabled(*enabled);
ui.separator();
let grid = egui::Grid::new("my_grid")
.striped(true)
.spacing([40.0, 4.0]);