Skip to main content

egui_extras/
layout.rs

1use egui::{Id, Pos2, Rect, Response, Sense, Ui, UiBuilder, emath::GuiRounding as _};
2
3#[derive(Clone, Copy)]
4pub(crate) enum CellSize {
5    /// Absolute size in points
6    Absolute(f32),
7
8    /// Take all available space
9    Remainder,
10}
11
12/// Cells are positioned in two dimensions, cells go in one direction and form lines.
13///
14/// In a strip there's only one line which goes in the direction of the strip:
15///
16/// In a horizontal strip, a [`StripLayout`] with horizontal [`CellDirection`] is used.
17/// Its cells go from left to right inside this [`StripLayout`].
18///
19/// In a table there's a [`StripLayout`] for each table row with a horizontal [`CellDirection`].
20/// Its cells go from left to right. And the lines go from top to bottom.
21pub(crate) enum CellDirection {
22    /// Cells go from left to right.
23    Horizontal,
24
25    /// Cells go from top to bottom.
26    Vertical,
27}
28
29/// Flags used by [`StripLayout::add`].
30#[derive(Clone, Copy, Default)]
31pub(crate) struct StripLayoutFlags {
32    pub(crate) clip: bool,
33    pub(crate) striped: bool,
34    pub(crate) hovered: bool,
35    pub(crate) selected: bool,
36    pub(crate) overline: bool,
37
38    /// Used when we want to accurately measure the size of this cell.
39    pub(crate) sizing_pass: bool,
40}
41
42/// Positions cells in [`CellDirection`] and starts a new line on [`StripLayout::end_line`]
43pub struct StripLayout<'l> {
44    pub(crate) ui: &'l mut Ui,
45    direction: CellDirection,
46    pub(crate) rect: Rect,
47    pub(crate) cursor: Pos2,
48
49    /// Keeps track of the max used position,
50    /// so we know how much space we used.
51    max: Pos2,
52
53    cell_layout: egui::Layout,
54    sense: Sense,
55}
56
57impl<'l> StripLayout<'l> {
58    pub(crate) fn new(
59        ui: &'l mut Ui,
60        direction: CellDirection,
61        cell_layout: egui::Layout,
62        sense: Sense,
63    ) -> Self {
64        let rect = ui.available_rect_before_wrap();
65        let pos = rect.left_top();
66
67        Self {
68            ui,
69            direction,
70            rect,
71            cursor: pos,
72            max: pos,
73            cell_layout,
74            sense,
75        }
76    }
77
78    fn cell_rect(&self, width: &CellSize, height: &CellSize) -> Rect {
79        Rect {
80            min: self.cursor,
81            max: Pos2 {
82                x: match width {
83                    CellSize::Absolute(width) => self.cursor.x + width,
84                    CellSize::Remainder => self.rect.right(),
85                },
86                y: match height {
87                    CellSize::Absolute(height) => self.cursor.y + height,
88                    CellSize::Remainder => self.rect.bottom(),
89                },
90            },
91        }
92    }
93
94    fn set_pos(&mut self, rect: Rect) {
95        self.max.x = self.max.x.max(rect.right());
96        self.max.y = self.max.y.max(rect.bottom());
97
98        match self.direction {
99            CellDirection::Horizontal => {
100                self.cursor.x = rect.right() + self.ui.spacing().item_spacing.x;
101            }
102            CellDirection::Vertical => {
103                self.cursor.y = rect.bottom() + self.ui.spacing().item_spacing.y;
104            }
105        }
106    }
107
108    pub(crate) fn empty(&mut self, width: CellSize, height: CellSize) {
109        self.set_pos(self.cell_rect(&width, &height));
110    }
111
112    /// This is the innermost part of [`crate::Table`] and [`crate::Strip`].
113    ///
114    /// Return the used space (`min_rect`) plus the [`Response`] of the whole cell.
115    pub(crate) fn add(
116        &mut self,
117        flags: StripLayoutFlags,
118        width: CellSize,
119        height: CellSize,
120        child_ui_id_salt: Id,
121        add_cell_contents: impl FnOnce(&mut Ui),
122    ) -> (Rect, Response) {
123        let max_rect = self.cell_rect(&width, &height);
124
125        // Make sure we don't have a gap in the stripe/frame/selection background:
126        let item_spacing = self.ui.spacing().item_spacing;
127        let gapless_rect = max_rect.expand2(0.5 * item_spacing).round_ui();
128
129        if flags.striped {
130            self.ui.painter().rect_filled(
131                gapless_rect,
132                egui::CornerRadius::ZERO,
133                self.ui.visuals().faint_bg_color,
134            );
135        }
136
137        if flags.selected {
138            self.ui.painter().rect_filled(
139                gapless_rect,
140                egui::CornerRadius::ZERO,
141                self.ui.visuals().selection.bg_fill,
142            );
143        }
144
145        if flags.hovered && !flags.selected && self.sense.interactive() {
146            self.ui.painter().rect_filled(
147                gapless_rect,
148                egui::CornerRadius::ZERO,
149                self.ui.visuals().widgets.hovered.bg_fill,
150            );
151        }
152
153        let mut child_ui = self.cell(flags, max_rect, child_ui_id_salt, add_cell_contents);
154
155        let used_rect = child_ui.min_rect();
156
157        // Make sure we catch clicks etc on the _whole_ cell:
158        child_ui.set_min_size(max_rect.size());
159
160        let allocation_rect = if self.ui.is_sizing_pass() {
161            used_rect
162        } else if flags.clip {
163            max_rect
164        } else {
165            max_rect | used_rect
166        };
167
168        self.set_pos(allocation_rect);
169
170        self.ui.advance_cursor_after_rect(allocation_rect);
171
172        let response = child_ui.response();
173
174        (used_rect, response)
175    }
176
177    /// only needed for layouts with multiple lines, like [`Table`](crate::Table).
178    pub fn end_line(&mut self) {
179        match self.direction {
180            CellDirection::Horizontal => {
181                self.cursor.y = self.max.y + self.ui.spacing().item_spacing.y;
182                self.cursor.x = self.rect.left();
183            }
184            CellDirection::Vertical => {
185                self.cursor.x = self.max.x + self.ui.spacing().item_spacing.x;
186                self.cursor.y = self.rect.top();
187            }
188        }
189    }
190
191    /// Skip a lot of space.
192    pub(crate) fn skip_space(&mut self, delta: egui::Vec2) {
193        let before = self.cursor;
194        self.cursor += delta;
195        let rect = Rect::from_two_pos(before, self.cursor);
196        self.ui.allocate_rect(rect, Sense::hover());
197    }
198
199    /// Return the Ui to which the contents where added
200    fn cell(
201        &mut self,
202        flags: StripLayoutFlags,
203        max_rect: Rect,
204        child_ui_id_salt: egui::Id,
205        add_cell_contents: impl FnOnce(&mut Ui),
206    ) -> Ui {
207        let mut ui_builder = UiBuilder::new()
208            .id_salt(child_ui_id_salt)
209            .ui_stack_info(egui::UiStackInfo::new(egui::UiKind::TableCell))
210            .max_rect(max_rect)
211            .layout(self.cell_layout)
212            .sense(self.sense);
213        if flags.sizing_pass {
214            ui_builder = ui_builder.sizing_pass();
215        }
216
217        let mut child_ui = self.ui.new_child(ui_builder);
218
219        if flags.clip {
220            let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin);
221            let margin = margin.min(0.5 * self.ui.spacing().item_spacing);
222            let clip_rect = max_rect.expand2(margin);
223            child_ui.shrink_clip_rect(clip_rect);
224
225            if !child_ui.is_sizing_pass() {
226                // Better to truncate (if we can), rather than hard clipping:
227                child_ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
228            }
229        }
230
231        if flags.selected {
232            let stroke_color = child_ui.style().visuals.selection.stroke.color;
233            child_ui.style_mut().visuals.override_text_color = Some(stroke_color);
234        }
235
236        if flags.overline {
237            child_ui.painter().hline(
238                max_rect.x_range(),
239                max_rect.top(),
240                child_ui.visuals().widgets.noninteractive.bg_stroke,
241            );
242        }
243
244        add_cell_contents(&mut child_ui);
245
246        child_ui
247    }
248
249    /// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size
250    pub fn allocate_rect(&mut self) -> Response {
251        let mut rect = self.rect;
252        rect.set_right(self.max.x);
253        rect.set_bottom(self.max.y);
254
255        self.ui.allocate_rect(rect, Sense::hover())
256    }
257}