Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Basic Scrolling in Tab Menu #3

Closed
wants to merge 14 commits into from
31 changes: 27 additions & 4 deletions examples/advanced.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,38 @@ impl egui_tiles::Behavior<Pane> for TreeBehavior {
format!("View {}", view.nr).into()
}

fn top_bar_rtl_ui(
fn top_bar_left_ui(
&mut self,
_tiles: &egui_tiles::Tiles<Pane>,
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<Pane>,
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;
}
}

Expand Down
17 changes: 16 additions & 1 deletion src/behavior.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,27 @@ pub trait Behavior<Pane> {
/// 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<Pane>,
_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<Pane>,
_ui: &mut Ui,
_tile_id: TileId,
_tabs: &crate::Tabs,
_offset: f32,
_scroll: &mut f32,
) {
// if ui.button("➕").clicked() {
// }
Expand Down
2 changes: 1 addition & 1 deletion src/container/linear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
217 changes: 176 additions & 41 deletions src/container/tabs.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -15,6 +15,17 @@ pub struct Tabs {
pub active: Option<TileId>,
}

#[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<TileId>) -> Self {
let active = children.first().copied();
Expand Down Expand Up @@ -86,6 +97,19 @@ impl Tabs {
) -> Option<TileId> {
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::<ScrollState>(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());
Expand All @@ -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::<ScrollState>(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));
});

// -----------
Expand Down