Skip to content

Commit a8f3018

Browse files
emilkclaude
andauthored
Keep the pane type out of the layout pass (#141)
Pure refactor, extracted from #139. No behavior change. The layout pass never looks at a pane's contents. All it needs from `Behavior` are a few numbers, so this gathers them up front into a `LayoutContext`: ```rust pub(crate) struct LayoutContext<'a> { pub gap_width: f32, pub tab_bar_height: f32, pub grid_auto_column_count: &'a dyn Fn(usize, Rect, f32) -> usize, pub tab_auto_selected: &'a Cell<bool>, } ``` Two consequences: * Every layout signature loses both its `style` and `behavior` arguments — `Tiles::layout_tile` goes from 5 parameters to 3. * The layout code no longer mentions `Pane` at all, so it can lay out a `Tiles<T>` for any `T`, not only the one the `Behavior` was written for. #139 needs exactly that. ### Drive-by fix `Tabs::layout` called `Behavior::on_edit(TabSelected)` from deep inside the layout recursion. It now reports the auto-selection up through the `LayoutContext` and the single caller emits the event. Same event, same conditions — but laying out a tree is no longer a way to trigger user-visible callbacks, which matters once anything wants to lay out a tree speculatively. ### Breaking ⚠️ `Linear::layout` is no longer `pub`, matching the other containers. It took `&mut Tiles` and a `&mut dyn Behavior`, so it was not callable in practice. ### Testing `cargo fmt --check`, `cargo clippy --all-features --all-targets` and `cargo test --all-features` all clean. No new tests: this is intended to be behavior-preserving, and the existing `test_grid_with_chaos_monkey` exercises the layout paths. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ef65447 commit a8f3018

8 files changed

Lines changed: 93 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# `egui_tiles` Changelog
22

33

4+
## Unreleased
5+
6+
* ⚠️ `Linear::layout` is no longer `pub`. It took `&mut Tiles` and a `&mut dyn Behavior`, so it could not be called usefully from outside the crate.
7+
8+
49
## 0.16.0 - 2026-06-26
510
Full diff at https://github.com/rerun-io/egui_tiles/compare/0.15.0..HEAD
611

src/behavior.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,57 @@ pub struct TabState {
3535
pub closable: bool,
3636
}
3737

38+
/// Everything the layout pass needs from a [`Behavior`], with the pane type erased.
39+
///
40+
/// The layout pass never looks at a pane's contents — it only needs a handful of numbers.
41+
/// Gathering them up front keeps `Pane` out of the layout signatures entirely, which lets the
42+
/// same code lay out any [`Tiles`], whatever it happens to store in its panes.
43+
pub(crate) struct LayoutContext<'a> {
44+
pub gap_width: f32,
45+
pub tab_bar_height: f32,
46+
pub grid_auto_column_count: &'a dyn Fn(usize, Rect, f32) -> usize,
47+
48+
/// Set by the layout pass if it had to pick an active tab for a [`crate::Tabs`] container.
49+
///
50+
/// Reported back to the caller rather than straight to [`Behavior::on_edit`]: laying out
51+
/// the tree is not the place to be emitting user-visible edit events from.
52+
pub tab_auto_selected: &'a std::cell::Cell<bool>,
53+
}
54+
55+
/// Lay out `tiles` starting at `root`, using only the pane-agnostic parts of `behavior`.
56+
///
57+
/// Generic over the pane type of `tiles`, which need not be the pane type `behavior` is for.
58+
///
59+
/// Returns `true` if the pass had to auto-select an active tab, in which case the caller
60+
/// should report [`EditAction::TabSelected`].
61+
pub(crate) fn layout_tiles<Pane, TilesPane>(
62+
tiles: &mut Tiles<TilesPane>,
63+
root: Option<TileId>,
64+
behavior: &dyn Behavior<Pane>,
65+
style: &egui::Style,
66+
rect: Rect,
67+
) -> bool {
68+
let Some(root) = root else {
69+
return false;
70+
};
71+
72+
let grid_auto_column_count = |num_visible_children, rect, gap| {
73+
behavior.grid_auto_column_count(num_visible_children, rect, gap)
74+
};
75+
let tab_auto_selected = std::cell::Cell::new(false);
76+
77+
let layout = LayoutContext {
78+
gap_width: behavior.gap_width(style),
79+
tab_bar_height: behavior.tab_bar_height(style),
80+
grid_auto_column_count: &grid_auto_column_count,
81+
tab_auto_selected: &tab_auto_selected,
82+
};
83+
84+
tiles.layout_tile(&layout, rect, root);
85+
86+
tab_auto_selected.get()
87+
}
88+
3889
/// Trait defining how the [`super::Tree`] and its panes should be shown.
3990
pub trait Behavior<Pane> {
4091
/// Show a pane tile in the given [`egui::Ui`].

src/container/grid.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use egui::{
55
};
66
use itertools::Itertools as _;
77

8-
use crate::behavior::EditAction;
8+
use crate::behavior::{EditAction, LayoutContext};
99
use crate::{
1010
Behavior, ContainerInsertion, DropContext, InsertionPoint, ResizeState, SimplifyAction, TileId,
1111
Tiles, Tree,
@@ -137,16 +137,15 @@ impl Grid {
137137
pub(super) fn layout<Pane>(
138138
&mut self,
139139
tiles: &mut Tiles<Pane>,
140-
style: &egui::Style,
141-
behavior: &mut dyn Behavior<Pane>,
140+
layout: &LayoutContext<'_>,
142141
rect: Rect,
143142
) {
144143
// clean up any empty holes at the end
145144
while self.children.last() == Some(&None) {
146145
self.children.pop();
147146
}
148147

149-
let gap = behavior.gap_width(style);
148+
let gap = layout.gap_width;
150149

151150
let visible_children_and_holes = self.visible_children_and_holes(tiles);
152151

@@ -156,7 +155,7 @@ impl Grid {
156155

157156
let num_cols = match self.layout {
158157
GridLayout::Auto => {
159-
behavior.grid_auto_column_count(num_visible_children, rect, gap)
158+
(layout.grid_auto_column_count)(num_visible_children, rect, gap)
160159
}
161160
GridLayout::Columns(num_columns) => num_columns,
162161
};
@@ -222,7 +221,7 @@ impl Grid {
222221
let col = i % num_cols;
223222
let row = i / num_cols;
224223
let child_rect = Rect::from_x_y_ranges(self.col_ranges[col], self.row_ranges[row]);
225-
tiles.layout_tile(style, behavior, child_rect, child);
224+
tiles.layout_tile(layout, child_rect, child);
226225
}
227226
}
228227

@@ -571,15 +570,15 @@ mod tests {
571570
};
572571

573572
let style = egui::Style::default();
574-
let mut behavior = TestBehavior {};
573+
let behavior = TestBehavior {};
575574
let area = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(1024.0, 768.0));
576575

577576
// Go crazy on it to make sure we never crash:
578577
let mut rng = Pcg64::new_seed(123_456_789_012);
579578

580579
for _ in 0..1000 {
581580
let root = tree.root.unwrap();
582-
tree.tiles.layout_tile(&style, &mut behavior, area, root);
581+
crate::behavior::layout_tiles(&mut tree.tiles, Some(root), &behavior, &style, area);
583582

584583
// Add some tiles:
585584
for _ in 0..rng.rand_u64() % 3 {

src/container/linear.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use egui::{NumExt as _, Rect, emath::GuiRounding as _, pos2, vec2};
44
use itertools::Itertools as _;
55

6-
use crate::behavior::EditAction;
6+
use crate::behavior::{EditAction, LayoutContext};
77
use crate::{
88
Behavior, ContainerInsertion, DropContext, InsertionPoint, ResizeState, SimplifyAction, TileId,
99
Tiles, Tree, is_being_dragged,
@@ -148,11 +148,10 @@ impl Linear {
148148
self.children.push(child);
149149
}
150150

151-
pub fn layout<Pane>(
151+
pub(super) fn layout<Pane>(
152152
&mut self,
153153
tiles: &mut Tiles<Pane>,
154-
style: &egui::Style,
155-
behavior: &mut dyn Behavior<Pane>,
154+
layout: &LayoutContext<'_>,
156155
rect: Rect,
157156
) {
158157
// GC:
@@ -161,23 +160,22 @@ impl Linear {
161160

162161
match self.dir {
163162
LinearDir::Horizontal => {
164-
self.layout_horizontal(tiles, style, behavior, rect);
163+
self.layout_horizontal(tiles, layout, rect);
165164
}
166-
LinearDir::Vertical => self.layout_vertical(tiles, style, behavior, rect),
165+
LinearDir::Vertical => self.layout_vertical(tiles, layout, rect),
167166
}
168167
}
169168

170169
fn layout_horizontal<Pane>(
171170
&self,
172171
tiles: &mut Tiles<Pane>,
173-
style: &egui::Style,
174-
behavior: &mut dyn Behavior<Pane>,
172+
layout: &LayoutContext<'_>,
175173
rect: Rect,
176174
) {
177175
let visible_children = self.visible_children(tiles);
178176

179177
let num_gaps = visible_children.len().saturating_sub(1);
180-
let gap_width = behavior.gap_width(style);
178+
let gap_width = layout.gap_width;
181179
let total_gap_width = gap_width * num_gaps as f32;
182180
let available_width = (rect.width() - total_gap_width).at_least(0.0);
183181

@@ -186,22 +184,21 @@ impl Linear {
186184
let mut x = rect.min.x;
187185
for (child, width) in visible_children.iter().zip(widths) {
188186
let child_rect = Rect::from_min_size(pos2(x, rect.min.y), vec2(width, rect.height()));
189-
tiles.layout_tile(style, behavior, child_rect, *child);
187+
tiles.layout_tile(layout, child_rect, *child);
190188
x += width + gap_width;
191189
}
192190
}
193191

194192
fn layout_vertical<Pane>(
195193
&self,
196194
tiles: &mut Tiles<Pane>,
197-
style: &egui::Style,
198-
behavior: &mut dyn Behavior<Pane>,
195+
layout: &LayoutContext<'_>,
199196
rect: Rect,
200197
) {
201198
let visible_children = self.visible_children(tiles);
202199

203200
let num_gaps = visible_children.len().saturating_sub(1);
204-
let gap_height = behavior.gap_width(style);
201+
let gap_height = layout.gap_width;
205202
let total_gap_height = gap_height * num_gaps as f32;
206203
let available_height = (rect.height() - total_gap_height).at_least(0.0);
207204

@@ -210,7 +207,7 @@ impl Linear {
210207
let mut y = rect.min.y;
211208
for (child, height) in visible_children.iter().zip(heights) {
212209
let child_rect = Rect::from_min_size(pos2(rect.min.x, y), vec2(rect.width(), height));
213-
tiles.layout_tile(style, behavior, child_rect, *child);
210+
tiles.layout_tile(layout, child_rect, *child);
214211
y += height + gap_height;
215212
}
216213
}

src/container/mod.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use egui::Rect;
33
use crate::Tree;
44

55
use super::{Behavior, DropContext, SimplifyAction, TileId, Tiles};
6+
use crate::behavior::LayoutContext;
67

78
mod grid;
89
mod linear;
@@ -223,20 +224,19 @@ impl Container {
223224
pub(super) fn layout<Pane>(
224225
&mut self,
225226
tiles: &mut Tiles<Pane>,
226-
style: &egui::Style,
227-
behavior: &mut dyn Behavior<Pane>,
227+
layout: &LayoutContext<'_>,
228228
rect: Rect,
229229
) {
230230
if self.is_empty() {
231231
return;
232232
}
233233

234234
match self {
235-
Self::Tabs(tabs) => tabs.layout(tiles, style, behavior, rect),
235+
Self::Tabs(tabs) => tabs.layout(tiles, layout, rect),
236236
Self::Linear(linear) => {
237-
linear.layout(tiles, style, behavior, rect);
237+
linear.layout(tiles, layout, rect);
238238
}
239-
Self::Grid(grid) => grid.layout(tiles, style, behavior, rect),
239+
Self::Grid(grid) => grid.layout(tiles, layout, rect),
240240
}
241241
}
242242

src/container/tabs.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use egui::{NumExt as _, Rect, Vec2, scroll_area::ScrollBarVisibility, vec2};
22

3-
use crate::behavior::{EditAction, TabState};
3+
use crate::behavior::{EditAction, LayoutContext, TabState};
44
use crate::{
55
Behavior, ContainerInsertion, DropContext, InsertionPoint, SimplifyAction, TileId, Tiles, Tree,
66
is_being_dragged,
@@ -163,22 +163,21 @@ impl Tabs {
163163
pub(super) fn layout<Pane>(
164164
&mut self,
165165
tiles: &mut Tiles<Pane>,
166-
style: &egui::Style,
167-
behavior: &mut dyn Behavior<Pane>,
166+
layout: &LayoutContext<'_>,
168167
rect: Rect,
169168
) {
170169
let prev_active = self.active;
171170
self.ensure_active(tiles);
172171
if prev_active != self.active {
173-
behavior.on_edit(EditAction::TabSelected);
172+
layout.tab_auto_selected.set(true);
174173
}
175174

176175
let mut active_rect = rect;
177-
active_rect.min.y += behavior.tab_bar_height(style);
176+
active_rect.min.y += layout.tab_bar_height;
178177

179178
if let Some(active) = self.active {
180179
// Only lay out the active tab (saves CPU):
181-
tiles.layout_tile(style, behavior, active_rect, active);
180+
tiles.layout_tile(layout, active_rect, active);
182181
}
183182
}
184183

src/tiles.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use egui::{Pos2, Rect};
22

3+
use crate::behavior::LayoutContext;
4+
35
use super::{
46
Behavior, Container, ContainerInsertion, ContainerKind, GcAction, Grid, InsertionPoint, Linear,
57
LinearDir, SimplificationOptions, SimplifyAction, Tabs, Tile, TileId,
@@ -411,21 +413,15 @@ impl<Pane> Tiles<Pane> {
411413
GcAction::Keep
412414
}
413415

414-
pub(super) fn layout_tile(
415-
&mut self,
416-
style: &egui::Style,
417-
behavior: &mut dyn Behavior<Pane>,
418-
rect: Rect,
419-
tile_id: TileId,
420-
) {
416+
pub(super) fn layout_tile(&mut self, layout: &LayoutContext<'_>, rect: Rect, tile_id: TileId) {
421417
let Some(mut tile) = self.tiles.remove(&tile_id) else {
422418
log::debug!("Failed to find tile {tile_id:?} during layout");
423419
return;
424420
};
425421
self.rects.insert(tile_id, rect);
426422

427423
if let Tile::Container(container) = &mut tile {
428-
container.layout(self, style, behavior, rect);
424+
container.layout(self, layout, rect);
429425
}
430426

431427
self.tiles.insert(tile_id, tile);

src/tree.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use egui::{NumExt as _, Rect, Ui};
22

3-
use crate::behavior::EditAction;
3+
use crate::behavior::{EditAction, layout_tiles};
44
use crate::{ContainerInsertion, ContainerKind, UiResponse};
55

66
use super::{
@@ -328,9 +328,11 @@ impl<Pane> Tree<Pane> {
328328
if self.width.is_finite() {
329329
rect.set_width(self.width);
330330
}
331-
if let Some(root) = self.root {
332-
self.tiles.layout_tile(ui.style(), behavior, rect, root);
331+
if layout_tiles(&mut self.tiles, self.root, behavior, ui.style(), rect) {
332+
behavior.on_edit(EditAction::TabSelected);
333+
}
333334

335+
if let Some(root) = self.root {
334336
self.tile_ui(behavior, &mut drop_context, ui, root);
335337
}
336338

0 commit comments

Comments
 (0)