From 2b62c68598f3ecfb5c464ff6758db5eda4d474df Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 3 Jul 2025 14:31:35 +0200 Subject: [PATCH] Add `egui::Sides` `shrink_left` / `shrink_right` (#7295) This allows contents (on one of the sides) in egui::Sides to shrink. * related https://github.com/rerun-io/rerun/issues/10494 --- crates/egui/src/containers/sides.rs | 212 ++++++++++++++---- .../tests/snapshots/sides/default_long.png | 3 + .../sides/default_long_fit_contents.png | 3 + .../tests/snapshots/sides/default_short.png | 3 + .../sides/default_short_fit_contents.png | 3 + .../snapshots/sides/shrink_left_long.png | 3 + .../sides/shrink_left_long_fit_contents.png | 3 + .../snapshots/sides/shrink_left_short.png | 3 + .../sides/shrink_left_short_fit_contents.png | 3 + .../snapshots/sides/shrink_right_long.png | 3 + .../sides/shrink_right_long_fit_contents.png | 3 + .../snapshots/sides/shrink_right_short.png | 3 + .../sides/shrink_right_short_fit_contents.png | 3 + .../tests/snapshots/sides/wrap_left_long.png | 3 + .../sides/wrap_left_long_fit_contents.png | 3 + .../tests/snapshots/sides/wrap_left_short.png | 3 + .../sides/wrap_left_short_fit_contents.png | 3 + .../tests/snapshots/sides/wrap_right_long.png | 3 + .../sides/wrap_right_long_fit_contents.png | 3 + .../snapshots/sides/wrap_right_short.png | 3 + .../sides/wrap_right_short_fit_contents.png | 3 + tests/egui_tests/tests/test_sides.rs | 76 +++++++ 22 files changed, 308 insertions(+), 40 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/sides/default_long.png create mode 100644 tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png create mode 100644 tests/egui_tests/tests/snapshots/sides/default_short.png create mode 100644 tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png create mode 100644 tests/egui_tests/tests/snapshots/sides/shrink_left_long.png create mode 100644 tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png create mode 100644 tests/egui_tests/tests/snapshots/sides/shrink_left_short.png create mode 100644 tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png create mode 100644 tests/egui_tests/tests/snapshots/sides/shrink_right_long.png create mode 100644 tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png create mode 100644 tests/egui_tests/tests/snapshots/sides/shrink_right_short.png create mode 100644 tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png create mode 100644 tests/egui_tests/tests/snapshots/sides/wrap_left_long.png create mode 100644 tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png create mode 100644 tests/egui_tests/tests/snapshots/sides/wrap_left_short.png create mode 100644 tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png create mode 100644 tests/egui_tests/tests/snapshots/sides/wrap_right_long.png create mode 100644 tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png create mode 100644 tests/egui_tests/tests/snapshots/sides/wrap_right_short.png create mode 100644 tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png create mode 100644 tests/egui_tests/tests/test_sides.rs diff --git a/crates/egui/src/containers/sides.rs b/crates/egui/src/containers/sides.rs index e34ae70e..8a67c6c5 100644 --- a/crates/egui/src/containers/sides.rs +++ b/crates/egui/src/containers/sides.rs @@ -1,4 +1,4 @@ -use emath::Align; +use emath::{Align, NumExt as _}; use crate::{Layout, Ui, UiBuilder}; @@ -20,8 +20,13 @@ use crate::{Layout, Ui, UiBuilder}; /// /// If the parent is not wide enough to fit all widgets, the parent will be expanded to the right. /// -/// The left widgets are first added to the ui, left-to-right. -/// Then the right widgets are added, right-to-left. +/// The left widgets are added left-to-right. +/// The right widgets are added right-to-left. +/// +/// Which side is first depends on the configuration: +/// - [`Sides::extend`] - left widgets are added first +/// - [`Sides::shrink_left`] - right widgets are added first +/// - [`Sides::shrink_right`] - left widgets are added first /// /// ``` /// # egui::__run_test_ui(|ui| { @@ -40,6 +45,16 @@ use crate::{Layout, Ui, UiBuilder}; pub struct Sides { height: Option, spacing: Option, + kind: SidesKind, + wrap_mode: Option, +} + +#[derive(Clone, Copy, Debug, Default)] +enum SidesKind { + #[default] + Extend, + ShrinkLeft, + ShrinkRight, } impl Sides { @@ -68,58 +83,175 @@ impl Sides { self } + /// Try to shrink widgets on the left side. + /// + /// Right widgets will be added first. The left [`Ui`]s max rect will be limited to the + /// remaining space. + #[inline] + pub fn shrink_left(mut self) -> Self { + self.kind = SidesKind::ShrinkLeft; + self + } + + /// Try to shrink widgets on the right side. + /// + /// Left widgets will be added first. The right [`Ui`]s max rect will be limited to the + /// remaining space. + #[inline] + pub fn shrink_right(mut self) -> Self { + self.kind = SidesKind::ShrinkRight; + self + } + + /// Extend the left and right sides to fill the available space. + /// + /// This is the default behavior. + /// The left widgets will be added first, followed by the right widgets. + #[inline] + pub fn extend(mut self) -> Self { + self.kind = SidesKind::Extend; + self + } + + /// The text wrap mode for the shrinking side. + /// + /// Does nothing if [`Self::extend`] is used (the default). + #[inline] + pub fn wrap_mode(mut self, wrap_mode: crate::TextWrapMode) -> Self { + self.wrap_mode = Some(wrap_mode); + self + } + + /// Truncate the text on the shrinking side. + /// + /// This is a shortcut for [`Self::wrap_mode`]. + /// Does nothing if [`Self::extend`] is used (the default). + #[inline] + pub fn truncate(mut self) -> Self { + self.wrap_mode = Some(crate::TextWrapMode::Truncate); + self + } + + /// Wrap the text on the shrinking side. + /// + /// This is a shortcut for [`Self::wrap_mode`]. + /// Does nothing if [`Self::extend`] is used (the default). + #[inline] + pub fn wrap(mut self) -> Self { + self.wrap_mode = Some(crate::TextWrapMode::Wrap); + self + } + pub fn show( self, ui: &mut Ui, add_left: impl FnOnce(&mut Ui) -> RetL, add_right: impl FnOnce(&mut Ui) -> RetR, ) -> (RetL, RetR) { - let Self { height, spacing } = self; + let Self { + height, + spacing, + mut kind, + mut wrap_mode, + } = self; let height = height.unwrap_or_else(|| ui.spacing().interact_size.y); let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing.x); let mut top_rect = ui.available_rect_before_wrap(); top_rect.max.y = top_rect.min.y + height; - let result_left; - let result_right; - - let left_rect = { - let left_max_rect = top_rect; - let mut left_ui = ui.new_child( - UiBuilder::new() - .max_rect(left_max_rect) - .layout(Layout::left_to_right(Align::Center)), - ); - result_left = add_left(&mut left_ui); - left_ui.min_rect() - }; - - let right_rect = { - let right_max_rect = top_rect.with_min_x(left_rect.max.x); - let mut right_ui = ui.new_child( - UiBuilder::new() - .max_rect(right_max_rect) - .layout(Layout::right_to_left(Align::Center)), - ); - result_right = add_right(&mut right_ui); - right_ui.min_rect() - }; - - let mut final_rect = left_rect.union(right_rect); - let min_width = left_rect.width() + spacing + right_rect.width(); - if ui.is_sizing_pass() { - // Make as small as possible: - final_rect.max.x = left_rect.min.x + min_width; - } else { - // If the rects overlap, make sure we expand the allocated rect so that the parent - // ui knows we overflowed, and resizes: - final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width); + kind = SidesKind::Extend; + wrap_mode = None; } - ui.advance_cursor_after_rect(final_rect); + match kind { + SidesKind::ShrinkLeft => { + let (right_rect, result_right) = Self::create_ui( + ui, + top_rect, + Layout::right_to_left(Align::Center), + add_right, + None, + ); + let available_width = top_rect.width() - right_rect.width() - spacing; + let left_rect_constraint = + top_rect.with_max_x(top_rect.min.x + available_width.at_least(0.0)); + let (left_rect, result_left) = Self::create_ui( + ui, + left_rect_constraint, + Layout::left_to_right(Align::Center), + add_left, + wrap_mode, + ); - (result_left, result_right) + ui.advance_cursor_after_rect(left_rect.union(right_rect)); + (result_left, result_right) + } + SidesKind::ShrinkRight => { + let (left_rect, result_left) = Self::create_ui( + ui, + top_rect, + Layout::left_to_right(Align::Center), + add_left, + None, + ); + let right_rect_constraint = top_rect.with_min_x(left_rect.max.x + spacing); + let (right_rect, result_right) = Self::create_ui( + ui, + right_rect_constraint, + Layout::right_to_left(Align::Center), + add_right, + wrap_mode, + ); + + ui.advance_cursor_after_rect(left_rect.union(right_rect)); + (result_left, result_right) + } + SidesKind::Extend => { + let (left_rect, result_left) = Self::create_ui( + ui, + top_rect, + Layout::left_to_right(Align::Center), + add_left, + None, + ); + let right_max_rect = top_rect.with_min_x(left_rect.max.x); + let (right_rect, result_right) = Self::create_ui( + ui, + right_max_rect, + Layout::right_to_left(Align::Center), + add_right, + None, + ); + + let mut final_rect = left_rect.union(right_rect); + let min_width = left_rect.width() + spacing + right_rect.width(); + + if ui.is_sizing_pass() { + final_rect.max.x = left_rect.min.x + min_width; + } else { + final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width); + } + + ui.advance_cursor_after_rect(final_rect); + (result_left, result_right) + } + } + } + + fn create_ui( + ui: &mut Ui, + max_rect: emath::Rect, + layout: Layout, + add_content: impl FnOnce(&mut Ui) -> Ret, + wrap_mode: Option, + ) -> (emath::Rect, Ret) { + let mut child_ui = ui.new_child(UiBuilder::new().max_rect(max_rect).layout(layout)); + if let Some(wrap_mode) = wrap_mode { + child_ui.style_mut().wrap_mode = Some(wrap_mode); + } + let result = add_content(&mut child_ui); + (child_ui.min_rect(), result) } } diff --git a/tests/egui_tests/tests/snapshots/sides/default_long.png b/tests/egui_tests/tests/snapshots/sides/default_long.png new file mode 100644 index 00000000..2d66f366 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7ceaa95512c67dcbf1c8ba5a8f33bf4833c2e863d09903fb71b5aa2822cc086 +size 7889 diff --git a/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png new file mode 100644 index 00000000..15e9dc46 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/default_short.png b/tests/egui_tests/tests/snapshots/sides/default_short.png new file mode 100644 index 00000000..756a3068 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png new file mode 100644 index 00000000..6f318926 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/default_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png new file mode 100644 index 00000000..39e1bab9 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88e1557dffa7295e7e7e37ed175fcec40aab939f9b67137a1ce33811e8ae4722 +size 7148 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png new file mode 100644 index 00000000..15e9dc46 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_short.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_short.png new file mode 100644 index 00000000..756a3068 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png new file mode 100644 index 00000000..6f318926 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png new file mode 100644 index 00000000..3326d952 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:508209ca303751ef323301b25bb3878410742ea79339b75363d2681b98d2712b +size 7068 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png new file mode 100644 index 00000000..15e9dc46 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_short.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_short.png new file mode 100644 index 00000000..756a3068 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png new file mode 100644 index 00000000..6f318926 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png new file mode 100644 index 00000000..36929a41 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0c9e39c18fc5bb1fc02a86dbf02e3ffca5537dbe8986d5c5b50cb4984c97466 +size 9085 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png new file mode 100644 index 00000000..15e9dc46 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_short.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_short.png new file mode 100644 index 00000000..756a3068 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png new file mode 100644 index 00000000..6f318926 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png new file mode 100644 index 00000000..47398293 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6e9ba0acb573853ef5b3dedb1156d99cdf80338ccb160093960e8aaa41bd5df +size 9048 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png new file mode 100644 index 00000000..15e9dc46 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442 +size 8552 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_short.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_short.png new file mode 100644 index 00000000..756a3068 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_short.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f +size 1647 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png new file mode 100644 index 00000000..6f318926 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_short_fit_contents.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f +size 1276 diff --git a/tests/egui_tests/tests/test_sides.rs b/tests/egui_tests/tests/test_sides.rs new file mode 100644 index 00000000..293abd31 --- /dev/null +++ b/tests/egui_tests/tests/test_sides.rs @@ -0,0 +1,76 @@ +use egui::{TextWrapMode, Vec2, containers::Sides}; +use egui_kittest::{Harness, SnapshotResults}; + +#[test] +fn sides_container_tests() { + let mut results = SnapshotResults::new(); + + test_variants("default", |sides| sides, &mut results); + + test_variants( + "shrink_left", + |sides| sides.shrink_left().truncate(), + &mut results, + ); + + test_variants( + "shrink_right", + |sides| sides.shrink_right().truncate(), + &mut results, + ); + + test_variants( + "wrap_left", + |sides| sides.shrink_left().wrap_mode(TextWrapMode::Wrap), + &mut results, + ); + + test_variants( + "wrap_right", + |sides| sides.shrink_right().wrap_mode(TextWrapMode::Wrap), + &mut results, + ); +} + +fn test_variants( + name: &str, + mut create_sides: impl FnMut(Sides) -> Sides, + results: &mut SnapshotResults, +) { + for (variant_name, left_text, right_text, fit_contents) in [ + ("short", "Left", "Right", false), + ( + "long", + "Very long left content that should not fit.", + "Very long right text that should also not fit.", + false, + ), + ("short_fit_contents", "Left", "Right", true), + ( + "long_fit_contents", + "Very long left content that should not fit.", + "Very long right text that should also not fit.", + true, + ), + ] { + let mut harness = Harness::builder() + .with_size(Vec2::new(400.0, 50.0)) + .build_ui(|ui| { + create_sides(Sides::new()).show( + ui, + |left| { + left.label(left_text); + }, + |right| { + right.label(right_text); + }, + ); + }); + + if fit_contents { + harness.fit_contents(); + } + + results.add(harness.try_snapshot(&format!("sides/{name}_{variant_name}"))); + } +}