diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b2d5595..9d959a5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,11 +20,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Add: `ui.spacing()`, `ui.spacing_mut()`, `ui.visuals()`, `ui.visuals_mut()`. * Add: `ctx.set_visuals()`. * You can now control text wrapping with `Style::wrap`. +* Add `Grid::max_col_width`. ### Changed 🔧 * Text will now wrap at newlines, spaces, dashes, punctuation or in the middle of a words if necessary, in that order of priority. * Widgets will now always line break at `\n` characters. +* Widgets will now more intelligently choose wether or not to wrap text. * `mouse` has been renamed `pointer` everywhere (to make it clear it includes touches too). * Most parts of `Response` are now methods, so `if ui.button("…").clicked {` is now `if ui.button("…").clicked() {`. * `Response::active` is now gone. You can use `response.dragged()` or `response.clicked()` instead. diff --git a/egui/src/grid.rs b/egui/src/grid.rs index c0772d1e..af649625 100644 --- a/egui/src/grid.rs +++ b/egui/src/grid.rs @@ -52,6 +52,7 @@ pub(crate) struct GridLayout { striped: bool, initial_x: f32, min_cell_size: Vec2, + max_cell_size: Vec2, col: usize, row: usize, } @@ -70,6 +71,7 @@ impl GridLayout { striped: false, initial_x: ui.cursor().x, min_cell_size: ui.spacing().interact_size, + max_cell_size: Vec2::INFINITY, col: 0, row: 0, } @@ -88,6 +90,10 @@ impl GridLayout { .unwrap_or(self.min_cell_size.y) } + pub(crate) fn wrap_text(&self) -> bool { + self.max_cell_size.x.is_finite() + } + pub(crate) fn available_rect(&self, region: &Region) -> Rect { // let mut rect = Rect::from_min_max(region.cursor, region.max_rect.max); // rect.set_height(rect.height().at_least(self.min_cell_size.y)); @@ -98,14 +104,22 @@ impl GridLayout { } pub(crate) fn available_rect_finite(&self, region: &Region) -> Rect { - // If we want to allow width-filling widgets like `Separator` in one of the first cells - // then we need to make sure they don't spill out of the first cell: - let width = self.prev_state.col_width(self.col); - let width = width.or_else(|| self.curr_state.col_width(self.col)); - let width = width.unwrap_or_default().at_least(self.min_cell_size.x); + let width = if self.max_cell_size.x.is_finite() { + // TODO: should probably heed `prev_state` here too + self.max_cell_size.x + } else { + // If we want to allow width-filling widgets like `Separator` in one of the first cells + // then we need to make sure they don't spill out of the first cell: + self.prev_state + .col_width(self.col) + .or_else(|| self.curr_state.col_width(self.col)) + .unwrap_or(self.min_cell_size.x) + }; let height = region.max_rect_finite().max.y - region.cursor.y; - let height = height.at_least(self.min_cell_size.y); + let height = height + .at_least(self.min_cell_size.y) + .at_most(self.max_cell_size.y); Rect::from_min_size(region.cursor, vec2(width, height)) } @@ -227,6 +241,7 @@ pub struct Grid { striped: bool, min_col_width: Option, min_row_height: Option, + max_cell_size: Vec2, spacing: Option, } @@ -238,6 +253,7 @@ impl Grid { striped: false, min_col_width: None, min_row_height: None, + max_cell_size: Vec2::INFINITY, spacing: None, } } @@ -265,6 +281,12 @@ impl Grid { self } + /// Set soft maximum width (wrapping width) of each column. + pub fn max_col_width(mut self, max_col_width: f32) -> Self { + self.max_cell_size.x = max_col_width; + self + } + /// Set spacing between columns/rows. /// Default: [`crate::style::Spacing::item_spacing`]. pub fn spacing(mut self, spacing: impl Into) -> Self { @@ -280,6 +302,7 @@ impl Grid { striped, min_col_width, min_row_height, + max_cell_size, spacing, } = self; let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x); @@ -296,6 +319,7 @@ impl Grid { striped, spacing, min_cell_size: vec2(min_col_width, min_row_height), + max_cell_size, ..GridLayout::new(ui, id) }; diff --git a/egui/src/placer.rs b/egui/src/placer.rs index 120ad89c..5e5ae10e 100644 --- a/egui/src/placer.rs +++ b/egui/src/placer.rs @@ -27,6 +27,10 @@ impl Placer { } } + pub(crate) fn grid(&self) -> Option<&grid::GridLayout> { + self.grid.as_ref() + } + pub(crate) fn is_grid(&self) -> bool { self.grid.is_some() } diff --git a/egui/src/ui.rs b/egui/src/ui.rs index be2b25d6..3d6eacc7 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -175,6 +175,8 @@ impl Ui { pub fn wrap_text(&self) -> bool { if let Some(wrap) = self.style.wrap { wrap + } else if let Some(grid) = self.placer.grid() { + grid.wrap_text() } else { // In vertical layouts we wrap text, but in horizontal we keep going. self.layout().is_vertical() @@ -1226,6 +1228,10 @@ impl Ui { self.placer.is_grid() } + pub(crate) fn grid(&self) -> Option<&grid::GridLayout> { + self.placer.grid() + } + /// Move to the next row in a grid layout or wrapping layout. /// Otherwise does nothing. pub fn end_row(&mut self) { diff --git a/egui/src/widgets/label.rs b/egui/src/widgets/label.rs index f4d419bd..1a51aae2 100644 --- a/egui/src/widgets/label.rs +++ b/egui/src/widgets/label.rs @@ -215,8 +215,12 @@ impl Label { fn should_wrap(&self, ui: &Ui) -> bool { self.wrap.or(ui.style().wrap).unwrap_or_else(|| { - let layout = ui.layout(); - layout.is_vertical() || layout.is_horizontal() && layout.main_wrap() + if let Some(grid) = ui.grid() { + grid.wrap_text() + } else { + let layout = ui.layout(); + layout.is_vertical() || layout.is_horizontal() && layout.main_wrap() + } }) } } diff --git a/egui_demo_lib/src/apps/demo/demo_windows.rs b/egui_demo_lib/src/apps/demo/demo_windows.rs index 2405bf98..e400c501 100644 --- a/egui_demo_lib/src/apps/demo/demo_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_windows.rs @@ -24,7 +24,8 @@ impl Default for Demos { // Tests: Box::new(super::layout_test::LayoutTest::default()), Box::new(super::tests::IdTest::default()), - Box::new(super::input_test::InputTest::default()), + Box::new(super::tests::TableTest::default()), + Box::new(super::tests::InputTest::default()), ]; Self { open: vec![false; demos.len()], diff --git a/egui_demo_lib/src/apps/demo/input_test.rs b/egui_demo_lib/src/apps/demo/input_test.rs deleted file mode 100644 index effe508a..00000000 --- a/egui_demo_lib/src/apps/demo/input_test.rs +++ /dev/null @@ -1,52 +0,0 @@ -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] -#[derive(Default)] -pub struct InputTest { - info: String, -} - -impl super::Demo for InputTest { - fn name(&self) -> &str { - "🖱 Input Test" - } - - fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { - egui::Window::new(self.name()) - .open(open) - .resizable(false) - .show(ctx, |ui| { - use super::View; - self.ui(ui); - }); - } -} - -impl super::View for InputTest { - fn ui(&mut self, ui: &mut egui::Ui) { - let response = ui.add( - egui::Button::new("Click, double-click or drag me with any mouse button") - .sense(egui::Sense::click_and_drag()), - ); - - let mut new_info = String::new(); - for &button in &[ - egui::PointerButton::Primary, - egui::PointerButton::Secondary, - egui::PointerButton::Middle, - ] { - if response.clicked_by(button) { - new_info += &format!("Clicked by {:?}\n", button); - } - if response.double_clicked_by(button) { - new_info += &format!("Double-clicked by {:?}\n", button); - } - if response.dragged() && ui.input().pointer.button_down(button) { - new_info += &format!("Dragged by {:?}\n", button); - } - } - if !new_info.is_empty() { - self.info = new_info; - } - - ui.label(&self.info); - } -} diff --git a/egui_demo_lib/src/apps/demo/layout_test.rs b/egui_demo_lib/src/apps/demo/layout_test.rs index 5dd569f1..808da7f3 100644 --- a/egui_demo_lib/src/apps/demo/layout_test.rs +++ b/egui_demo_lib/src/apps/demo/layout_test.rs @@ -29,7 +29,7 @@ impl Default for LayoutTest { impl super::Demo for LayoutTest { fn name(&self) -> &str { - "🗺 Layout Test" + "Layout Test" } fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { diff --git a/egui_demo_lib/src/apps/demo/mod.rs b/egui_demo_lib/src/apps/demo/mod.rs index b46b61da..b0a90a74 100644 --- a/egui_demo_lib/src/apps/demo/mod.rs +++ b/egui_demo_lib/src/apps/demo/mod.rs @@ -12,7 +12,6 @@ pub mod drag_and_drop; pub mod font_book; pub mod font_contents_emoji; pub mod font_contents_ubuntu; -pub mod input_test; pub mod layout_test; pub mod painting; pub mod scrolling; diff --git a/egui_demo_lib/src/apps/demo/tests.rs b/egui_demo_lib/src/apps/demo/tests.rs index 2287a7b8..2be2b010 100644 --- a/egui_demo_lib/src/apps/demo/tests.rs +++ b/egui_demo_lib/src/apps/demo/tests.rs @@ -3,7 +3,7 @@ pub struct IdTest {} impl super::Demo for IdTest { fn name(&self) -> &str { - "📋 ID Test" + "ID Test" } fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { @@ -49,3 +49,135 @@ impl super::View for IdTest { }); } } + +// ---------------------------------------------------------------------------- + +pub struct TableTest { + num_cols: usize, + num_rows: usize, + min_col_width: f32, + max_col_width: f32, +} + +impl Default for TableTest { + fn default() -> Self { + Self { + num_cols: 4, + num_rows: 4, + min_col_width: 10.0, + max_col_width: 200.0, + } + } +} + +impl super::Demo for TableTest { + fn name(&self) -> &str { + "Table Test" + } + + fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { + egui::Window::new(self.name()).open(open).show(ctx, |ui| { + use super::View; + self.ui(ui); + }); + } +} + +impl super::View for TableTest { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.add( + egui::Slider::f32(&mut self.min_col_width, 0.0..=400.0).text("Minimum column width"), + ); + ui.add( + egui::Slider::f32(&mut self.max_col_width, 0.0..=400.0).text("Maximum column width"), + ); + ui.add(egui::Slider::usize(&mut self.num_cols, 0..=5).text("Columns")); + ui.add(egui::Slider::usize(&mut self.num_rows, 0..=20).text("Rows")); + + ui.separator(); + + let words = [ + "random", "words", "in", "a", "random", "order", "that", "just", "keeps", "going", + "with", "some", "more", + ]; + + egui::Grid::new("my_grid") + .striped(true) + .min_col_width(self.min_col_width) + .max_col_width(self.max_col_width) + .show(ui, |ui| { + for row in 0..self.num_rows { + for col in 0..self.num_cols { + if col == 0 { + ui.label(format!("row {}", row)); + } else { + let word_idx = row * 3 + col * 5; + let word_count = (row * 5 + col * 75) % 13; + let mut string = String::new(); + for word in words.iter().cycle().skip(word_idx).take(word_count) { + string += word; + string += " "; + } + ui.label(string); + } + } + ui.end_row(); + } + }); + } +} + +// ---------------------------------------------------------------------------- + +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +#[derive(Default)] +pub struct InputTest { + info: String, +} + +impl super::Demo for InputTest { + fn name(&self) -> &str { + "Input Test" + } + + fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(false) + .show(ctx, |ui| { + use super::View; + self.ui(ui); + }); + } +} + +impl super::View for InputTest { + fn ui(&mut self, ui: &mut egui::Ui) { + let response = ui.add( + egui::Button::new("Click, double-click or drag me with any mouse button") + .sense(egui::Sense::click_and_drag()), + ); + + let mut new_info = String::new(); + for &button in &[ + egui::PointerButton::Primary, + egui::PointerButton::Secondary, + egui::PointerButton::Middle, + ] { + if response.clicked_by(button) { + new_info += &format!("Clicked by {:?}\n", button); + } + if response.double_clicked_by(button) { + new_info += &format!("Double-clicked by {:?}\n", button); + } + if response.dragged() && ui.input().pointer.button_down(button) { + new_info += &format!("Dragged by {:?}\n", button); + } + } + if !new_info.is_empty() { + self.info = new_info; + } + + ui.label(&self.info); + } +}