wip
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m50s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m48s

This commit is contained in:
2026-01-23 15:59:19 +07:00
parent 2fa4436e2d
commit 6617c8eea3
33 changed files with 1568 additions and 1269 deletions

View File

@@ -5,17 +5,18 @@ use gpui::{
div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent,
Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
StatefulInteractiveElement, Styled, WeakEntity, Window,
StatefulInteractiveElement, Styled, WeakEntity, Window, WindowControlArea,
};
use theme::ActiveTheme;
use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT};
use ui::button::{Button, ButtonVariants as _};
use ui::popup_menu::{PopupMenu, PopupMenuExt};
use ui::tab::tab_bar::TabBar;
use ui::tab::Tab;
use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
use crate::dock::DockPlacement;
use crate::panel::{Panel, PanelView};
use crate::stack_panel::StackPanel;
use crate::tab::tab_bar::TabBar;
use crate::tab::Tab;
use crate::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
#[derive(Clone)]
@@ -64,16 +65,32 @@ impl Render for DragPanel {
pub struct TabPanel {
focus_handle: FocusHandle,
dock_area: WeakEntity<DockArea>,
/// The stock_panel can be None, if is None, that means the panels can't be split or move
stack_panel: Option<WeakEntity<StackPanel>>,
/// List of panels in the tab panel
pub(crate) panels: Vec<Arc<dyn PanelView>>,
/// Current active panel index
pub(crate) active_ix: usize,
/// If this is true, the Panel closeable will follow the active panel's closeable,
/// otherwise this TabPanel will not able to close
pub(crate) closable: bool,
/// The stock_panel can be None, if is None, that means the panels can't be split or move
stack_panel: Option<WeakEntity<StackPanel>>,
/// Scroll handle for the tab bar
tab_bar_scroll_handle: ScrollHandle,
is_zoomed: bool,
is_collapsed: bool,
/// Whether the tab panel is zoomeds
zoomed: bool,
/// Whether the tab panel is collapsed
collapsed: bool,
/// Whether window is moving
window_move: bool,
/// When drag move, will get the placement of the panel to be split
will_split_placement: Option<Placement>,
}
@@ -141,8 +158,9 @@ impl TabPanel {
active_ix: 0,
tab_bar_scroll_handle: ScrollHandle::new(),
will_split_placement: None,
is_zoomed: false,
is_collapsed: false,
zoomed: false,
collapsed: false,
window_move: false,
closable: true,
}
}
@@ -338,7 +356,7 @@ impl TabPanel {
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.is_collapsed = collapsed;
self.collapsed = collapsed;
cx.notify();
}
@@ -351,7 +369,7 @@ impl TabPanel {
return true;
}
if self.is_zoomed {
if self.zoomed {
return true;
}
@@ -407,7 +425,7 @@ impl TabPanel {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let is_zoomed = self.is_zoomed && state.zoomable;
let is_zoomed = self.zoomed && state.zoomable;
let view = cx.entity().clone();
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
let toolbar = self.toolbar_buttons(window, cx);
@@ -419,7 +437,7 @@ impl TabPanel {
.occlude()
.rounded_full()
.children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded()))
.when(self.is_zoomed, |this| {
.when(self.zoomed, |this| {
this.child(
Button::new("zoom")
.icon(IconName::Zoom)
@@ -432,8 +450,7 @@ impl TabPanel {
)
})
.when(has_toolbar, |this| {
this.bg(cx.theme().surface_background)
.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
})
.child(
Button::new("menu")
@@ -460,21 +477,115 @@ impl TabPanel {
)
}
fn render_dock_toggle_button(
&self,
placement: DockPlacement,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Button> {
if self.zoomed {
return None;
}
let dock_area = self.dock_area.upgrade()?.read(cx);
if !dock_area.toggle_button_visible {
return None;
}
if !dock_area.is_dock_collapsible(placement, cx) {
return None;
}
let view_entity_id = cx.entity().entity_id();
let toggle_button_panels = dock_area.toggle_button_panels;
// Check if current TabPanel's entity_id matches the one stored in DockArea for this placement
if !match placement {
DockPlacement::Left => {
dock_area.left_dock.is_some() && toggle_button_panels.left == Some(view_entity_id)
}
DockPlacement::Right => {
dock_area.right_dock.is_some() && toggle_button_panels.right == Some(view_entity_id)
}
DockPlacement::Bottom => {
dock_area.bottom_dock.is_some()
&& toggle_button_panels.bottom == Some(view_entity_id)
}
DockPlacement::Center => unreachable!(),
} {
return None;
}
let is_open = dock_area.is_dock_open(placement, cx);
let icon = match placement {
DockPlacement::Left => {
if is_open {
IconName::PanelLeft
} else {
IconName::PanelLeftOpen
}
}
DockPlacement::Right => {
if is_open {
IconName::PanelRight
} else {
IconName::PanelRightOpen
}
}
DockPlacement::Bottom => {
if is_open {
IconName::PanelBottom
} else {
IconName::PanelBottomOpen
}
}
DockPlacement::Center => unreachable!(),
};
Some(
Button::new(SharedString::from(format!("toggle-dock:{:?}", placement)))
.icon(icon)
.small()
.ghost()
.tab_stop(false)
.tooltip(match is_open {
true => "Collapse",
false => "Expand",
})
.on_click(cx.listener({
let dock_area = self.dock_area.clone();
move |_this, _ev, window, cx| {
_ = dock_area.update(cx, |dock_area, cx| {
dock_area.toggle_dock(placement, window, cx);
});
}
})),
)
}
fn render_title_bar(
&self,
state: &TabState,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let view = cx.entity().clone();
// Get the dock area entity
let Some(dock_area) = self.dock_area.upgrade() else {
// Return a default element if the dock area is not available
return div().into_any_element();
};
let panel_style = dock_area.read(cx).panel_style;
#[cfg(target_os = "linux")]
let supported_controls = window.window_controls();
let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, window, cx);
let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, window, cx);
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
let has_extend_dock_button = left_dock_button.is_some() || bottom_dock_button.is_some();
let tabs_count = self.panels.len();
if self.panels.len() == 1 && panel_style == PanelStyle::Default {
if tabs_count == 1 && dock_area.read(cx).panel_style == PanelStyle::Default {
let panel = self.panels.first().unwrap();
if !panel.visible(cx) {
@@ -487,7 +598,21 @@ impl TabPanel {
.line_height(rems(1.0))
.h(px(30.))
.py_2()
.px_3()
.pl_3()
.pr_2()
.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
.when(left_dock_button.is_some(), |this| this.pl_2())
.when(right_dock_button.is_some(), |this| this.pr_2())
.when(has_extend_dock_button, |this| {
this.child(
h_flex()
.flex_shrink_0()
.mr_1()
.gap_1()
.children(left_dock_button)
.children(bottom_dock_button),
)
})
.child(
div()
.id("tab")
@@ -506,7 +631,7 @@ impl TabPanel {
this.on_drag(
DragPanel {
panel: panel.clone(),
tab_panel: view,
tab_panel: cx.entity(),
},
|drag, _, _, cx| {
cx.stop_propagation();
@@ -525,25 +650,46 @@ impl TabPanel {
.into_any_element();
}
let tabs_count = self.panels.len();
TabBar::new("tab-bar")
.track_scroll(self.tab_bar_scroll_handle.clone())
TabBar::new()
.track_scroll(&self.tab_bar_scroll_handle)
.h(TITLEBAR_HEIGHT)
.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
.when(self.zoomed, |this| {
this.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(has_extend_dock_button, |this| {
this.prefix(
h_flex()
.items_center()
.top_0()
.right(-px(1.))
.border_r_1()
.h_full()
.border_color(cx.theme().border)
.bg(cx.theme().elevated_surface_background)
.px_2()
.children(left_dock_button)
.children(bottom_dock_button),
)
})
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
let disabled = self.collapsed;
let mut active = state.active_panel.as_ref() == Some(panel);
let disabled = self.is_collapsed;
// If the panel is not visible, hide the tabbar
if !panel.visible(cx) {
return None;
}
// Always not show active tab style, if the panel is collapsed
if self.is_collapsed {
if self.collapsed {
active = false;
}
Some(
Tab::new(("tab", ix), panel.title(cx))
Tab::new()
.ix(ix)
.label(panel.title(cx))
.py_2()
.selected(active)
.disabled(disabled)
@@ -562,7 +708,7 @@ impl TabPanel {
}))
.when(state.draggable, |this| {
this.on_drag(
DragPanel::new(panel.clone(), view.clone()),
DragPanel::new(panel.clone(), cx.entity().clone()),
|drag, _, _, cx| {
cx.stop_propagation();
cx.new(|_| drag.clone())
@@ -586,16 +732,48 @@ impl TabPanel {
}),
)
}))
.child(
.last_empty_space(
// empty space to allow move to last tab right
div()
.id("tab-bar-empty-space")
.window_control_area(WindowControlArea::Drag)
.h_full()
.flex_grow()
.min_w_16()
.rounded(cx.theme().radius)
.when(!window.is_fullscreen(), |this| match cx.theme().platform {
PlatformKind::Linux => this
.when(supported_controls.window_menu, |this| {
this.on_mouse_down(MouseButton::Right, move |ev, window, _| {
window.show_window_menu(ev.position)
})
})
.on_mouse_move(cx.listener(move |this, _ev, window, _| {
if this.window_move {
this.window_move = false;
window.start_window_move();
}
}))
.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
this.window_move = false;
}))
.on_mouse_up(
MouseButton::Left,
cx.listener(move |this, _ev, _window, _cx| {
this.window_move = false;
}),
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _ev, _window, _cx| {
this.window_move = true;
}),
),
_ => this,
})
.when(state.droppable, |this| {
this.drag_over::<DragPanel>(|this, _, _, cx| {
let view = cx.entity();
this.drag_over::<DragPanel>(|this, _d, _window, cx| {
this.bg(cx.theme().surface_background)
})
.on_drop(cx.listener(
@@ -632,7 +810,7 @@ impl TabPanel {
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
if self.is_collapsed {
if self.collapsed {
return Empty {}.into_any_element();
}
@@ -905,16 +1083,16 @@ impl TabPanel {
return;
}
if !self.is_zoomed {
if !self.zoomed {
cx.emit(PanelEvent::ZoomIn)
} else {
cx.emit(PanelEvent::ZoomOut)
}
self.is_zoomed = !self.is_zoomed;
self.zoomed = !self.zoomed;
cx.spawn({
let is_zoomed = self.is_zoomed;
let is_zoomed = self.zoomed;
async move |view, cx| {
view.update(cx, |view, cx| {
view.set_zoomed(is_zoomed, cx);
@@ -948,7 +1126,6 @@ impl Focusable for TabPanel {
}
impl EventEmitter<DismissEvent> for TabPanel {}
impl EventEmitter<PanelEvent> for TabPanel {}
impl Render for TabPanel {
@@ -969,11 +1146,12 @@ impl Render for TabPanel {
}
v_flex()
.when(!self.is_collapsed, |this| {
.when(!self.collapsed, |this| {
this.on_action(cx.listener(Self::on_action_toggle_zoom))
.on_action(cx.listener(Self::on_action_close_panel))
})
.id("tab-panel")
.tab_group()
.track_focus(&focus_handle)
.size_full()
.overflow_hidden()