use egui::{NumExt as _, Rect, Vec2, scroll_area::ScrollBarVisibility, vec2};
use crate::behavior::{EditAction, TabState};
use crate::{
Behavior, ContainerInsertion, DropContext, InsertionPoint, SimplifyAction, TileId, Tiles, Tree,
is_being_dragged,
};
const SCROLL_ARROW_SIZE: Vec2 = Vec2::splat(20.0);
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Tabs {
pub children: Vec<TileId>,
pub active: Option<TileId>,
}
#[derive(Clone, Copy, Debug, Default)]
struct ScrollState {
pub offset: f32,
pub offset_debt: f32,
pub content_size: Vec2,
pub available: Vec2,
pub show_left_arrow: bool,
pub show_right_arrow: bool,
pub showed_left_arrow_prev: bool,
pub showed_right_arrow_prev: bool,
}
impl ScrollState {
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;
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();
Self { children, active }
}
pub fn add_child(&mut self, child: TileId) {
self.children.push(child);
}
pub fn set_active(&mut self, child: TileId) {
self.active = Some(child);
}
pub fn is_active(&self, child: TileId) -> bool {
Some(child) == self.active
}
pub(super) fn layout<Pane>(
&mut self,
tiles: &mut Tiles<Pane>,
style: &egui::Style,
behavior: &mut dyn Behavior<Pane>,
rect: Rect,
) {
let prev_active = self.active;
self.ensure_active(tiles);
if prev_active != self.active {
behavior.on_edit(EditAction::TabSelected);
}
let mut active_rect = rect;
active_rect.min.y += behavior.tab_bar_height(style);
if let Some(active) = self.active {
tiles.layout_tile(style, behavior, active_rect, active);
}
}
pub fn ensure_active<Pane>(&mut self, tiles: &Tiles<Pane>) {
if let Some(active) = self.active {
if !tiles.is_visible(active) {
self.active = None;
}
}
if !self.children.iter().any(|&child| self.is_active(child)) {
self.active = self
.children
.iter()
.copied()
.find(|&child_id| tiles.is_visible(child_id));
}
}
pub(super) fn ui<Pane>(
&mut self,
tree: &mut Tree<Pane>,
behavior: &mut dyn Behavior<Pane>,
drop_context: &mut DropContext,
ui: &mut egui::Ui,
rect: Rect,
tile_id: TileId,
) {
let next_active = self.tab_bar_ui(tree, behavior, ui, rect, drop_context, tile_id);
if let Some(active) = self.active {
tree.tile_ui(behavior, drop_context, ui, active);
crate::cover_tile_if_dragged(tree, behavior, ui, active);
}
self.active = next_active;
}
#[allow(clippy::too_many_lines)]
fn tab_bar_ui<Pane>(
&self,
tree: &mut Tree<Pane>,
behavior: &mut dyn Behavior<Pane>,
ui: &mut egui::Ui,
rect: Rect,
drop_context: &mut DropContext,
tile_id: TileId,
) -> Option<TileId> {
let mut next_active = self.active;
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.new_child(egui::UiBuilder::new().max_rect(tab_bar_rect));
let mut button_rects = ahash::HashMap::default();
let mut dragged_index = None;
ui.painter()
.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| {
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()
});
behavior.top_bar_right_ui(&tree.tiles, ui, tile_id, self, &mut scroll_state.offset);
let scroll_area_width = scroll_state.update(ui);
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);
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) {
let sense = egui::Sense::click_and_drag();
if ui
.interact(ui.max_rect(), ui.id().with("background"), sense)
.on_hover_cursor(egui::CursorIcon::Grab)
.drag_started()
{
behavior.on_edit(EditAction::TileDragged);
ui.ctx().set_dragged_id(tile_id.egui_id(tree.id));
}
}
ui.spacing_mut().item_spacing.x = 0.0;
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(), tree.id, child_id);
let selected = self.is_active(child_id);
let id = child_id.egui_id(tree.id);
let tab_state = TabState {
active: selected,
is_being_dragged,
closable: behavior.is_tab_closable(&tree.tiles, child_id),
};
let response =
behavior.tab_ui(&mut tree.tiles, ui, id, child_id, &tab_state);
if response.clicked() {
behavior.on_edit(EditAction::TabSelected);
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)
{
behavior.on_edit(EditAction::TabSelected);
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()
.data_mut(|data| data.insert_temp(scroll_state_id, scroll_state));
});
let preview_thickness = 6.0;
let after_rect = |rect: Rect| {
let dragged_size = if let Some(dragged_index) = dragged_index {
button_rects[&self.children[dragged_index]].size()
} else {
rect.size() };
Rect::from_min_size(
rect.right_top() + vec2(ui.spacing().item_spacing.x, 0.0),
dragged_size,
)
};
super::linear::drop_zones(
preview_thickness,
&self.children,
dragged_index,
super::LinearDir::Horizontal,
|tile_id| button_rects.get(&tile_id).copied(),
|rect, i| {
drop_context.suggest_rect(
InsertionPoint::new(tile_id, ContainerInsertion::Tabs(i)),
rect,
);
},
after_rect,
);
next_active
}
pub(super) fn simplify_children(&mut self, mut simplify: impl FnMut(TileId) -> SimplifyAction) {
self.children.retain_mut(|child| match simplify(*child) {
SimplifyAction::Remove => false,
SimplifyAction::Keep => true,
SimplifyAction::Replace(new) => {
if self.active == Some(*child) {
self.active = Some(new);
}
*child = new;
true
}
});
}
pub(crate) fn remove_child(&mut self, needle: TileId) -> Option<usize> {
let index = self.children.iter().position(|&child| child == needle)?;
self.children.remove(index);
Some(index)
}
}