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 all 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
3 changes: 2 additions & 1 deletion examples/advanced.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,13 @@ impl egui_tiles::Behavior<Pane> for TreeBehavior {
format!("View {}", view.nr).into()
}

fn top_bar_rtl_ui(
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,
_scroll_offset: &mut f32,
) {
if ui.button("➕").clicked() {
self.add_child_to = Some(tile_id);
Expand Down
6 changes: 5 additions & 1 deletion src/behavior.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,16 @@ 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(
///
/// `_scroll_offset` is a mutable reference to the tab scroll value.
/// Adding to this value will scroll the tabs to the right, subtracting to the left.
fn top_bar_right_ui(
&mut self,
_tiles: &Tiles<Pane>,
_ui: &mut Ui,
_tile_id: TileId,
_tabs: &crate::Tabs,
_scroll_offset: &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 @@ -121,7 +121,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
271 changes: 215 additions & 56 deletions src/container/tabs.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use egui::{vec2, Rect};
use egui::{scroll_area::ScrollBarVisibility, vec2, NumExt, Rect, Vec2};

use crate::{
is_being_dragged, Behavior, ContainerInsertion, DropContext, InsertionPoint, SimplifyAction,
TileId, Tiles, Tree,
};

/// Fixed size icons for `⏴` and `⏵`
const SCROLL_ARROW_SIZE: Vec2 = Vec2::splat(20.0);

/// A container with tabs. Only one tab is open (active) at a time.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
Expand All @@ -16,6 +19,116 @@ pub struct Tabs {
pub active: Option<TileId>,
}

/// The current tab scrolling state
#[derive(Clone, Copy, Debug, Default)]
struct ScrollState {
/// The current horizontal scroll offset.
///
/// Positive: scroll right.
/// Negatie: scroll left.
pub offset: f32,

/// Outstanding offset to apply smoothly over the next few frames.
/// This is what the buttons update.
pub offset_debt: f32,

/// The size of all the tabs last frame.
pub content_size: Vec2,

/// The available size for the tabs.
pub available: Vec2,

/// Show the left scroll-arrow this frame?
pub show_left_arrow: bool,

/// Show the right scroll-arrow this frame?
pub show_right_arrow: bool,

/// Did we show the left scroll-arrow last frame?
pub showed_left_arrow_prev: bool,

/// Did we show the right scroll-arrow last frame?
pub showed_right_arrow_prev: bool,
}

impl ScrollState {
/// Returns the space left for the tabs after the scroll arrows.
pub fn update(&mut self, ui: &egui::Ui) -> f32 {
let mut scroll_area_width = ui.available_width();

let button_and_spacing_width = SCROLL_ARROW_SIZE.x + ui.spacing().item_spacing.x;

let margin = 0.1;

self.show_left_arrow = SCROLL_ARROW_SIZE.x < self.offset;

if self.show_left_arrow {
scroll_area_width -= button_and_spacing_width;
}

self.show_right_arrow = self.offset + scroll_area_width + margin < self.content_size.x;

// Compensate for showing/hiding of arrow:
self.offset += button_and_spacing_width
* ((self.show_left_arrow as i32 as f32) - (self.showed_left_arrow_prev as i32 as f32));

if self.show_right_arrow {
scroll_area_width -= button_and_spacing_width;
}

self.showed_left_arrow_prev = self.show_left_arrow;
self.showed_right_arrow_prev = self.show_right_arrow;

if self.offset_debt != 0.0 {
const SPEED: f32 = 500.0;

let dt = ui.input(|i| i.stable_dt).min(0.1);
let max_movement = dt * SPEED;
if self.offset_debt.abs() <= max_movement {
self.offset += self.offset_debt;
self.offset_debt = 0.0;
} else {
let movement = self.offset_debt.signum() * max_movement;
self.offset += movement;
self.offset_debt -= movement;
ui.ctx().request_repaint();
}
}

scroll_area_width
}

fn scroll_increment(&self) -> f32 {
(self.available.x / 3.0).at_least(20.0)
}

pub fn left_arrow(&mut self, ui: &mut egui::Ui) {
if !self.show_left_arrow {
return;
}

if ui
.add_sized(SCROLL_ARROW_SIZE, egui::Button::new("⏴"))
.clicked()
{
self.offset_debt -= self.scroll_increment();
}
}

pub fn right_arrow(&mut self, ui: &mut egui::Ui) {
if !self.show_right_arrow {
return;
}

if ui
.add_sized(SCROLL_ARROW_SIZE, egui::Button::new("⏵"))
.clicked()
{
self.offset_debt += self.scroll_increment();
}
}
}

impl Tabs {
pub fn new(children: Vec<TileId>) -> Self {
let active = children.first().copied();
Expand Down Expand Up @@ -86,6 +199,7 @@ impl Tabs {
}

/// Returns the next active tab (e.g. the one clicked, or the current).
#[allow(clippy::too_many_lines)]
fn tab_bar_ui<Pane>(
&self,
tree: &mut Tree<Pane>,
Expand All @@ -108,64 +222,109 @@ impl Tabs {
.rect_filled(ui.max_rect(), 0.0, behavior.tab_bar_color(ui.visuals()));

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()
{
behavior.on_edit();
ui.memory_mut(|mem| mem.set_dragged_id(tile_id.egui_id()));
}
}
let scroll_state_id = ui.make_persistent_id(tile_id);
let mut scroll_state = ui.ctx().memory_mut(|m| {
m.data
.get_temp::<ScrollState>(scroll_state_id)
.unwrap_or_default()
});

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() {
behavior.on_edit();
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!
behavior.on_edit();
next_active = Some(child_id);
// 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);

let scroll_area_width = scroll_state.update(ui);

// We're in a right-to-left layout, so start with the right scroll-arrow:
scroll_state.right_arrow(ui);

ui.allocate_ui_with_layout(
ui.available_size(),
egui::Layout::left_to_right(egui::Align::Center),
|ui| {
scroll_state.left_arrow(ui);

// Prepare to show the scroll area with the tabs:

scroll_state.offset = scroll_state
.offset
.at_most(scroll_state.content_size.x - ui.available_width());
scroll_state.offset = scroll_state.offset.at_least(0.0);

let scroll_area = egui::ScrollArea::horizontal()
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.max_width(scroll_area_width)
.auto_shrink([false; 2])
.horizontal_scroll_offset(scroll_state.offset);

let output = scroll_area.show(ui, |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()
{
behavior.on_edit();
ui.memory_mut(|mem| mem.set_dragged_id(tile_id.egui_id()));
}
}
}

button_rects.insert(child_id, response.rect);
if is_being_dragged {
dragged_index = Some(i);
}
}
});
ui.spacing_mut().item_spacing.x = 0.0; // Tabs have spacing built-in

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() {
behavior.on_edit();
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!
behavior.on_edit();
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.x;
scroll_state.content_size = output.content_size;
scroll_state.available = output.inner_rect.size();
},
);

ui.ctx()
.memory_mut(|m| m.data.insert_temp(scroll_state_id, scroll_state));
});

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