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
This commit is contained in:
Lucas Meurer 2025-07-03 14:31:35 +02:00 committed by GitHub
parent 77df407f50
commit 2b62c68598
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 308 additions and 40 deletions

View File

@ -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<f32>,
spacing: Option<f32>,
kind: SidesKind,
wrap_mode: Option<crate::TextWrapMode>,
}
#[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<RetL, RetR>(
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;
if ui.is_sizing_pass() {
kind = SidesKind::Extend;
wrap_mode = None;
}
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)),
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 = add_left(&mut left_ui);
left_ui.min_rect()
};
let right_rect = {
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 mut right_ui = ui.new_child(
UiBuilder::new()
.max_rect(right_max_rect)
.layout(Layout::right_to_left(Align::Center)),
let (right_rect, result_right) = Self::create_ui(
ui,
right_max_rect,
Layout::right_to_left(Align::Center),
add_right,
None,
);
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);
}
ui.advance_cursor_after_rect(final_rect);
(result_left, result_right)
}
}
}
fn create_ui<Ret>(
ui: &mut Ui,
max_rect: emath::Rect,
layout: Layout,
add_content: impl FnOnce(&mut Ui) -> Ret,
wrap_mode: Option<crate::TextWrapMode>,
) -> (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)
}
}

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b7ceaa95512c67dcbf1c8ba5a8f33bf4833c2e863d09903fb71b5aa2822cc086
size 7889

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442
size 8552

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f
size 1647

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f
size 1276

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88e1557dffa7295e7e7e37ed175fcec40aab939f9b67137a1ce33811e8ae4722
size 7148

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442
size 8552

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f
size 1647

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f
size 1276

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:508209ca303751ef323301b25bb3878410742ea79339b75363d2681b98d2712b
size 7068

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442
size 8552

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f
size 1647

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f
size 1276

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a0c9e39c18fc5bb1fc02a86dbf02e3ffca5537dbe8986d5c5b50cb4984c97466
size 9085

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442
size 8552

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f
size 1647

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f
size 1276

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a6e9ba0acb573853ef5b3dedb1156d99cdf80338ccb160093960e8aaa41bd5df
size 9048

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:931af33f4548924b3bb75a2e513b9e689bce94436b10a9f811140eb11e9d6442
size 8552

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6448d44c1c9bed08cd6a4af39b141f3a4ca203ca5f7b967cbc0e0d0c2b4fb73f
size 1647

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:44622002ebe287208d26359a06804b1f8737f86eb482322bdf3881a1fe53941f
size 1276

View File

@ -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}")));
}
}