egui_extras/
layout.rs
1use egui::{Id, Pos2, Rect, Response, Sense, Ui, UiBuilder};
2
3#[derive(Clone, Copy)]
4pub(crate) enum CellSize {
5 Absolute(f32),
7
8 Remainder,
10}
11
12pub(crate) enum CellDirection {
22 Horizontal,
24
25 Vertical,
27}
28
29#[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 pub(crate) sizing_pass: bool,
39}
40
41pub 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 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 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 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 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 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 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 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 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 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}