egui_extras/
layout.rs

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