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

Scrollable tab bar #9

Merged
merged 35 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2827675
feat: scrolling ui line
bennjii May 23, 2023
856d437
feat: scrolling with chevrons + click to focus
bennjii May 23, 2023
277b716
mod: cleanup
bennjii May 23, 2023
3655053
mod: cargo fmt
bennjii May 24, 2023
0808cd4
mod: checks
bennjii May 24, 2023
ffeb56f
feat: hide/show icons based on scroll type [right not positioned]
bennjii Jun 1, 2023
26c163f
wip: split filled layout
bennjii Jun 2, 2023
1eba301
wip: prev w/ clippy
bennjii Jun 2, 2023
67e904d
mod: working version
bennjii Jun 2, 2023
c7d1064
mod: without slide-in
bennjii Jun 2, 2023
bee4b33
mod: constantification~
bennjii Jun 2, 2023
c41b7d2
feat: right hand icon dissapear
bennjii Jun 2, 2023
ccd4885
mod: clippy
bennjii Jun 2, 2023
badf4dc
mod: late formatting changes :)
bennjii Jun 14, 2023
a594e59
Merge branch 'main' into scroll-indicators
emilk Jul 6, 2023
1a54688
mod: fixed ui right
bennjii Jul 6, 2023
d859103
mod: smoother right button release
bennjii Jul 6, 2023
0dec9d5
mod: cleanup
bennjii Jul 6, 2023
d0941f1
Fix a couple of clippy linta
emilk Nov 10, 2023
4ac86e3
Replace `unwrap` with `unwrap_or_default`
emilk Nov 10, 2023
7757f5e
Simplify ScrollState retrieval
emilk Nov 10, 2023
50357e2
Break out some code
emilk Nov 10, 2023
a5be57c
Misc cleanup
emilk Nov 10, 2023
791740e
Simplify: remove offset_delta
emilk Nov 10, 2023
17a3a9f
Better naming
emilk Nov 10, 2023
c3f5527
Some better naming
emilk Nov 10, 2023
9e65eaa
Better naming
emilk Nov 10, 2023
e80a794
Combine the scroll sizes
emilk Nov 10, 2023
68abe0c
Simplify the scroll logic
emilk Nov 10, 2023
7315c2d
Single-dimension scrolling
emilk Nov 10, 2023
20c2a9d
Animate the scrolling when you click the buttons
emilk Nov 10, 2023
dab6b6d
Simpler and better layout calculations
emilk Nov 10, 2023
0a5a405
Merge branch 'main' into scroll-indicators
emilk Nov 10, 2023
5d82552
Fix typo
emilk Nov 10, 2023
60315f2
Code cleanup
emilk Nov 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
bennjii marked this conversation as resolved.
Show resolved Hide resolved
}
}

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);
// }
bennjii marked this conversation as resolved.
Show resolved Hide resolved

if ui.button("⏵").clicked() {
// Integer value to move scroll by
// +'ve is right
// -'ve is left
bennjii marked this conversation as resolved.
Show resolved Hide resolved
*_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 @@ -108,12 +108,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,
bennjii marked this conversation as resolved.
Show resolved Hide resolved
) {
// 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 @@ -116,7 +116,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
222 changes: 178 additions & 44 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,
bennjii marked this conversation as resolved.
Show resolved Hide resolved
}

impl Tabs {
pub fn new(children: Vec<TileId>) -> Self {
let active = children.first().copied();
Expand Down Expand Up @@ -96,6 +107,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 @@ -108,60 +132,170 @@ 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.egui_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() {
if !tree.is_visible(child_id) {
continue;
}
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;
}

if scroll_state.offset.x > LEFT_FRAME_SIZE {
if !scroll_state.prev_frame_left {
scroll_state.offset_delta.x += LEFT_FRAME_SIZE;
}

let is_being_dragged = is_being_dragged(ui.ctx(), child_id);
scroll_state.prev_frame_left = true;

let selected = self.is_active(child_id);
let id = child_id.egui_id();
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;
}

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);
}
// 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,
);
}
bennjii marked this conversation as resolved.
Show resolved Hide resolved

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.egui_id()));
}
}

for (i, &child_id) in self.children.iter().enumerate() {
if !tree.is_visible(child_id) {
continue;
}

let is_being_dragged = is_being_dragged(ui.ctx(), child_id);

let selected = self.is_active(child_id);
let id = child_id.egui_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);
}

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