diff --git a/examples/advanced.rs b/examples/advanced.rs index c2285dd..9427419 100644 --- a/examples/advanced.rs +++ b/examples/advanced.rs @@ -128,15 +128,38 @@ impl egui_tiles::Behavior for TreeBehavior { format!("View {}", view.nr).into() } - fn top_bar_rtl_ui( + fn top_bar_left_ui( &mut self, _tiles: &egui_tiles::Tiles, ui: &mut egui::Ui, - tile_id: egui_tiles::TileId, + _tile_id: egui_tiles::TileId, + _tabs: &egui_tiles::Tabs, + _offset: f32, + _scroll: &mut f32, + ) { + if ui.button("⏴").clicked() { + *_scroll += -45.0; + } + } + + fn top_bar_right_ui( + &mut self, + _tiles: &egui_tiles::Tiles, + ui: &mut egui::Ui, + _tile_id: egui_tiles::TileId, _tabs: &egui_tiles::Tabs, + _offset: f32, + _scroll: &mut f32, ) { - if ui.button("➕").clicked() { - self.add_child_to = Some(tile_id); + // if ui.button("➕").clicked() { + // self.add_child_to = Some(tile_id); + // } + + if ui.button("⏵").clicked() { + // Integer value to move scroll by + // +'ve is right + // -'ve is left + *_scroll += 45.0; } } diff --git a/src/behavior.rs b/src/behavior.rs index 68662de..4c53e27 100644 --- a/src/behavior.rs +++ b/src/behavior.rs @@ -107,12 +107,27 @@ pub trait Behavior { /// You can use this to, for instance, add a button for adding new tabs. /// /// The widgets will be added right-to-left. - fn top_bar_rtl_ui( + fn top_bar_right_ui( &mut self, _tiles: &Tiles, _ui: &mut Ui, _tile_id: TileId, _tabs: &crate::Tabs, + _offset: f32, + _scroll: &mut f32, + ) { + // if ui.button("➕").clicked() { + // } + } + + fn top_bar_left_ui( + &mut self, + _tiles: &Tiles, + _ui: &mut Ui, + _tile_id: TileId, + _tabs: &crate::Tabs, + _offset: f32, + _scroll: &mut f32, ) { // if ui.button("➕").clicked() { // } diff --git a/src/container/linear.rs b/src/container/linear.rs index c609c54..3c3bfb3 100644 --- a/src/container/linear.rs +++ b/src/container/linear.rs @@ -110,7 +110,7 @@ impl Linear { /// /// The `fraction` is the fraction of the total width that the first child should get. pub fn new_binary(dir: LinearDir, children: [TileId; 2], fraction: f32) -> Self { - debug_assert!(0.0 <= fraction && fraction <= 1.0); + debug_assert!((0.0..=1.0).contains(&fraction)); let mut slf = Self { children: children.into(), dir, diff --git a/src/container/tabs.rs b/src/container/tabs.rs index 04adc66..37fcd3c 100644 --- a/src/container/tabs.rs +++ b/src/container/tabs.rs @@ -1,4 +1,4 @@ -use egui::{vec2, Rect}; +use egui::{scroll_area::ScrollBarVisibility, vec2, Rect, Vec2}; use crate::{ is_being_dragged, Behavior, ContainerInsertion, DropContext, InsertionPoint, SimplifyAction, @@ -15,6 +15,17 @@ pub struct Tabs { pub active: Option, } +#[derive(Default, Clone)] +struct ScrollState { + pub offset: Vec2, + pub consumed: Vec2, + pub available: Vec2, + pub offset_delta: Vec2, + + pub prev_frame_left: bool, + pub prev_frame_right: bool, +} + impl Tabs { pub fn new(children: Vec) -> Self { let active = children.first().copied(); @@ -86,6 +97,19 @@ impl Tabs { ) -> Option { let mut next_active = self.active; + let scroll_state: ScrollState = ScrollState { + prev_frame_left: false, + prev_frame_right: false, + ..ScrollState::default() + }; + let id = ui.make_persistent_id(tile_id); + + ui.ctx().memory_mut(|m| { + if m.data.get_temp::(id).is_none() { + m.data.insert_temp(id, scroll_state) + } + }); + let tab_bar_height = behavior.tab_bar_height(ui.style()); let tab_bar_rect = rect.split_top_bottom_at_y(rect.top() + tab_bar_height).0; let mut ui = ui.child_ui(tab_bar_rect, *ui.layout()); @@ -98,56 +122,167 @@ impl Tabs { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { // Add buttons such as "add new tab" - behavior.top_bar_rtl_ui(&tree.tiles, ui, tile_id, self); - ui.spacing_mut().item_spacing.x = 0.0; // Tabs have spacing built-in - ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| { - ui.set_clip_rect(ui.max_rect()); // Don't cover the `rtl_ui` buttons. - - if !tree.is_root(tile_id) { - // Make the background behind the buttons draggable (to drag the parent container tile): - if ui - .interact( - ui.max_rect(), - ui.id().with("background"), - egui::Sense::drag(), - ) - .on_hover_cursor(egui::CursorIcon::Grab) - .drag_started() - { - ui.memory_mut(|mem| mem.set_dragged_id(tile_id.id())); - } + let mut scroll_state: ScrollState = ui + .ctx() + .memory_mut(|m| m.data.get_temp::(id)) + .unwrap(); + + const LEFT_FRAME_SIZE: f32 = 20.0; + const RIGHT_FRAME_SIZE: f32 = 20.0; + + let mut consume = ui.available_width(); + + if (scroll_state.offset.x - RIGHT_FRAME_SIZE) > scroll_state.available.x { + if scroll_state.prev_frame_right { + scroll_state.offset_delta.x += RIGHT_FRAME_SIZE; } - for (i, &child_id) in self.children.iter().enumerate() { - let is_being_dragged = is_being_dragged(ui.ctx(), child_id); + scroll_state.prev_frame_right = false; + } else if (scroll_state.offset.x - 0.0) > scroll_state.available.x { + // DO NOTHING + } else { + scroll_state.prev_frame_right = true; + } - let selected = self.is_active(child_id); - let id = child_id.id(); + if scroll_state.offset.x > LEFT_FRAME_SIZE { + if !scroll_state.prev_frame_left { + scroll_state.offset_delta.x += LEFT_FRAME_SIZE; + } - let response = - behavior.tab_ui(&tree.tiles, ui, id, child_id, selected, is_being_dragged); - let response = response.on_hover_cursor(egui::CursorIcon::Grab); - if response.clicked() { - next_active = Some(child_id); - } + scroll_state.prev_frame_left = true; + + consume -= LEFT_FRAME_SIZE; + } else if scroll_state.offset.x > 0.0 { + if scroll_state.prev_frame_left { + scroll_state.offset.x -= LEFT_FRAME_SIZE; + } + + // Uncomment the following for an ~animated~ reveal. + // consume -= scroll_state.offset.x; + } else { + scroll_state.prev_frame_left = false; + } + + if scroll_state.consumed.x > scroll_state.available.x + && (scroll_state.offset.x - RIGHT_FRAME_SIZE) < scroll_state.available.x + { + consume -= RIGHT_FRAME_SIZE; + + behavior.top_bar_right_ui( + &tree.tiles, + ui, + tile_id, + self, + scroll_state.offset.x, + &mut scroll_state.offset_delta.x, + ); + } + + ui.set_clip_rect(ui.available_rect_before_wrap()); // Don't cover the `rtl_ui` buttons. - if let Some(mouse_pos) = drop_context.mouse_pos { - if drop_context.dragged_tile_id.is_some() - && response.rect.contains(mouse_pos) - { - // Expand this tab - maybe the user wants to drop something into it! - next_active = Some(child_id); + let mut scroll_area_size = Vec2::ZERO; + scroll_area_size.x = consume; + scroll_area_size.y = ui.available_height(); + + ui.allocate_ui_with_layout( + scroll_area_size, + egui::Layout::left_to_right(egui::Align::Center), + |ui| { + let mut area = egui::ScrollArea::horizontal() + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_width(consume); + + { + // Max is: [`ui.available_width()`] + if scroll_state.offset_delta.x >= ui.available_width() { + scroll_state.offset_delta.x = ui.available_width(); } - } - button_rects.insert(child_id, response.rect); - if is_being_dragged { - dragged_index = Some(i); + area = area.to_owned().horizontal_scroll_offset( + scroll_state.offset.x + scroll_state.offset_delta.x, + ); + + // Reset delta after use + scroll_state.offset_delta = Vec2::ZERO; } - } - }); + + let output = area.show_viewport(ui, |ui, _| { + ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| { + if !tree.is_root(tile_id) { + // Make the background behind the buttons draggable (to drag the parent container tile): + if ui + .interact( + ui.max_rect(), + ui.id().with("background"), + egui::Sense::drag(), + ) + .on_hover_cursor(egui::CursorIcon::Grab) + .drag_started() + { + ui.memory_mut(|mem| mem.set_dragged_id(tile_id.id())); + } + } + + for (i, &child_id) in self.children.iter().enumerate() { + let is_being_dragged = is_being_dragged(ui.ctx(), child_id); + + let selected = self.is_active(child_id); + let id = child_id.id(); + + let response = behavior.tab_ui( + &tree.tiles, + ui, + id, + child_id, + selected, + is_being_dragged, + ); + let response = response.on_hover_cursor(egui::CursorIcon::Grab); + if response.clicked() { + next_active = Some(child_id); + response.scroll_to_me(None) + } + + if let Some(mouse_pos) = drop_context.mouse_pos { + if drop_context.dragged_tile_id.is_some() + && response.rect.contains(mouse_pos) + { + // Expand this tab - maybe the user wants to drop something into it! + next_active = Some(child_id); + } + } + + button_rects.insert(child_id, response.rect); + if is_being_dragged { + dragged_index = Some(i); + } + } + }); + }); + + scroll_state.offset = output.state.offset; + scroll_state.consumed = output.content_size; + scroll_state.available = output.inner_rect.size(); + }, + ); + + if scroll_state.offset.x > LEFT_FRAME_SIZE { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + behavior.top_bar_left_ui( + &tree.tiles, + ui, + tile_id, + self, + scroll_state.offset.x, + &mut scroll_state.offset_delta.x, + ); + }); + } + + ui.ctx() + .memory_mut(|m| m.data.insert_temp(id, scroll_state)); }); // -----------