Grid layout and widget gallery (#106)

* Wrap Layout and Region into a new struct Placer

* [egui] Add a simple grid layout

* Refactor CollapsingHeader code (simplify header painting)

* Fix: allow putting a CollapsingHeader inside of a grid layout

* [demo] Add a widget gallery

Closes https://github.com/emilk/egui/issues/88

* Add optional striped grid background
This commit is contained in:
Emil Ernerfeldt 2021-01-11 20:58:36 +01:00 committed by GitHub
parent d344c9d9a3
commit 0b10fa5c29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 663 additions and 174 deletions

View file

@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added ⭐ ### Added ⭐
* Add a simple grid layout (`Grid`).
* Add `ui.allocate_at_least` and `ui.allocate_exact_size`. * Add `ui.allocate_at_least` and `ui.allocate_exact_size`.
### Changed 🔧 ### Changed 🔧

View file

@ -203,7 +203,12 @@ impl CollapsingHeader {
state.toggle(ui); state.toggle(ui);
} }
let bg_index = ui.painter().add(Shape::Noop); ui.painter().add(Shape::Rect {
rect: header_response.rect,
corner_radius: ui.style().interact(&header_response).corner_radius,
fill: ui.style().interact(&header_response).bg_fill,
stroke: Default::default(),
});
{ {
let (mut icon_rect, _) = ui.style().spacing.icon_rectangles(header_response.rect); let (mut icon_rect, _) = ui.style().spacing.icon_rectangles(header_response.rect);
@ -219,24 +224,13 @@ impl CollapsingHeader {
paint_icon(ui, openness, &icon_response); paint_icon(ui, openness, &icon_response);
} }
let painter = ui.painter(); ui.painter().galley(
painter.galley(
text_pos, text_pos,
galley, galley,
label.text_style_or_default(ui.style()), label.text_style_or_default(ui.style()),
ui.style().interact(&header_response).text_color(), ui.style().interact(&header_response).text_color(),
); );
painter.set(
bg_index,
Shape::Rect {
rect: header_response.rect,
corner_radius: ui.style().interact(&header_response).corner_radius,
fill: ui.style().interact(&header_response).bg_fill,
stroke: Default::default(),
},
);
Prepared { Prepared {
id, id,
header_response, header_response,
@ -249,27 +243,32 @@ impl CollapsingHeader {
ui: &mut Ui, ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> CollapsingResponse<R> { ) -> CollapsingResponse<R> {
let Prepared { // Make sure contents are bellow header,
id, // and make sure it is one unit (necessary for putting a `CollapsingHeader` in a grid).
header_response, ui.vertical(|ui| {
mut state, let Prepared {
} = self.begin(ui); id,
let ret_response = state.add_contents(ui, id, |ui| ui.indent(id, add_contents).0); header_response,
ui.memory().collapsing_headers.insert(id, state); mut state,
} = self.begin(ui);
let ret_response = state.add_contents(ui, id, |ui| ui.indent(id, add_contents).0);
ui.memory().collapsing_headers.insert(id, state);
if let Some((ret, response)) = ret_response { if let Some((ret, response)) = ret_response {
CollapsingResponse { CollapsingResponse {
header_response, header_response,
body_response: Some(response), body_response: Some(response),
body_returned: Some(ret), body_returned: Some(ret),
}
} else {
CollapsingResponse {
header_response,
body_response: None,
body_returned: None,
}
} }
} else { })
CollapsingResponse { .0
header_response,
body_response: None,
body_returned: None,
}
}
} }
} }

189
egui/src/grid.rs Normal file
View file

@ -0,0 +1,189 @@
use crate::*;
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
pub(crate) struct State {
col_widths: Vec<f32>,
row_heights: Vec<f32>,
}
impl State {
/// Returns `true` if this made the column wider.
fn set_min_col_width(&mut self, col: usize, width: f32) -> bool {
self.col_widths
.resize(self.col_widths.len().max(col + 1), 0.0);
if self.col_widths[col] < width {
self.col_widths[col] = width;
true
} else {
false
}
}
/// Returns `true` if this made the row higher.
fn set_min_row_height(&mut self, row: usize, height: f32) -> bool {
self.row_heights
.resize(self.row_heights.len().max(row + 1), 0.0);
if self.row_heights[row] < height {
self.row_heights[row] = height;
true
} else {
false
}
}
fn col_width(&self, col: usize) -> Option<f32> {
self.col_widths.get(col).copied()
}
fn row_height(&self, row: usize) -> Option<f32> {
self.row_heights.get(row).copied()
}
fn full_width(&self, x_spacing: f32) -> f32 {
self.col_widths.iter().sum::<f32>()
+ (self.col_widths.len().at_least(1) - 1) as f32 * x_spacing
}
}
// ----------------------------------------------------------------------------
pub(crate) struct GridLayout {
ctx: CtxRef,
id: Id,
state: State,
spacing: Vec2,
striped: bool,
initial_x: f32,
default_row_height: f32,
col: usize,
row: usize,
}
impl GridLayout {
pub(crate) fn new(ui: &Ui, id: Id) -> Self {
Self {
ctx: ui.ctx().clone(),
id,
state: ui.memory().grid.get(&id).cloned().unwrap_or_default(),
spacing: ui.style().spacing.item_spacing,
striped: false,
initial_x: ui.cursor().x,
default_row_height: 0.0,
col: 0,
row: 0,
}
}
/// If `true`, add a subtle background color to every other row.
///
/// This can make a table easier to read.
/// Default: `false`.
pub(crate) fn striped(mut self, striped: bool) -> Self {
self.striped = striped;
self
}
}
impl GridLayout {
fn row_height(&self, row: usize) -> f32 {
self.state
.row_height(row)
.unwrap_or(self.default_row_height)
}
pub(crate) fn available_rect(&self, region: &Region) -> Rect {
Rect::from_min_max(region.cursor, region.max_rect.max)
}
pub(crate) fn next_cell(&self, cursor: Pos2, child_size: Vec2) -> Rect {
let width = self.state.col_width(self.col).unwrap_or(0.0);
let height = self.row_height(self.row);
let size = child_size.max(vec2(width, height));
Rect::from_min_size(cursor, size)
}
pub(crate) fn advance(&mut self, cursor: &mut Pos2, rect: Rect) {
let dirty = self.state.set_min_col_width(self.col, rect.width());
let dirty = self.state.set_min_row_height(self.row, rect.height()) || dirty;
if dirty {
self.ctx.memory().grid.insert(self.id, self.state.clone());
self.ctx.request_repaint();
}
self.col += 1;
cursor.x += rect.width() + self.spacing.x;
}
pub(crate) fn end_row(&mut self, cursor: &mut Pos2, painter: &Painter) {
let row_height = self.row_height(self.row);
cursor.x = self.initial_x;
cursor.y += row_height + self.spacing.y;
self.col = 0;
self.row += 1;
if self.striped && self.row % 2 == 1 {
if let Some(height) = self.state.row_height(self.row) {
// Paint background for coming row:
let size = Vec2::new(self.state.full_width(self.spacing.x), height);
let rect = Rect::from_min_size(*cursor, size);
let color = Rgba::from_white_alpha(0.0075);
// let color = Rgba::from_black_alpha(0.2);
painter.rect_filled(rect, 2.0, color);
}
}
}
}
// ----------------------------------------------------------------------------
/// A simple `Grid` layout.
///
/// ```
/// # let ui = &mut egui::Ui::__test();
/// egui::Grid::new("some_unique_id").show(ui, |ui| {
/// ui.label("First row, first column");
/// ui.label("First row, second column");
/// ui.end_row();
///
/// ui.label("Second row, first column");
/// ui.label("Second row, second column");
/// ui.label("Second row, third column");
/// });
/// ```
pub struct Grid {
id_source: Id,
striped: bool,
}
impl Grid {
pub fn new(id_source: impl std::hash::Hash) -> Self {
Self {
id_source: Id::new(id_source),
striped: false,
}
}
/// If `true`, add a subtle background color to every other row.
///
/// This can make a table easier to read.
/// Default: `false`.
pub fn striped(mut self, striped: bool) -> Self {
self.striped = striped;
self
}
}
impl Grid {
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
let Self { id_source, striped } = self;
ui.wrap(|ui| {
let id = ui.make_persistent_id(id_source);
let grid = GridLayout::new(ui, id).striped(striped);
ui.set_grid(grid);
add_contents(ui)
})
.0
}
}

