egui/containers/
sides.rs

1use emath::{Align, NumExt as _};
2
3use crate::{Layout, Ui, UiBuilder};
4
5/// Put some widgets on the left and right sides of a ui.
6///
7/// The result will look like this:
8/// ```text
9///                        parent Ui
10///  ______________________________________________________
11/// |                    |           |                     |  ^
12/// | -> left widgets -> |    gap    | <- right widgets <- |  | height
13/// |____________________|           |_____________________|  v
14/// |                                                      |
15/// |                                                      |
16/// ```
17///
18/// The width of the gap is dynamic, based on the max width of the parent [`Ui`].
19/// When the parent is being auto-sized ([`Ui::is_sizing_pass`]) the gap will be as small as possible.
20///
21/// If the parent is not wide enough to fit all widgets, the parent will be expanded to the right.
22///
23/// The left widgets are added left-to-right.
24/// The right widgets are added right-to-left.
25///
26/// Which side is first depends on the configuration:
27///  - [`Sides::extend`] - left widgets are added first
28///  - [`Sides::shrink_left`] - right widgets are added first
29///  - [`Sides::shrink_right`] - left widgets are added first
30///
31/// ```
32/// # egui::__run_test_ui(|ui| {
33/// egui::containers::Sides::new().show(ui,
34///     |ui| {
35///         ui.label("Left");
36///     },
37///     |ui| {
38///         ui.label("Right");
39///     }
40/// );
41/// # });
42/// ```
43#[must_use = "You should call sides.show()"]
44#[derive(Clone, Copy, Debug, Default)]
45pub struct Sides {
46    height: Option<f32>,
47    spacing: Option<f32>,
48    kind: SidesKind,
49    wrap_mode: Option<crate::TextWrapMode>,
50}
51
52#[derive(Clone, Copy, Debug, Default)]
53enum SidesKind {
54    #[default]
55    Extend,
56    ShrinkLeft,
57    ShrinkRight,
58}
59
60impl Sides {
61    #[inline]
62    pub fn new() -> Self {
63        Default::default()
64    }
65
66    /// The minimum height of the sides.
67    ///
68    /// The content will be centered vertically within this height.
69    /// The default height is [`crate::Spacing::interact_size`]`.y`.
70    #[inline]
71    pub fn height(mut self, height: f32) -> Self {
72        self.height = Some(height);
73        self
74    }
75
76    /// The horizontal spacing between the left and right UIs.
77    ///
78    /// This is the minimum gap.
79    /// The default is [`crate::Spacing::item_spacing`]`.x`.
80    #[inline]
81    pub fn spacing(mut self, spacing: f32) -> Self {
82        self.spacing = Some(spacing);
83        self
84    }
85
86    /// Try to shrink widgets on the left side.
87    ///
88    /// Right widgets will be added first. The left [`Ui`]s max rect will be limited to the
89    /// remaining space.
90    #[inline]
91    pub fn shrink_left(mut self) -> Self {
92        self.kind = SidesKind::ShrinkLeft;
93        self
94    }
95
96    /// Try to shrink widgets on the right side.
97    ///
98    /// Left widgets will be added first. The right [`Ui`]s max rect will be limited to the
99    /// remaining space.
100    #[inline]
101    pub fn shrink_right(mut self) -> Self {
102        self.kind = SidesKind::ShrinkRight;
103        self
104    }
105
106    /// Extend the left and right sides to fill the available space.
107    ///
108    /// This is the default behavior.
109    /// The left widgets will be added first, followed by the right widgets.
110    #[inline]
111    pub fn extend(mut self) -> Self {
112        self.kind = SidesKind::Extend;
113        self
114    }
115
116    /// The text wrap mode for the shrinking side.
117    ///
118    /// Does nothing if [`Self::extend`] is used (the default).
119    #[inline]
120    pub fn wrap_mode(mut self, wrap_mode: crate::TextWrapMode) -> Self {
121        self.wrap_mode = Some(wrap_mode);
122        self
123    }
124
125    /// Truncate the text on the shrinking side.
126    ///
127    /// This is a shortcut for [`Self::wrap_mode`].
128    /// Does nothing if [`Self::extend`] is used (the default).
129    #[inline]
130    pub fn truncate(mut self) -> Self {
131        self.wrap_mode = Some(crate::TextWrapMode::Truncate);
132        self
133    }
134
135    /// Wrap the text on the shrinking side.
136    ///
137    /// This is a shortcut for [`Self::wrap_mode`].
138    /// Does nothing if [`Self::extend`] is used (the default).
139    #[inline]
140    pub fn wrap(mut self) -> Self {
141        self.wrap_mode = Some(crate::TextWrapMode::Wrap);
142        self
143    }
144
145    pub fn show<RetL, RetR>(
146        self,
147        ui: &mut Ui,
148        add_left: impl FnOnce(&mut Ui) -> RetL,
149        add_right: impl FnOnce(&mut Ui) -> RetR,
150    ) -> (RetL, RetR) {
151        let Self {
152            height,
153            spacing,
154            mut kind,
155            mut wrap_mode,
156        } = self;
157        let height = height.unwrap_or_else(|| ui.spacing().interact_size.y);
158        let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing.x);
159
160        let mut top_rect = ui.available_rect_before_wrap();
161        top_rect.max.y = top_rect.min.y + height;
162
163        if ui.is_sizing_pass() {
164            kind = SidesKind::Extend;
165            wrap_mode = None;
166        }
167
168        match kind {
169            SidesKind::ShrinkLeft => {
170                let (right_rect, result_right) = Self::create_ui(
171                    ui,
172                    top_rect,
173                    Layout::right_to_left(Align::Center),
174                    add_right,
175                    None,
176                );
177                let available_width = top_rect.width() - right_rect.width() - spacing;
178                let left_rect_constraint =
179                    top_rect.with_max_x(top_rect.min.x + available_width.at_least(0.0));
180                let (left_rect, result_left) = Self::create_ui(
181                    ui,
182                    left_rect_constraint,
183                    Layout::left_to_right(Align::Center),
184                    add_left,
185                    wrap_mode,
186                );
187
188                ui.advance_cursor_after_rect(left_rect | right_rect);
189                (result_left, result_right)
190            }
191            SidesKind::ShrinkRight => {
192                let (left_rect, result_left) = Self::create_ui(
193                    ui,
194                    top_rect,
195                    Layout::left_to_right(Align::Center),
196                    add_left,
197                    None,
198                );
199                let right_rect_constraint = top_rect.with_min_x(left_rect.max.x + spacing);
200                let (right_rect, result_right) = Self::create_ui(
201                    ui,
202                    right_rect_constraint,
203                    Layout::right_to_left(Align::Center),
204                    add_right,
205                    wrap_mode,
206                );
207
208                ui.advance_cursor_after_rect(left_rect | right_rect);
209                (result_left, result_right)
210            }
211            SidesKind::Extend => {
212                let (left_rect, result_left) = Self::create_ui(
213                    ui,
214                    top_rect,
215                    Layout::left_to_right(Align::Center),
216                    add_left,
217                    None,
218                );
219                let right_max_rect = top_rect.with_min_x(left_rect.max.x);
220                let (right_rect, result_right) = Self::create_ui(
221                    ui,
222                    right_max_rect,
223                    Layout::right_to_left(Align::Center),
224                    add_right,
225                    None,
226                );
227
228                let mut final_rect = left_rect | right_rect;
229                let min_width = left_rect.width() + spacing + right_rect.width();
230
231                if ui.is_sizing_pass() {
232                    final_rect.max.x = left_rect.min.x + min_width;
233                } else {
234                    final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width);
235                }
236
237                ui.advance_cursor_after_rect(final_rect);
238                (result_left, result_right)
239            }
240        }
241    }
242
243    fn create_ui<Ret>(
244        ui: &mut Ui,
245        max_rect: emath::Rect,
246        layout: Layout,
247        add_content: impl FnOnce(&mut Ui) -> Ret,
248        wrap_mode: Option<crate::TextWrapMode>,
249    ) -> (emath::Rect, Ret) {
250        let mut child_ui = ui.new_child(UiBuilder::new().max_rect(max_rect).layout(layout));
251        if let Some(wrap_mode) = wrap_mode {
252            child_ui.style_mut().wrap_mode = Some(wrap_mode);
253        }
254        let result = add_content(&mut child_ui);
255        (child_ui.min_rect(), result)
256    }
257}