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 ⭐
* Add a simple grid layout (`Grid`).
* Add `ui.allocate_at_least` and `ui.allocate_exact_size`.
### Changed 🔧

View file

@ -203,7 +203,12 @@ impl CollapsingHeader {
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);
@ -219,24 +224,13 @@ impl CollapsingHeader {
paint_icon(ui, openness, &icon_response);
}
let painter = ui.painter();
painter.galley(
ui.painter().galley(
text_pos,
galley,
label.text_style_or_default(ui.style()),
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 {
id,
header_response,
@ -249,27 +243,32 @@ impl CollapsingHeader {
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> CollapsingResponse<R> {
let Prepared {
id,
header_response,
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);
// Make sure contents are bellow header,
// and make sure it is one unit (necessary for putting a `CollapsingHeader` in a grid).
ui.vertical(|ui| {
let Prepared {
id,
header_response,
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 {
CollapsingResponse {
header_response,
body_response: Some(response),
body_returned: Some(ret),
if let Some((ret, response)) = ret_response {
CollapsingResponse {
header_response,
body_response: Some(response),
body_returned: Some(ret),
}
} else {
CollapsingResponse {
header_response,
body_response: None,
body_returned: None,
}
}
} else {
CollapsingResponse {
header_response,
body_response: None,
body_returned: None,
}
}
})
.0
}
}

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`.
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() {
debug_assert!((rect.width() - child_size.x).abs() < 0.1);
if self.cross_justify {
rect // 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
child_size.y = rect.height(); // fill full height
}
Align2([Align::Center, self.cross_align]).align_size_within_rect(child_size, rect)
} else {
debug_assert!((rect.height() - child_size.y).abs() < 0.1);
if self.cross_justify {
rect // justified: 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
child_size.x = rect.width(); // fill full width
}
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),
};
}
/// 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
impl Layout {
/// 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::*;
let cursor = region.cursor;
let color = Color32::GREEN;
let stroke = Stroke::new(2.0, color);
let align;
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;
pub mod containers;
mod context;
pub(crate) mod grid;
mod id;
mod input;
mod introspection;
@ -89,6 +90,7 @@ mod layout;
mod memory;
pub mod menu;
mod painter;
pub(crate) mod placer;
pub mod style;
mod types;
mod ui;
@ -111,6 +113,7 @@ pub use epaint::{
pub use {
containers::*,
context::{Context, CtxRef},
grid::Grid,
id::Id,
input::*,
layers::*,

View file

@ -28,6 +28,7 @@ pub struct Memory {
// states of various types of widgets
pub(crate) collapsing_headers: HashMap<Id, collapsing_header::State>,
pub(crate) grid: HashMap<Id, crate::grid::State>,
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) menu_bar: HashMap<Id, menu::BarState>,
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 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.
@ -43,11 +44,8 @@ pub struct Ui {
/// The `Ui` implements copy-on-write for this.
style: Arc<Style>,
/// The strategy for where to put the next widget.
layout: Layout,
/// Sizes/bounds and cursor used by `Layout`.
region: Region,
/// Handles the `Ui` size and the placement of new widgets.
placer: Placer,
}
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 {
let style = ctx.style();
let layout = Layout::default();
let region = layout.region_from_max_rect(max_rect);
Ui {
id,
next_auto_id: id.with("auto").value(),
painter: Painter::new(ctx, layer_id, clip_rect),
style,
layout,
region,
placer: Placer::new(max_rect, Layout::default()),
}
}
pub fn child_ui(&mut self, max_rect: Rect, layout: Layout) -> Self {
self.next_auto_id = self.next_auto_id.wrapping_add(1);
let region = layout.region_from_max_rect(max_rect);
Ui {
id: self.id.with("child"),
next_auto_id: Id::new(self.next_auto_id).with("child").value(),
painter: self.painter.clone(),
style: self.style.clone(),
layout,
region,
placer: Placer::new(max_rect, layout),
}
}
@ -126,7 +119,7 @@ impl Ui {
}
pub fn layout(&self) -> &Layout {
&self.layout
self.placer.layout()
}
/// 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.
pub fn min_rect(&self) -> Rect {
self.region.min_rect
self.placer.min_rect()
}
/// 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
/// `Ui` will make room for it by expanding both `min_rect` and `max_rect`.
pub fn max_rect(&self) -> Rect {
self.region.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;
self.placer.max_rect()
}
/// This is like `max_rect()`, but will never be infinite.
/// If the desired rect is infinite ("be as big as you want")
/// this will be bounded by `min_rect` instead.
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.
/// You won't be able to shrink it below the current minimum size.
pub fn set_max_width(&mut self, width: f32) {
#![allow(clippy::float_cmp)]
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());
}
self.placer.set_max_width(width);
}
/// Set the maximum height of the ui.
/// You won't be able to shrink it below the current minimum size.
pub fn set_max_height(&mut self, height: f32) {
#![allow(clippy::float_cmp)]
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());
}
self.placer.set_max_height(height);
}
// ------------------------------------------------------------------------
@ -272,33 +247,13 @@ impl Ui {
/// Set the minimum width of the ui.
/// This can't shrink the ui, only make it larger.
pub fn set_min_width(&mut self, width: f32) {
#![allow(clippy::float_cmp)]
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());
self.placer.set_min_width(width);
}
/// Set the minimum height of the ui.
/// This can't shrink the ui, only make it larger.
pub fn set_min_height(&mut self, height: f32) {
#![allow(clippy::float_cmp)]
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());
self.placer.set_min_height(height);
}
// ------------------------------------------------------------------------
@ -318,7 +273,7 @@ impl Ui {
/// 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) {
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);`.
@ -342,7 +297,7 @@ impl Ui {
/// A small size should be interpreted as "as little as possible".
/// An infinite size should be interpreted as "as much as you want".
pub fn available_size(&self) -> Vec2 {
self.layout.available_size(&self.region)
self.placer.available_size()
}
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?
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.
/// 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`.
pub fn available_size_before_wrap_finite(&self) -> Vec2 {
self.layout
.available_rect_before_wrap_finite(&self.region)
.size()
self.placer.available_rect_before_wrap_finite().size()
}
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.
/// 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`.
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.
/// This is useful for creating some extra space between widgets.
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.
@ -474,7 +427,7 @@ impl Ui {
pub fn allocate_exact_size(&mut self, desired_size: Vec2, sense: Sense) -> (Rect, Response) {
let response = self.allocate_response(desired_size, sense);
let rect = self
.layout()
.placer
.align_size_within_rect(desired_size, response.rect);
(rect, response)
}
@ -554,34 +507,28 @@ impl Ui {
/// Returns where to put the widget.
fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect {
let item_spacing = self.style().spacing.item_spacing;
let outer_child_rect = self
.layout
.next_space(&self.region, desired_size, item_spacing);
let inner_child_rect = self.layout.justify_or_align(outer_child_rect, desired_size);
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);
self.layout.advance_after_outer_rect(
&mut self.region,
outer_child_rect,
inner_child_rect,
item_spacing,
);
self.region.expand_to_include_rect(inner_child_rect);
self.placer
.advance_after_outer_rect(outer_child_rect, inner_child_rect, item_spacing);
self.expand_to_include_rect(inner_child_rect);
inner_child_rect
}
pub(crate) fn advance_cursor_after_rect(&mut self, rect: Rect) -> Id {
let item_spacing = self.style().spacing.item_spacing;
self.layout
.advance_after_outer_rect(&mut self.region, rect, rect, item_spacing);
self.region.expand_to_include_rect(rect);
self.placer
.advance_after_outer_rect(rect, rect, item_spacing);
self.expand_to_include_rect(rect);
self.next_auto_id = self.next_auto_id.wrapping_add(1);
Id::new(self.next_auto_id)
}
pub(crate) fn cursor(&self) -> Pos2 {
self.region.cursor
self.placer.cursor()
}
/// Allocated the given space and then adds content to that space.
@ -594,22 +541,19 @@ impl Ui {
add_contents: impl FnOnce(&mut Self) -> R,
) -> (R, Response) {
let item_spacing = self.style().spacing.item_spacing;
let outer_child_rect = self
.layout
.next_space(&self.region, desired_size, item_spacing);
let inner_child_rect = self.layout.justify_or_align(outer_child_rect, desired_size);
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 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 final_child_rect = child_ui.region.min_rect;
let final_child_rect = child_ui.min_rect();
self.layout.advance_after_outer_rect(
&mut self.region,
self.placer.advance_after_outer_rect(
outer_child_rect.union(final_child_rect),
final_child_rect,
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());
(ret, response)
@ -640,8 +584,7 @@ impl Ui {
/// });
/// ```
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));
}
}
@ -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.
pub fn wrap<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Response) {
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 size = child_ui.min_size();
let response = self.allocate_response(size, Sense::hover());
@ -962,16 +905,15 @@ impl Ui {
add_contents: impl FnOnce(&mut Ui) -> R,
) -> (R, Response) {
assert!(
self.layout.is_vertical(),
self.layout().is_vertical(),
"You can only indent vertical layouts, found {:?}",
self.layout
self.layout()
);
let indent = vec2(self.style().spacing.indent, 0.0);
let child_rect =
Rect::from_min_max(self.region.cursor + indent, self.max_rect().right_bottom()); // TODO: wrong for reversed layouts
let child_rect = Rect::from_min_max(self.cursor() + indent, self.max_rect().right_bottom()); // TODO: wrong for reversed layouts
let mut child_ui = Self {
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 size = child_ui.min_size();
@ -1017,10 +959,10 @@ impl Ui {
};
self.child_ui(
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),
),
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
);
let layout = if self.layout.prefer_right_to_left() {
let layout = if self.placer.prefer_right_to_left() {
Layout::right_to_left()
} else {
Layout::left_to_right()
@ -1161,12 +1103,23 @@ impl Ui {
let ret = add_contents(&mut child_ui);
let rect = child_ui.min_rect();
let item_spacing = self.style().spacing.item_spacing;
self.layout
.advance_after_outer_rect(&mut self.region, rect, rect, item_spacing);
self.region.expand_to_include_rect(rect);
self.placer
.advance_after_outer_rect(rect, rect, item_spacing);
self.expand_to_include_rect(rect);
(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.
///
/// ```
@ -1184,7 +1137,7 @@ impl Ui {
let spacing = self.style().spacing.item_spacing.x;
let total_spacing = spacing * (num_columns as f32 - 1.0);
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)
.map(|col_idx| {
@ -1224,6 +1177,6 @@ impl Ui {
impl Ui {
/// Shows where the next widget is going to be placed
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 {
fn default() -> Self {
let demos: Vec<Box<dyn super::Demo>> = vec![
Box::new(super::WidgetGallery::default()),
Box::new(super::FontBook::default()),
Box::new(super::Painting::default()),
Box::new(super::DancingStrings::default()),

View file

@ -17,13 +17,14 @@ mod scrolls;
mod sliders;
mod tests;
pub mod toggle_switch;
mod widget_gallery;
mod widgets;
mod window_options;
pub use {
app::*, dancing_strings::DancingStrings, demo_window::DemoWindow, demo_windows::*,
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 {
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);
ui.label("Paint with your mouse/touch!");
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!());
}
}