View file

@ -431,33 +431,17 @@ impl Layout {
} }
/// Apply justify or alignment after calling `next_space`. /// Apply justify or alignment after calling `next_space`.
pub(crate) fn justify_or_align(&self, mut rect: Rect, child_size: Vec2) -> Rect { pub(crate) fn justify_or_align(&self, rect: Rect, mut child_size: Vec2) -> Rect {
if self.main_dir.is_horizontal() { if self.main_dir.is_horizontal() {
debug_assert!((rect.width() - child_size.x).abs() < 0.1);
if self.cross_justify { if self.cross_justify {
rect // fill full height child_size.y = rect.height(); // fill full height
} else {
rect.min.y += match self.cross_align {
Align::Min => 0.0,
Align::Center => 0.5 * (rect.size().y - child_size.y),
Align::Max => rect.size().y - child_size.y,
};
rect.max.y = rect.min.y + child_size.y;
rect
} }
Align2([Align::Center, self.cross_align]).align_size_within_rect(child_size, rect)
} else { } else {
debug_assert!((rect.height() - child_size.y).abs() < 0.1);
if self.cross_justify { if self.cross_justify {
rect // justified: fill full width child_size.x = rect.width(); // fill full width
} else {
rect.min.x += match self.cross_align {
Align::Min => 0.0,
Align::Center => 0.5 * (rect.size().x - child_size.x),
Align::Max => rect.size().x - child_size.x,
};
rect.max.x = rect.min.x + child_size.x;
rect
} }
Align2([self.cross_align, Align::Center]).align_size_within_rect(child_size, rect)
} }
} }
@ -488,6 +472,18 @@ impl Layout {
Direction::BottomUp => pos2(outer_rect.left(), inner_rect.top() - item_spacing.y), Direction::BottomUp => pos2(outer_rect.left(), inner_rect.top() - item_spacing.y),
}; };
} }
/// Move to the next row in a wrapping layout.
/// Otherwise does nothing.
pub(crate) fn end_row(&mut self, region: &mut Region, item_spacing: Vec2) {
if self.main_wrap && self.is_horizontal() {
// New row
region.cursor = pos2(
region.max_rect.left(),
region.max_rect.bottom() + item_spacing.y,
);
}
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -495,14 +491,16 @@ impl Layout {
/// ## Debug stuff /// ## Debug stuff
impl Layout { impl Layout {
/// Shows where the next widget is going to be placed /// Shows where the next widget is going to be placed
pub(crate) fn debug_paint_cursor(&self, region: &Region, painter: &crate::Painter) { pub(crate) fn debug_paint_cursor(
&self,
region: &Region,
stroke: epaint::Stroke,
painter: &crate::Painter,
) {
use crate::paint::*; use crate::paint::*;
let cursor = region.cursor; let cursor = region.cursor;
let color = Color32::GREEN;
let stroke = Stroke::new(2.0, color);
let align; let align;
let l = 64.0; let l = 64.0;
@ -526,6 +524,6 @@ impl Layout {
} }
} }
painter.text(cursor, align, "cursor", TextStyle::Monospace, color); painter.text(cursor, align, "cursor", TextStyle::Monospace, stroke.color);
} }
} }

