From 0b10fa5c291f99e458117529bb1bbc432aa0ce08 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 11 Jan 2021 20:58:36 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + egui/src/containers/collapsing_header.rs | 63 +++-- egui/src/grid.rs | 189 +++++++++++++++ egui/src/layout.rs | 50 ++-- egui/src/lib.rs | 3 + egui/src/memory.rs | 1 + egui/src/placer.rs | 226 ++++++++++++++++++ egui/src/ui.rs | 181 ++++++-------- egui_demo_lib/src/apps/demo/demo_windows.rs | 1 + egui_demo_lib/src/apps/demo/mod.rs | 3 +- egui_demo_lib/src/apps/demo/painting.rs | 2 +- egui_demo_lib/src/apps/demo/widget_gallery.rs | 117 +++++++++ 12 files changed, 663 insertions(+), 174 deletions(-) create mode 100644 egui/src/grid.rs create mode 100644 egui/src/placer.rs create mode 100644 egui_demo_lib/src/apps/demo/widget_gallery.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index d9c71772..deeb786f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 🔧 diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index 7ad90478..987390f9 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -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 { - 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 } } diff --git a/egui/src/grid.rs b/egui/src/grid.rs new file mode 100644 index 00000000..d30ba63d --- /dev/null +++ b/egui/src/grid.rs @@ -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, + row_heights: Vec, +} + +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 { + self.col_widths.get(col).copied() + } + + fn row_height(&self, row: usize) -> Option { + self.row_heights.get(row).copied() + } + + fn full_width(&self, x_spacing: f32) -> f32 { + self.col_widths.iter().sum::() + + (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(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 + } +} diff --git a/egui/src/layout.rs b/egui/src/layout.rs index fd42bd3f..c99d73ea 100644 --- a/egui/src/layout.rs +++ b/egui/src/layout.rs @@ -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); } } diff --git a/egui/src/lib.rs b/egui/src/lib.rs index eebb1c3b..89d79e4b 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -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::*, diff --git a/egui/src/memory.rs b/egui/src/memory.rs index dea1441f..3ec9b934 100644 --- a/egui/src/memory.rs +++ b/egui/src/memory.rs @@ -28,6 +28,7 @@ pub struct Memory { // states of various types of widgets pub(crate) collapsing_headers: HashMap, + pub(crate) grid: HashMap, #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) menu_bar: HashMap, pub(crate) resize: HashMap, diff --git a/egui/src/placer.rs b/egui/src/placer.rs new file mode 100644 index 00000000..75107294 --- /dev/null +++ b/egui/src/placer.rs @@ -0,0 +1,226 @@ +use crate::*; + +pub(crate) struct Placer { + /// If set this will take precedence over [`layout`]. + grid: Option, + 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) + } + } +} diff --git a/egui/src/ui.rs b/egui/src/ui.rs index a22bb15d..75d73540 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -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