From b1d739ad46f2287b440e9b37f27465098ba2e07e Mon Sep 17 00:00:00 2001 From: SpruceCloud <1888686+SpruceCloud@users.noreply.github.com> Date: Mon, 11 May 2026 19:14:28 +0200 Subject: [PATCH 1/3] Tabs: Add `tab_bar_leading_ui` and `tab_bar_trailing_ui` hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new `Behavior` trait methods with no-op defaults let consumers inject custom UI into the tab bar pinned to the outer edges: - `tab_bar_leading_ui` — left of the left scroll-arrow - `tab_bar_trailing_ui` — right of `top_bar_right_ui` and the right scroll-arrow Both slots sit outside the scrollable tab strip, so they don't move when tabs are scrolled. Their consumed width is automatically subtracted from the area available to the tab strip (via `ui.available_width()` shrinking before `ScrollState::update` reads it), so tabs shrink rather than overlap. `scroll_area_width` is additionally clamped against the inner LTR ui's remaining width so the leading slot is handled the same way. `top_bar_right_ui` is unchanged — the new trailing slot lives further outside it. The `advanced` example gains a hamburger (☰) leading and a gear (⚙) trailing widget to demonstrate the pinning behavior. --- examples/advanced.rs | 20 ++++++++++++++++++++ src/behavior.rs | 38 ++++++++++++++++++++++++++++++++++++++ src/container/tabs.rs | 17 +++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/examples/advanced.rs b/examples/advanced.rs index d5cc900..2079de8 100644 --- a/examples/advanced.rs +++ b/examples/advanced.rs @@ -140,6 +140,26 @@ impl egui_tiles::Behavior for TreeBehavior { } } + fn tab_bar_leading_ui( + &mut self, + _tiles: &egui_tiles::Tiles, + ui: &mut egui::Ui, + _tile_id: egui_tiles::TileId, + _tabs: &egui_tiles::Tabs, + ) { + ui.label("☰"); + } + + fn tab_bar_trailing_ui( + &mut self, + _tiles: &egui_tiles::Tiles, + ui: &mut egui::Ui, + _tile_id: egui_tiles::TileId, + _tabs: &egui_tiles::Tabs, + ) { + let _ = ui.small_button("⚙"); + } + // --- // Settings: diff --git a/src/behavior.rs b/src/behavior.rs index 82c5a45..7eee8fd 100644 --- a/src/behavior.rs +++ b/src/behavior.rs @@ -254,6 +254,44 @@ pub trait Behavior { // } } + /// Adds custom UI to the leftmost edge of each tab bar, pinned outside the + /// scrollable tab strip and to the LEFT of the left scroll-arrow. + /// + /// Width consumed by widgets added here is automatically subtracted from + /// the area available to the scrollable tab strip — tabs shrink rather + /// than overlap. The slot does not move when tabs are scrolled. + /// + /// To match the visual height of the scroll arrows, use + /// `ui.add_sized(egui::Vec2::splat(self.tab_bar_height(...)), widget)`. + fn tab_bar_leading_ui( + &mut self, + _tiles: &Tiles, + _ui: &mut Ui, + _tile_id: TileId, + _tabs: &crate::Tabs, + ) { + } + + /// Adds custom UI to the rightmost edge of each tab bar, pinned outside + /// the scrollable tab strip and to the RIGHT of both + /// [`Self::top_bar_right_ui`] and the right scroll-arrow. + /// + /// Width consumed by widgets added here is automatically subtracted from + /// the area available to the scrollable tab strip — tabs shrink rather + /// than overlap. The slot does not move when tabs are scrolled. + /// + /// Widgets are added right-to-left (this slot lives inside the same + /// right-to-left layout that hosts [`Self::top_bar_right_ui`] and the + /// right scroll-arrow). + fn tab_bar_trailing_ui( + &mut self, + _tiles: &Tiles, + _ui: &mut Ui, + _tile_id: TileId, + _tabs: &crate::Tabs, + ) { + } + /// The height of the bar holding tab titles. fn tab_bar_height(&self, _style: &egui::Style) -> f32 { 24.0 diff --git a/src/container/tabs.rs b/src/container/tabs.rs index 580ba2c..b6bd19b 100644 --- a/src/container/tabs.rs +++ b/src/container/tabs.rs @@ -242,6 +242,13 @@ impl Tabs { .unwrap_or_default() }); + // Trailing custom slot — first call in a right-to-left layout + // means rightmost on screen, so this sits to the right of both + // `top_bar_right_ui` and the right scroll-arrow. The width it + // consumes is automatically excluded from `ui.available_width()` + // by the time `ScrollState::update` reads it below. + behavior.tab_bar_trailing_ui(&tree.tiles, ui, tile_id, self); + // Allow user to add buttons such as "add new tab". // They can also read and modify the scroll state if they want. behavior.top_bar_right_ui(&tree.tiles, ui, tile_id, self, &mut scroll_state.offset); @@ -255,8 +262,18 @@ impl Tabs { ui.available_size(), egui::Layout::left_to_right(egui::Align::Center), |ui| { + // Leading custom slot — first call in this LTR child layout + // means leftmost on screen, so it sits to the left of the + // left scroll-arrow. + behavior.tab_bar_leading_ui(&tree.tiles, ui, tile_id, self); + scroll_state.left_arrow(ui, arrow_size); + // Clamp the precomputed width so it can't exceed what's + // left after the leading slot + left arrow consumed space + // inside this LTR child ui. + let scroll_area_width = scroll_area_width.min(ui.available_width()).max(0.0); + // Prepare to show the scroll area with the tabs: scroll_state.offset = scroll_state From 71aa95e5985e4b192d7c42065c03b0826a930fee Mon Sep 17 00:00:00 2001 From: SpruceCloud <1888686+SpruceCloud@users.noreply.github.com> Date: Mon, 18 May 2026 17:25:09 +0200 Subject: [PATCH 2/3] chore: clippy fix for trailing tab-bar slot example Bare `let _ = ui.small_button(...)` tripped `clippy::let_underscore_must_use` and `clippy::let_underscore_untyped`. Renaming to `let _response` silences both while keeping the discard intent. --- examples/advanced.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/advanced.rs b/examples/advanced.rs index 2079de8..ae8d2bb 100644 --- a/examples/advanced.rs +++ b/examples/advanced.rs @@ -157,7 +157,7 @@ impl egui_tiles::Behavior for TreeBehavior { _tile_id: egui_tiles::TileId, _tabs: &egui_tiles::Tabs, ) { - let _ = ui.small_button("⚙"); + let _response = ui.small_button("⚙"); } // --- From edbb3d82b431379746533ef903dd7414652ac7cd Mon Sep 17 00:00:00 2001 From: SpruceCloud <1888686+SpruceCloud@users.noreply.github.com> Date: Fri, 26 Jun 2026 19:11:02 +0200 Subject: [PATCH 3/3] Refactor: rename tab_bar_leading_ui to tab_bar_left_ui and remove redundant tab_bar_trailing_ui --- examples/advanced.rs | 13 ++----------- src/behavior.rs | 22 +--------------------- src/container/tabs.rs | 11 ++--------- 3 files changed, 5 insertions(+), 41 deletions(-) diff --git a/examples/advanced.rs b/examples/advanced.rs index ae8d2bb..d7025c3 100644 --- a/examples/advanced.rs +++ b/examples/advanced.rs @@ -135,12 +135,13 @@ impl egui_tiles::Behavior for TreeBehavior { _tabs: &egui_tiles::Tabs, _scroll_offset: &mut f32, ) { + let _response = ui.small_button("⚙"); if ui.button("➕").clicked() { self.add_child_to = Some(tile_id); } } - fn tab_bar_leading_ui( + fn tab_bar_left_ui( &mut self, _tiles: &egui_tiles::Tiles, ui: &mut egui::Ui, @@ -150,16 +151,6 @@ impl egui_tiles::Behavior for TreeBehavior { ui.label("☰"); } - fn tab_bar_trailing_ui( - &mut self, - _tiles: &egui_tiles::Tiles, - ui: &mut egui::Ui, - _tile_id: egui_tiles::TileId, - _tabs: &egui_tiles::Tabs, - ) { - let _response = ui.small_button("⚙"); - } - // --- // Settings: diff --git a/src/behavior.rs b/src/behavior.rs index 7eee8fd..499fdaf 100644 --- a/src/behavior.rs +++ b/src/behavior.rs @@ -263,27 +263,7 @@ pub trait Behavior { /// /// To match the visual height of the scroll arrows, use /// `ui.add_sized(egui::Vec2::splat(self.tab_bar_height(...)), widget)`. - fn tab_bar_leading_ui( - &mut self, - _tiles: &Tiles, - _ui: &mut Ui, - _tile_id: TileId, - _tabs: &crate::Tabs, - ) { - } - - /// Adds custom UI to the rightmost edge of each tab bar, pinned outside - /// the scrollable tab strip and to the RIGHT of both - /// [`Self::top_bar_right_ui`] and the right scroll-arrow. - /// - /// Width consumed by widgets added here is automatically subtracted from - /// the area available to the scrollable tab strip — tabs shrink rather - /// than overlap. The slot does not move when tabs are scrolled. - /// - /// Widgets are added right-to-left (this slot lives inside the same - /// right-to-left layout that hosts [`Self::top_bar_right_ui`] and the - /// right scroll-arrow). - fn tab_bar_trailing_ui( + fn tab_bar_left_ui( &mut self, _tiles: &Tiles, _ui: &mut Ui, diff --git a/src/container/tabs.rs b/src/container/tabs.rs index b6bd19b..5b0913b 100644 --- a/src/container/tabs.rs +++ b/src/container/tabs.rs @@ -242,13 +242,6 @@ impl Tabs { .unwrap_or_default() }); - // Trailing custom slot — first call in a right-to-left layout - // means rightmost on screen, so this sits to the right of both - // `top_bar_right_ui` and the right scroll-arrow. The width it - // consumes is automatically excluded from `ui.available_width()` - // by the time `ScrollState::update` reads it below. - behavior.tab_bar_trailing_ui(&tree.tiles, ui, tile_id, self); - // Allow user to add buttons such as "add new tab". // They can also read and modify the scroll state if they want. behavior.top_bar_right_ui(&tree.tiles, ui, tile_id, self, &mut scroll_state.offset); @@ -262,10 +255,10 @@ impl Tabs { ui.available_size(), egui::Layout::left_to_right(egui::Align::Center), |ui| { - // Leading custom slot — first call in this LTR child layout + // Left custom slot — first call in this LTR child layout // means leftmost on screen, so it sits to the left of the // left scroll-arrow. - behavior.tab_bar_leading_ui(&tree.tiles, ui, tile_id, self); + behavior.tab_bar_left_ui(&tree.tiles, ui, tile_id, self); scroll_state.left_arrow(ui, arrow_size);