View file

@ -81,6 +81,7 @@
mod animation_manager; mod animation_manager;
pub mod containers; pub mod containers;
mod context; mod context;
pub(crate) mod grid;
mod id; mod id;
mod input; mod input;
mod introspection; mod introspection;
@ -89,6 +90,7 @@ mod layout;
mod memory; mod memory;
pub mod menu; pub mod menu;
mod painter; mod painter;
pub(crate) mod placer;
pub mod style; pub mod style;
mod types; mod types;
mod ui; mod ui;
@ -111,6 +113,7 @@ pub use epaint::{
pub use { pub use {
containers::*, containers::*,
context::{Context, CtxRef}, context::{Context, CtxRef},
grid::Grid,
id::Id, id::Id,
input::*, input::*,
layers::*, layers::*,

View file

@ -28,6 +28,7 @@ pub struct Memory {
// states of various types of widgets // states of various types of widgets
pub(crate) collapsing_headers: HashMap<Id, collapsing_header::State>, pub(crate) collapsing_headers: HashMap<Id, collapsing_header::State>,
pub(crate) grid: HashMap<Id, crate::grid::State>,
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) menu_bar: HashMap<Id, menu::BarState>, pub(crate) menu_bar: HashMap<Id, menu::BarState>,
pub(crate) resize: HashMap<Id, resize::State>, pub(crate) resize: HashMap<Id, resize::State>,

226
egui/src/placer.rs Normal file
View file

@ -0,0 +1,226 @@
use crate::*;
pub(crate) struct Placer {
/// If set this will take precedence over [`layout`].
grid: Option<grid::GridLayout>,
layout: Layout,
region: Region,
}
impl Placer {
pub(crate) fn new(max_rect: Rect, layout: Layout) -> Self {
let region = layout.region_from_max_rect(max_rect);
Self {
grid: None,
layout,
region,
}
}
pub(crate) fn set_grid(&mut self, grid: grid::GridLayout) {
self.grid = Some(grid);
}
pub(crate) fn layout(&self) -> &Layout {
&self.layout
}
pub(crate) fn prefer_right_to_left(&self) -> bool {
self.layout.prefer_right_to_left()
}
pub(crate) fn min_rect(&self) -> Rect {
self.region.min_rect
}
pub(crate) fn max_rect(&self) -> Rect {
self.region.max_rect
}
pub(crate) fn max_rect_finite(&self) -> Rect {
self.region.max_rect_finite()
}
pub(crate) fn force_set_min_rect(&mut self, min_rect: Rect) {
self.region.min_rect = min_rect;
}
pub(crate) fn cursor(&self) -> Pos2 {
self.region.cursor
}
}
impl Placer {
pub(crate) fn align_size_within_rect(&self, size: Vec2, outer: Rect) -> Rect {
self.layout.align_size_within_rect(size, outer)
}
pub(crate) fn available_rect_before_wrap(&self) -> Rect {
if let Some(grid) = &self.grid {
grid.available_rect(&self.region)
} else {
self.layout.available_rect_before_wrap(&self.region)
}
}
pub(crate) fn available_rect_before_wrap_finite(&self) -> Rect {
if let Some(grid) = &self.grid {
grid.available_rect(&self.region)
} else {
self.layout.available_rect_before_wrap_finite(&self.region)
}
}
/// Amount of space available for a widget.
/// For wrapping layouts, this is the maximum (after wrap).
pub(crate) fn available_size(&self) -> Vec2 {
if let Some(grid) = &self.grid {
grid.available_rect(&self.region).size()
} else {
self.layout.available_size(&self.region)
}
}
/// Returns where to put the next widget that is of the given size.
/// The returned "outer" `Rect` will always be justified along the cross axis.
/// This is what you then pass to `advance_after_outer_rect`.
/// Use `justify_or_align` to get the inner `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)
} else {
self.layout
.next_space(&self.region, child_size, item_spacing)
}
}
/// Apply justify or alignment after calling `next_space`.
pub(crate) fn justify_or_align(&self, rect: Rect, child_size: Vec2) -> Rect {
self.layout.justify_or_align(rect, child_size)
}
/// Advance the cursor by this many points.
pub(crate) fn advance_cursor(&mut self, amount: f32) {
debug_assert!(
self.grid.is_none(),
"You cannot advance the cursor when in a grid layout"
);
self.layout.advance_cursor(&mut self.region, amount)
}
/// Advance cursor after a widget was added to a specific rectangle.
/// `outer_rect` is a hack needed because the Vec2 cursor is not quite sufficient to keep track
/// of what is happening when we are doing wrapping layouts.
pub(crate) fn advance_after_outer_rect(
&mut self,
outer_rect: Rect,
inner_rect: Rect,
item_spacing: Vec2,
) {
if let Some(grid) = &mut self.grid {
grid.advance(&mut self.region.cursor, outer_rect)
} else {
self.layout.advance_after_outer_rect(
&mut self.region,
outer_rect,
inner_rect,
item_spacing,
)
}
}
/// Move to the next row in a grid layout or wrapping layout.
/// Otherwise does nothing.
pub(crate) fn end_row(&mut self, item_spacing: Vec2, painter: &Painter) {
if let Some(grid) = &mut self.grid {
grid.end_row(&mut self.region.cursor, painter)
} else {
self.layout.end_row(&mut self.region, item_spacing)
}
}
}
impl Placer {
/// Expand the `min_rect` and `max_rect` of this ui to include a child at the given rect.
pub(crate) fn expand_to_include_rect(&mut self, rect: Rect) {
self.region.expand_to_include_rect(rect);
}
/// Set the maximum width of the ui.
/// You won't be able to shrink it below the current minimum size.
pub(crate) fn set_max_width(&mut self, width: f32) {
#![allow(clippy::float_cmp)]
let Self { layout, region, .. } = self;
if layout.main_dir() == Direction::RightToLeft {
debug_assert_eq!(region.min_rect.max.x, region.max_rect.max.x);
region.max_rect.min.x = region.max_rect.max.x - width.at_least(region.min_rect.width());
} else {
debug_assert_eq!(region.min_rect.min.x, region.max_rect.min.x);
region.max_rect.max.x = region.max_rect.min.x + width.at_least(region.min_rect.width());
}
}
/// Set the maximum height of the ui.
/// You won't be able to shrink it below the current minimum size.
pub(crate) fn set_max_height(&mut self, height: f32) {
#![allow(clippy::float_cmp)]
let Self { layout, region, .. } = self;
if layout.main_dir() == Direction::BottomUp {
debug_assert_eq!(region.min_rect.max.y, region.max_rect.max.y);
region.max_rect.min.y =
region.max_rect.max.y - height.at_least(region.min_rect.height());
} else {
debug_assert_eq!(region.min_rect.min.y, region.max_rect.min.y);
region.max_rect.max.y =
region.max_rect.min.y + height.at_least(region.min_rect.height());
}
}
/// Set the minimum width of the ui.
/// This can't shrink the ui, only make it larger.
pub(crate) fn set_min_width(&mut self, width: f32) {
#![allow(clippy::float_cmp)]
let Self { layout, region, .. } = self;
if layout.main_dir() == Direction::RightToLeft {
debug_assert_eq!(region.min_rect.max.x, region.max_rect.max.x);
let min_rect = &mut region.min_rect;
min_rect.min.x = min_rect.min.x.min(min_rect.max.x - width);
} else {
debug_assert_eq!(region.min_rect.min.x, region.max_rect.min.x);
let min_rect = &mut region.min_rect;
min_rect.max.x = min_rect.max.x.max(min_rect.min.x + width);
}
region.max_rect = region.max_rect.union(region.min_rect);
}
/// Set the minimum height of the ui.
/// This can't shrink the ui, only make it larger.
pub(crate) fn set_min_height(&mut self, height: f32) {
#![allow(clippy::float_cmp)]
let Self { layout, region, .. } = self;
if layout.main_dir() == Direction::BottomUp {
debug_assert_eq!(region.min_rect.max.y, region.max_rect.max.y);
let min_rect = &mut region.min_rect;
min_rect.min.y = min_rect.min.y.min(min_rect.max.y - height);
} else {
debug_assert_eq!(region.min_rect.min.y, region.max_rect.min.y);
let min_rect = &mut region.min_rect;
min_rect.max.y = min_rect.max.y.max(min_rect.min.y + height);
}
region.max_rect = region.max_rect.union(region.min_rect);
}
}
impl Placer {
pub(crate) fn debug_paint_cursor(&self, painter: &crate::Painter) {
let color = Color32::GREEN;
let stroke = Stroke::new(2.0, color);
if let Some(grid) = &self.grid {
painter.rect_stroke(grid.next_cell(self.cursor(), Vec2::splat(0.0)), 1.0, stroke)
} else {
self.layout
.debug_paint_cursor(&self.region, stroke, painter)
}
}
}

View file

@ -3,7 +3,8 @@
use std::{hash::Hash, sync::Arc}; use std::{hash::Hash, sync::Arc};
use crate::{ use crate::{
color::*, containers::*, layout::*, mutex::MutexGuard, paint::text::Fonts, widgets::*, *, color::*, containers::*, layout::*, mutex::MutexGuard, paint::text::Fonts, placer::Placer,
widgets::*, *,
}; };
/// This is what you use to place widgets. /// This is what you use to place widgets.
@ -43,11 +44,8 @@ pub struct Ui {
/// The `Ui` implements copy-on-write for this. /// The `Ui` implements copy-on-write for this.
style: Arc<Style>, style: Arc<Style>,
/// The strategy for where to put the next widget. /// Handles the `Ui` size and the placement of new widgets.
layout: Layout, placer: Placer,
/// Sizes/bounds and cursor used by `Layout`.
region: Region,
} }
impl Ui { impl Ui {
@ -56,29 +54,24 @@ impl Ui {
pub fn new(ctx: CtxRef, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self { pub fn new(ctx: CtxRef, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self {
let style = ctx.style(); let style = ctx.style();
let layout = Layout::default();
let region = layout.region_from_max_rect(max_rect);
Ui { Ui {
id, id,
next_auto_id: id.with("auto").value(), next_auto_id: id.with("auto").value(),
painter: Painter::new(ctx, layer_id, clip_rect), painter: Painter::new(ctx, layer_id, clip_rect),
style, style,
layout, placer: Placer::new(max_rect, Layout::default()),
region,
} }
} }
pub fn child_ui(&mut self, max_rect: Rect, layout: Layout) -> Self { pub fn child_ui(&mut self, max_rect: Rect, layout: Layout) -> Self {
self.next_auto_id = self.next_auto_id.wrapping_add(1); self.next_auto_id = self.next_auto_id.wrapping_add(1);
let region = layout.region_from_max_rect(max_rect);
Ui { Ui {
id: self.id.with("child"), id: self.id.with("child"),
next_auto_id: Id::new(self.next_auto_id).with("child").value(), next_auto_id: Id::new(self.next_auto_id).with("child").value(),
painter: self.painter.clone(), painter: self.painter.clone(),
style: self.style.clone(), style: self.style.clone(),
layout, placer: Placer::new(max_rect, layout),
region,
} }
} }
@ -126,7 +119,7 @@ impl Ui {
} }
pub fn layout(&self) -> &Layout { pub fn layout(&self) -> &Layout {
&self.layout self.placer.layout()
} }
/// Create a painter for a sub-region of this Ui. /// Create a painter for a sub-region of this Ui.
@ -190,7 +183,7 @@ impl Ui {
/// ///
/// This will grow as new widgets are added, but never shrink. /// This will grow as new widgets are added, but never shrink.
pub fn min_rect(&self) -> Rect { pub fn min_rect(&self) -> Rect {
self.region.min_rect self.placer.min_rect()
} }
/// Size of content; same as `min_rect().size()` /// Size of content; same as `min_rect().size()`
@ -206,19 +199,19 @@ impl Ui {
/// If a new widget doesn't fit within the `max_rect` then the /// If a new widget doesn't fit within the `max_rect` then the
/// `Ui` will make room for it by expanding both `min_rect` and `max_rect`. /// `Ui` will make room for it by expanding both `min_rect` and `max_rect`.
pub fn max_rect(&self) -> Rect { pub fn max_rect(&self) -> Rect {
self.region.max_rect self.placer.max_rect()
}
/// Used for animation, kind of hacky
pub(crate) fn force_set_min_rect(&mut self, min_rect: Rect) {
self.region.min_rect = min_rect;
} }
/// This is like `max_rect()`, but will never be infinite. /// This is like `max_rect()`, but will never be infinite.
/// If the desired rect is infinite ("be as big as you want") /// If the desired rect is infinite ("be as big as you want")
/// this will be bounded by `min_rect` instead. /// this will be bounded by `min_rect` instead.
pub fn max_rect_finite(&self) -> Rect { pub fn max_rect_finite(&self) -> Rect {
self.region.max_rect_finite() self.placer.max_rect_finite()
}
/// Used for animation, kind of hacky
pub(crate) fn force_set_min_rect(&mut self, min_rect: Rect) {
self.placer.force_set_min_rect(min_rect)
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -233,31 +226,13 @@ impl Ui {
/// Set the maximum width of the ui. /// Set the maximum width of the ui.
/// You won't be able to shrink it below the current minimum size. /// You won't be able to shrink it below the current minimum size.
pub fn set_max_width(&mut self, width: f32) { pub fn set_max_width(&mut self, width: f32) {
#![allow(clippy::float_cmp)] self.placer.set_max_width(width);
if self.layout.main_dir() == Direction::RightToLeft {
debug_assert_eq!(self.min_rect().max.x, self.max_rect().max.x);
self.region.max_rect.min.x =
self.region.max_rect.max.x - width.at_least(self.min_rect().width());
} else {
debug_assert_eq!(self.min_rect().min.x, self.region.max_rect.min.x);
self.region.max_rect.max.x =
self.region.max_rect.min.x + width.at_least(self.min_rect().width());
}
} }
/// Set the maximum height of the ui. /// Set the maximum height of the ui.
/// You won't be able to shrink it below the current minimum size. /// You won't be able to shrink it below the current minimum size.
pub fn set_max_height(&mut self, height: f32) { pub fn set_max_height(&mut self, height: f32) {
#![allow(clippy::float_cmp)] self.placer.set_max_height(height);
if self.layout.main_dir() == Direction::BottomUp {
debug_assert_eq!(self.min_rect().max.y, self.region.max_rect.max.y);
self.region.max_rect.min.y =
self.region.max_rect.max.y - height.at_least(self.min_rect().height());
} else {
debug_assert_eq!(self.min_rect().min.y, self.region.max_rect.min.y);
self.region.max_rect.max.y =
self.region.max_rect.min.y + height.at_least(self.min_rect().height());
}
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -272,33 +247,13 @@ impl Ui {
/// Set the minimum width of the ui. /// Set the minimum width of the ui.
/// This can't shrink the ui, only make it larger. /// This can't shrink the ui, only make it larger.
pub fn set_min_width(&mut self, width: f32) { pub fn set_min_width(&mut self, width: f32) {
#![allow(clippy::float_cmp)] self.placer.set_min_width(width);
if self.layout.main_dir() == Direction::RightToLeft {
debug_assert_eq!(self.region.min_rect.max.x, self.region.max_rect.max.x);
let min_rect = &mut self.region.min_rect;
min_rect.min.x = min_rect.min.x.min(min_rect.max.x - width);
} else {
debug_assert_eq!(self.region.min_rect.min.x, self.region.max_rect.min.x);
let min_rect = &mut self.region.min_rect;
min_rect.max.x = min_rect.max.x.max(min_rect.min.x + width);
}
self.region.max_rect = self.region.max_rect.union(self.min_rect());
} }
/// Set the minimum height of the ui. /// Set the minimum height of the ui.
/// This can't shrink the ui, only make it larger. /// This can't shrink the ui, only make it larger.
pub fn set_min_height(&mut self, height: f32) { pub fn set_min_height(&mut self, height: f32) {
#![allow(clippy::float_cmp)] self.placer.set_min_height(height);
if self.layout.main_dir() == Direction::BottomUp {
debug_assert_eq!(self.region.min_rect.max.y, self.region.max_rect.max.y);
let min_rect = &mut self.region.min_rect;
min_rect.min.y = min_rect.min.y.min(min_rect.max.y - height);
} else {
debug_assert_eq!(self.region.min_rect.min.y, self.region.max_rect.min.y);
let min_rect = &mut self.region.min_rect;
min_rect.max.y = min_rect.max.y.max(min_rect.min.y + height);
}
self.region.max_rect = self.region.max_rect.union(self.min_rect());
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -318,7 +273,7 @@ impl Ui {
/// Expand the `min_rect` and `max_rect` of this ui to include a child at the given rect. /// Expand the `min_rect` and `max_rect` of this ui to include a child at the given rect.
pub fn expand_to_include_rect(&mut self, rect: Rect) { pub fn expand_to_include_rect(&mut self, rect: Rect) {
self.region.expand_to_include_rect(rect); self.placer.expand_to_include_rect(rect);
} }
/// `ui.set_width_range(min..=max);` is equivalent to `ui.set_min_width(min); ui.set_max_width(max);`. /// `ui.set_width_range(min..=max);` is equivalent to `ui.set_min_width(min); ui.set_max_width(max);`.
@ -342,7 +297,7 @@ impl Ui {
/// A small size should be interpreted as "as little as possible". /// A small size should be interpreted as "as little as possible".
/// An infinite size should be interpreted as "as much as you want". /// An infinite size should be interpreted as "as much as you want".
pub fn available_size(&self) -> Vec2 { pub fn available_size(&self) -> Vec2 {
self.layout.available_size(&self.region) self.placer.available_size()
} }
pub fn available_width(&self) -> f32 { pub fn available_width(&self) -> f32 {
@ -351,27 +306,25 @@ impl Ui {
/// In case of a wrapping layout, how much space is left on this row/column? /// In case of a wrapping layout, how much space is left on this row/column?
pub fn available_size_before_wrap(&self) -> Vec2 { pub fn available_size_before_wrap(&self) -> Vec2 {
self.layout.available_rect_before_wrap(&self.region).size() self.placer.available_rect_before_wrap().size()
} }
/// This is like `available_size_before_wrap()`, but will never be infinite. /// This is like `available_size_before_wrap()`, but will never be infinite.
/// Use this for components that want to grow without bounds (but shouldn't). /// Use this for components that want to grow without bounds (but shouldn't).
/// In most layouts the next widget will be put in the top left corner of this `Rect`. /// In most layouts the next widget will be put in the top left corner of this `Rect`.
pub fn available_size_before_wrap_finite(&self) -> Vec2 { pub fn available_size_before_wrap_finite(&self) -> Vec2 {
self.layout self.placer.available_rect_before_wrap_finite().size()
.available_rect_before_wrap_finite(&self.region)
.size()
} }
pub fn available_rect_before_wrap(&self) -> Rect { pub fn available_rect_before_wrap(&self) -> Rect {
self.layout.available_rect_before_wrap(&self.region) self.placer.available_rect_before_wrap()
} }
/// This is like `available_rect_before_wrap()`, but will never be infinite. /// This is like `available_rect_before_wrap()`, but will never be infinite.
/// Use this for components that want to grow without bounds (but shouldn't). /// Use this for components that want to grow without bounds (but shouldn't).
/// In most layouts the next widget will be put in the top left corner of this `Rect`. /// In most layouts the next widget will be put in the top left corner of this `Rect`.
pub fn available_rect_before_wrap_finite(&self) -> Rect { pub fn available_rect_before_wrap_finite(&self) -> Rect {
self.layout.available_rect_before_wrap_finite(&self.region) self.placer.available_rect_before_wrap_finite()
} }
} }
@ -439,7 +392,7 @@ impl Ui {
/// The direction is dependent on the layout. /// The direction is dependent on the layout.
/// This is useful for creating some extra space between widgets. /// This is useful for creating some extra space between widgets.
pub fn advance_cursor(&mut self, amount: f32) { pub fn advance_cursor(&mut self, amount: f32) {
self.layout.advance_cursor(&mut self.region, amount); self.placer.advance_cursor(amount);
} }
/// Allocate space for a widget and check for interaction in the space. /// Allocate space for a widget and check for interaction in the space.
@ -474,7 +427,7 @@ impl Ui {
pub fn allocate_exact_size(&mut self, desired_size: Vec2, sense: Sense) -> (Rect, Response) { pub fn allocate_exact_size(&mut self, desired_size: Vec2, sense: Sense) -> (Rect, Response) {
let response = self.allocate_response(desired_size, sense); let response = self.allocate_response(desired_size, sense);
let rect = self let rect = self
.layout() .placer
.align_size_within_rect(desired_size, response.rect); .align_size_within_rect(desired_size, response.rect);
(rect, response) (rect, response)
} }
@ -554,34 +507,28 @@ impl Ui {
/// Returns where to put the widget. /// Returns where to put the widget.
fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect { fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect {
let item_spacing = self.style().spacing.item_spacing; let item_spacing = self.style().spacing.item_spacing;
let outer_child_rect = self let outer_child_rect = self.placer.next_space(desired_size, item_spacing);
.layout let inner_child_rect = self.placer.justify_or_align(outer_child_rect, desired_size);
.next_space(&self.region, desired_size, item_spacing);
let inner_child_rect = self.layout.justify_or_align(outer_child_rect, desired_size);
self.layout.advance_after_outer_rect( self.placer
&mut self.region, .advance_after_outer_rect(outer_child_rect, inner_child_rect, item_spacing);
outer_child_rect, self.expand_to_include_rect(inner_child_rect);
inner_child_rect,
item_spacing,
);
self.region.expand_to_include_rect(inner_child_rect);
inner_child_rect inner_child_rect
} }
pub(crate) fn advance_cursor_after_rect(&mut self, rect: Rect) -> Id { pub(crate) fn advance_cursor_after_rect(&mut self, rect: Rect) -> Id {
let item_spacing = self.style().spacing.item_spacing; let item_spacing = self.style().spacing.item_spacing;
self.layout self.placer
.advance_after_outer_rect(&mut self.region, rect, rect, item_spacing); .advance_after_outer_rect(rect, rect, item_spacing);
self.region.expand_to_include_rect(rect); self.expand_to_include_rect(rect);
self.next_auto_id = self.next_auto_id.wrapping_add(1); self.next_auto_id = self.next_auto_id.wrapping_add(1);
Id::new(self.next_auto_id) Id::new(self.next_auto_id)
} }
pub(crate) fn cursor(&self) -> Pos2 { pub(crate) fn cursor(&self) -> Pos2 {
self.region.cursor self.placer.cursor()
} }
/// Allocated the given space and then adds content to that space. /// Allocated the given space and then adds content to that space.
@ -594,22 +541,19 @@ impl Ui {
add_contents: impl FnOnce(&mut Self) -> R, add_contents: impl FnOnce(&mut Self) -> R,
) -> (R, Response) { ) -> (R, Response) {
let item_spacing = self.style().spacing.item_spacing; let item_spacing = self.style().spacing.item_spacing;
let outer_child_rect = self let outer_child_rect = self.placer.next_space(desired_size, item_spacing);
.layout let inner_child_rect = self.placer.justify_or_align(outer_child_rect, desired_size);
.next_space(&self.region, desired_size, item_spacing);
let inner_child_rect = self.layout.justify_or_align(outer_child_rect, desired_size);
let mut child_ui = self.child_ui(inner_child_rect, self.layout); let mut child_ui = self.child_ui(inner_child_rect, *self.layout());
let ret = add_contents(&mut child_ui); let ret = add_contents(&mut child_ui);
let final_child_rect = child_ui.region.min_rect; let final_child_rect = child_ui.min_rect();
self.layout.advance_after_outer_rect( self.placer.advance_after_outer_rect(
&mut self.region,
outer_child_rect.union(final_child_rect), outer_child_rect.union(final_child_rect),
final_child_rect, final_child_rect,
item_spacing, item_spacing,
); );
self.region.expand_to_include_rect(final_child_rect); self.expand_to_include_rect(final_child_rect);
let response = self.interact(final_child_rect, child_ui.id, Sense::hover()); let response = self.interact(final_child_rect, child_ui.id, Sense::hover());
(ret, response) (ret, response)
@ -640,8 +584,7 @@ impl Ui {
/// }); /// });
/// ``` /// ```
pub fn scroll_to_cursor(&mut self, align: Align) { pub fn scroll_to_cursor(&mut self, align: Align) {
let scroll_y = self.region.cursor.y; let scroll_y = self.cursor().y;
self.ctx().frame_state().scroll_target = Some((scroll_y, align)); self.ctx().frame_state().scroll_target = Some((scroll_y, align));
} }
} }
@ -918,7 +861,7 @@ impl Ui {
/// Create a child ui. You can use this to temporarily change the Style of a sub-region, for instance. /// Create a child ui. You can use this to temporarily change the Style of a sub-region, for instance.
pub fn wrap<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) { pub fn wrap<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) {
let child_rect = self.available_rect_before_wrap(); let child_rect = self.available_rect_before_wrap();
let mut child_ui = self.child_ui(child_rect, self.layout); let mut child_ui = self.child_ui(child_rect, *self.layout());
let ret = add_contents(&mut child_ui); let ret = add_contents(&mut child_ui);
let size = child_ui.min_size(); let size = child_ui.min_size();
let response = self.allocate_response(size, Sense::hover()); let response = self.allocate_response(size, Sense::hover());
@ -962,16 +905,15 @@ impl Ui {
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> (R, Response) { ) -> (R, Response) {
assert!( assert!(
self.layout.is_vertical(), self.layout().is_vertical(),
"You can only indent vertical layouts, found {:?}", "You can only indent vertical layouts, found {:?}",
self.layout self.layout()
); );
let indent = vec2(self.style().spacing.indent, 0.0); let indent = vec2(self.style().spacing.indent, 0.0);
let child_rect = let child_rect = Rect::from_min_max(self.cursor() + indent, self.max_rect().right_bottom()); // TODO: wrong for reversed layouts
Rect::from_min_max(self.region.cursor + indent, self.max_rect().right_bottom()); // TODO: wrong for reversed layouts
let mut child_ui = Self { let mut child_ui = Self {
id: self.id.with(id_source), id: self.id.with(id_source),
..self.child_ui(child_rect, self.layout) ..self.child_ui(child_rect, *self.layout())
}; };
let ret = add_contents(&mut child_ui); let ret = add_contents(&mut child_ui);
let size = child_ui.min_size(); let size = child_ui.min_size();
@ -1017,10 +959,10 @@ impl Ui {
}; };
self.child_ui( self.child_ui(
Rect::from_min_size( Rect::from_min_size(
self.region.cursor + vec2(x, 0.0), self.cursor() + vec2(x, 0.0),
vec2(width, self.available_size_before_wrap().y), vec2(width, self.available_size_before_wrap().y),
), ),
self.layout, *self.layout(),
) )
} }
@ -1116,7 +1058,7 @@ impl Ui {
self.style().spacing.interact_size.y, // Assume there will be something interactive on the horizontal layout self.style().spacing.interact_size.y, // Assume there will be something interactive on the horizontal layout
); );
let layout = if self.layout.prefer_right_to_left() { let layout = if self.placer.prefer_right_to_left() {
Layout::right_to_left() Layout::right_to_left()
} else { } else {
Layout::left_to_right() Layout::left_to_right()
@ -1161,12 +1103,23 @@ impl Ui {
let ret = add_contents(&mut child_ui); let ret = add_contents(&mut child_ui);
let rect = child_ui.min_rect(); let rect = child_ui.min_rect();
let item_spacing = self.style().spacing.item_spacing; let item_spacing = self.style().spacing.item_spacing;
self.layout self.placer
.advance_after_outer_rect(&mut self.region, rect, rect, item_spacing); .advance_after_outer_rect(rect, rect, item_spacing);
self.region.expand_to_include_rect(rect); self.expand_to_include_rect(rect);
(ret, self.interact(rect, child_ui.id, Sense::hover())) (ret, self.interact(rect, child_ui.id, Sense::hover()))
} }
pub(crate) fn set_grid(&mut self, grid: grid::GridLayout) {
self.placer.set_grid(grid);
}
/// Move to the next row in a grid layout or wrapping layout.
/// Otherwise does nothing.
pub fn end_row(&mut self) {
self.placer
.end_row(self.style().spacing.item_spacing, &self.painter().clone());
}
/// Temporarily split split an Ui into several columns. /// Temporarily split split an Ui into several columns.
/// ///
/// ``` /// ```
@ -1184,7 +1137,7 @@ impl Ui {
let spacing = self.style().spacing.item_spacing.x; let spacing = self.style().spacing.item_spacing.x;
let total_spacing = spacing * (num_columns as f32 - 1.0); let total_spacing = spacing * (num_columns as f32 - 1.0);
let column_width = (self.available_width() - total_spacing) / (num_columns as f32); let column_width = (self.available_width() - total_spacing) / (num_columns as f32);
let top_left = self.region.cursor; let top_left = self.cursor();
let mut columns: Vec<Self> = (0..num_columns) let mut columns: Vec<Self> = (0..num_columns)
.map(|col_idx| { .map(|col_idx| {
@ -1224,6 +1177,6 @@ impl Ui {
impl Ui { impl Ui {
/// Shows where the next widget is going to be placed /// Shows where the next widget is going to be placed
pub fn debug_paint_cursor(&self) { pub fn debug_paint_cursor(&self) {
self.layout.debug_paint_cursor(&self.region, &self.painter); self.placer.debug_paint_cursor(&self.painter);
} }
} }

View file

@ -13,6 +13,7 @@ struct Demos {
impl Default for Demos { impl Default for Demos {
fn default() -> Self { fn default() -> Self {
let demos: Vec<Box<dyn super::Demo>> = vec![ let demos: Vec<Box<dyn super::Demo>> = vec![
Box::new(super::WidgetGallery::default()),
Box::new(super::FontBook::default()), Box::new(super::FontBook::default()),
Box::new(super::Painting::default()), Box::new(super::Painting::default()),
Box::new(super::DancingStrings::default()), Box::new(super::DancingStrings::default()),

View file

@ -17,13 +17,14 @@ mod scrolls;
mod sliders; mod sliders;
mod tests; mod tests;
pub mod toggle_switch; pub mod toggle_switch;
mod widget_gallery;
mod widgets; mod widgets;
mod window_options; mod window_options;
pub use { pub use {
app::*, dancing_strings::DancingStrings, demo_window::DemoWindow, demo_windows::*, app::*, dancing_strings::DancingStrings, demo_window::DemoWindow, demo_windows::*,
drag_and_drop::*, font_book::FontBook, painting::Painting, scrolls::Scrolls, sliders::Sliders, drag_and_drop::*, font_book::FontBook, painting::Painting, scrolls::Scrolls, sliders::Sliders,
tests::Tests, widgets::Widgets, window_options::WindowOptions, tests::Tests, widget_gallery::*, widgets::Widgets, window_options::WindowOptions,
}; };
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View file

@ -75,7 +75,7 @@ impl super::Demo for Painting {
impl super::View for Painting { impl super::View for Painting {
fn ui(&mut self, ui: &mut Ui) { fn ui(&mut self, ui: &mut Ui) {
ui.add(crate::__egui_github_link_file!("(source code)")); ui.add(crate::__egui_github_link_file!());
self.ui_control(ui); self.ui_control(ui);
ui.label("Paint with your mouse/touch!"); ui.label("Paint with your mouse/touch!");
Frame::dark_canvas(ui.style()).show(ui, |ui| { Frame::dark_canvas(ui.style()).show(ui, |ui| {

View file

@ -0,0 +1,117 @@
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
enum Enum {
First,
Second,
Third,
}
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
pub struct WidgetGallery {
boolean: bool,
radio: Enum,
scalar: f32,
string: String,
color: egui::Color32,
}
impl Default for WidgetGallery {
fn default() -> Self {
Self {
boolean: false,
radio: Enum::First,
scalar: 42.0,
string: "Hello World!".to_owned(),
color: egui::Color32::LIGHT_BLUE,
}
}
}
impl super::Demo for WidgetGallery {
fn name(&self) -> &str {
"🗄 Widget Gallery"
}
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 WidgetGallery {
fn ui(&mut self, ui: &mut egui::Ui) {
let Self {
boolean,
radio,
scalar,
string,
color,
} = self;
egui::Grid::new("my_grid").striped(true).show(ui, |ui| {
ui.label("Label:");
ui.label("Welcome to the widget gallery!");
ui.end_row();
ui.label("Text Input:");
ui.text_edit_singleline(string);
ui.end_row();
ui.label("Checkbox:");
ui.checkbox(boolean, "Checkbox");
ui.end_row();
ui.label("Radio buttons:");
ui.horizontal(|ui| {
ui.radio_value(radio, Enum::First, "First");
ui.radio_value(radio, Enum::Second, "Second");
ui.radio_value(radio, Enum::Third, "Third");
});
ui.end_row();
ui.label("ComboBox:");
egui::combo_box_with_label(ui, "Take your pick", format!("{:?}", radio), |ui| {
ui.selectable_value(radio, Enum::First, "First");
ui.selectable_value(radio, Enum::Second, "Second");
ui.selectable_value(radio, Enum::Third, "Third");
});
ui.end_row();
ui.label("Slider:");
ui.add(egui::Slider::f32(scalar, 0.0..=100.0).text("value"));
ui.end_row();
ui.label("DragValue:");
ui.add(egui::DragValue::f32(scalar).speed(1.0));
ui.end_row();
ui.label("Color picker:");
ui.color_edit_button_srgba(color);
ui.end_row();
ui.label("Image:");
ui.image(egui::TextureId::Egui, [24.0, 16.0])
.on_hover_text("The font texture");
ui.end_row();
ui.label("Button:");
if ui.button("Toggle boolean").clicked {
*boolean = !*boolean;
}
ui.end_row();
ui.label("ImageButton:");
if ui
.add(egui::ImageButton::new(egui::TextureId::Egui, [24.0, 16.0]))
.clicked
{
*boolean = !*boolean;
}
ui.end_row();
});
ui.add(crate::__egui_github_link_file!());
}
}