update tab
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Migrate the GPUI foundation to the published `gpui-pre` crates and GPUI Kit 0.6, off the zed and gpui-component git pins
|
- Migrate the GPUI foundation to the published `gpui-pre` crates and GPUI Kit 0.6, off the zed and gpui-component git pins
|
||||||
|
- Redesign the dock tab bar: the active tab is a muted grey, each tab holds a close button that shows on hover or when active, double-clicking a tab zooms its panel, and the trailing panel toolbar is gone
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
+154
-160
@@ -5,9 +5,9 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Anchor, Animation, AnimationExt as _, AnyElement, AnyView, App, AppContext as _, Bounds,
|
Animation, AnimationExt as _, AnyElement, AnyView, App, AppContext as _, Bounds, Context, Div,
|
||||||
Context, Div, Empty, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Point,
|
Empty, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Point, Render,
|
||||||
Render, ScrollHandle, SharedString, Stateful, StatefulInteractiveElement as _, StyleRefinement,
|
ScrollHandle, SharedString, Stateful, StatefulInteractiveElement as _, StyleRefinement,
|
||||||
Styled as _, Window, div, px, size,
|
Styled as _, Window, div, px, size,
|
||||||
};
|
};
|
||||||
use gpui_base::dock::{
|
use gpui_base::dock::{
|
||||||
@@ -18,21 +18,17 @@ use gpui_base::{ElementExt, InteractiveElementExt, Tab, Tabs};
|
|||||||
use gpui_component::animation::{Lerp as _, ease_out_cubic};
|
use gpui_component::animation::{Lerp as _, ease_out_cubic};
|
||||||
use gpui_component::button::{Button, ButtonVariants as _};
|
use gpui_component::button::{Button, ButtonVariants as _};
|
||||||
use gpui_component::dock::{ClosePanel, PanelControl, PanelHandle, ToggleZoom};
|
use gpui_component::dock::{ClosePanel, PanelControl, PanelHandle, ToggleZoom};
|
||||||
use gpui_component::menu::DropdownMenu as _;
|
use gpui_component::{ActiveTheme as _, Disableable as _, IconName, Sizable as _, h_flex, v_flex};
|
||||||
use gpui_component::{
|
|
||||||
ActiveTheme as _, Disableable as _, IconName, Selectable as _, Sizable as _, h_flex, v_flex,
|
|
||||||
};
|
|
||||||
use signed_ui::title_bar_drag_handlers;
|
use signed_ui::title_bar_drag_handlers;
|
||||||
|
|
||||||
use crate::dock_area::SkinShared;
|
use crate::dock_area::SkinShared;
|
||||||
use crate::{TAB_BAR_HEIGHT, t, window_controls};
|
use crate::{TAB_BAR_HEIGHT, t, window_controls};
|
||||||
|
|
||||||
/// The drag preview's size, reported to base for the drop placeholder.
|
|
||||||
const DRAG_PREVIEW_SIZE: gpui::Size<gpui::Pixels> = size(px(96.), px(30.));
|
const DRAG_PREVIEW_SIZE: gpui::Size<gpui::Pixels> = size(px(96.), px(30.));
|
||||||
|
|
||||||
/// The key a collapsed dock's stand-in tab bar is tagged with, so a test can
|
|
||||||
/// measure that it fits the strip base reserves. A no-op outside test builds.
|
|
||||||
const COLLAPSED_TAB_BAR_SELECTOR: &str = "signed-dock-collapsed-tab-bar";
|
const COLLAPSED_TAB_BAR_SELECTOR: &str = "signed-dock-collapsed-tab-bar";
|
||||||
|
const ACTIVE_TAB_SELECTOR: &str = "signed-dock-active-tab";
|
||||||
|
const ACTIVE_TAB_CLOSE_SELECTOR: &str = "signed-dock-active-tab-close";
|
||||||
|
const TAB_HOVER_GROUP: &str = "signed-dock-tab";
|
||||||
|
|
||||||
/// A panel's title, or its registered name when the panel has no handle.
|
/// A panel's title, or its registered name when the panel has no handle.
|
||||||
pub(crate) fn panel_title(
|
pub(crate) fn panel_title(
|
||||||
@@ -47,6 +43,7 @@ pub(crate) fn panel_title(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The preview that follows the cursor while a panel is dragged.
|
/// The preview that follows the cursor while a panel is dragged.
|
||||||
|
///
|
||||||
/// Base's `DragPanel` is the payload and draws nothing, this is the appearance half.
|
/// Base's `DragPanel` is the payload and draws nothing, this is the appearance half.
|
||||||
struct DragPanelPreview {
|
struct DragPanelPreview {
|
||||||
panel: Arc<dyn BasePanelView>,
|
panel: Arc<dyn BasePanelView>,
|
||||||
@@ -73,9 +70,14 @@ impl Render for DragPanelPreview {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The zoom affordance for the group's displayed panel, if it offers one.
|
/// The zoom affordance for the group's displayed panel, if it offers one.
|
||||||
/// The panel must offer a control and be zoomable, base refuses a zoom otherwise.
|
|
||||||
fn zoom_control(group: &TabGroupContext, cx: &App) -> Option<PanelControl> {
|
fn zoom_control(group: &TabGroupContext, cx: &App) -> Option<PanelControl> {
|
||||||
let panel = group.active_panel()?;
|
panel_zoom_control(group.active_panel()?, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The zoom affordance `panel` offers, if any.
|
||||||
|
///
|
||||||
|
/// The panel must offer a control and be zoomable, base refuses a zoom otherwise.
|
||||||
|
fn panel_zoom_control(panel: &Arc<dyn BasePanelView>, cx: &App) -> Option<PanelControl> {
|
||||||
panel
|
panel
|
||||||
.zoomable(cx)
|
.zoomable(cx)
|
||||||
.then(|| PanelHandle::of(panel).and_then(|handle| handle.zoom_control(cx)))
|
.then(|| PanelHandle::of(panel).and_then(|handle| handle.zoom_control(cx)))
|
||||||
@@ -83,6 +85,7 @@ fn zoom_control(group: &TabGroupContext, cx: &App) -> Option<PanelControl> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The left-most, top-most tab group in a container.
|
/// The left-most, top-most tab group in a container.
|
||||||
|
///
|
||||||
/// A left dock's collapse button lives in this group.
|
/// A left dock's collapse button lives in this group.
|
||||||
fn left_top_group(node: &PaneNode) -> Option<NodeId> {
|
fn left_top_group(node: &PaneNode) -> Option<NodeId> {
|
||||||
match node.kind() {
|
match node.kind() {
|
||||||
@@ -92,6 +95,7 @@ fn left_top_group(node: &PaneNode) -> Option<NodeId> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The right-most, top-most tab group.
|
/// The right-most, top-most tab group.
|
||||||
|
///
|
||||||
/// A vertical split picks its first child, a horizontal split picks its last.
|
/// A vertical split picks its first child, a horizontal split picks its last.
|
||||||
fn right_top_group(node: &PaneNode) -> Option<NodeId> {
|
fn right_top_group(node: &PaneNode) -> Option<NodeId> {
|
||||||
match node.kind() {
|
match node.kind() {
|
||||||
@@ -104,6 +108,53 @@ fn right_top_group(node: &PaneNode) -> Option<NodeId> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The bottom or right dock whose root tab group is this one, if any.
|
||||||
|
///
|
||||||
|
/// Base keeps a dock's last group, so the skin removes these docks as a whole.
|
||||||
|
fn dock_root_group(
|
||||||
|
shared: &SkinShared,
|
||||||
|
group: &TabGroupContext,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<DockPlacement> {
|
||||||
|
let area = shared.area().upgrade()?;
|
||||||
|
let area = area.read(cx);
|
||||||
|
|
||||||
|
[DockPlacement::Bottom, DockPlacement::Right]
|
||||||
|
.into_iter()
|
||||||
|
.find(|placement| {
|
||||||
|
area.layout(*placement)
|
||||||
|
.is_some_and(|tree| tree.root().id() == group.node())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close `panel`, removing its dock instead when base would refuse an empty one.
|
||||||
|
///
|
||||||
|
/// A bottom or right dock's only panel is that case, so the whole dock goes.
|
||||||
|
fn close_panel(
|
||||||
|
shared: &SkinShared,
|
||||||
|
group: &TabGroupContext,
|
||||||
|
panel: &Arc<dyn BasePanelView>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
if !panel.closable(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dock = (group.panels().len() <= 1)
|
||||||
|
.then(|| dock_root_group(shared, group, cx))
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
match dock {
|
||||||
|
Some(placement) => {
|
||||||
|
_ = shared.area().update(cx, |area, cx| {
|
||||||
|
area.remove_dock(placement, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None => group.close(panel.panel_id(cx), window, cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// One tab group's appearance, built once per container so its geometry is its own.
|
/// One tab group's appearance, built once per container so its geometry is its own.
|
||||||
pub(crate) struct SignedTabGroupSkin {
|
pub(crate) struct SignedTabGroupSkin {
|
||||||
shared: Rc<SkinShared>,
|
shared: Rc<SkinShared>,
|
||||||
@@ -146,26 +197,14 @@ impl SignedTabGroupSkin {
|
|||||||
left == group.node() && group.panels().len() == 1
|
left == group.node() && group.panels().len() == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The bottom or right dock whose root tab group is this one, if any.
|
|
||||||
/// Base keeps a dock's last group, so the skin removes these docks as a whole.
|
|
||||||
fn is_dock_root_group(&self, group: &TabGroupContext, cx: &App) -> Option<DockPlacement> {
|
|
||||||
let area = self.shared.area().upgrade()?;
|
|
||||||
let area = area.read(cx);
|
|
||||||
[DockPlacement::Bottom, DockPlacement::Right]
|
|
||||||
.into_iter()
|
|
||||||
.find(|placement| {
|
|
||||||
area.layout(*placement)
|
|
||||||
.is_some_and(|tree| tree.root().id() == group.node())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The tab's drag payload, or `None` when the group must not be rearranged.
|
/// The tab's drag payload, or `None` when the group must not be rearranged.
|
||||||
|
///
|
||||||
/// A locked group never is, a bottom or right dock root always is.
|
/// A locked group never is, a bottom or right dock root always is.
|
||||||
fn tab_drag(&self, group: &TabGroupContext, ix: usize, cx: &App) -> Option<DragPanel> {
|
fn tab_drag(&self, group: &TabGroupContext, ix: usize, cx: &App) -> Option<DragPanel> {
|
||||||
if group.is_locked() {
|
if group.is_locked() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if !group.is_draggable() && self.is_dock_root_group(group, cx).is_none() {
|
if !group.is_draggable() && dock_root_group(&self.shared, group, cx).is_none() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
group.drag_panel(ix, cx)
|
group.drag_panel(ix, cx)
|
||||||
@@ -277,95 +316,6 @@ impl SignedTabGroupSkin {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The trailing controls, the panel's own buttons, zoom and the ellipsis menu.
|
|
||||||
fn render_toolbar(
|
|
||||||
&self,
|
|
||||||
group: &TabGroupContext,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
if group.is_collapsed() {
|
|
||||||
return div();
|
|
||||||
}
|
|
||||||
|
|
||||||
let zoomed = group.is_zoomed();
|
|
||||||
let handle = group.active_panel().and_then(PanelHandle::of);
|
|
||||||
let control = zoom_control(group, cx);
|
|
||||||
let toolbar_zoom = control.is_some_and(|control| control.toolbar_visible());
|
|
||||||
let menu_zoom = control.is_some_and(|control| control.menu_visible());
|
|
||||||
// A bottom or right dock's only panel cannot close through the group.
|
|
||||||
// The close item is offered, the skin removes the whole dock instead.
|
|
||||||
let closable = group.is_closable()
|
|
||||||
|| (self.is_dock_root_group(group, cx).is_some()
|
|
||||||
&& group.active_panel().is_some_and(|panel| panel.closable(cx)));
|
|
||||||
let buttons = handle.and_then(|handle| handle.toolbar_buttons(window, cx));
|
|
||||||
let panel = handle.map(|handle| handle.panel());
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.occlude()
|
|
||||||
.when_some(buttons, |this, buttons| {
|
|
||||||
this.children(
|
|
||||||
buttons
|
|
||||||
.into_iter()
|
|
||||||
.map(|button| button.small().ghost().tab_stop(false)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map(|this| {
|
|
||||||
let value = if zoomed {
|
|
||||||
Some(("zoom-out", IconName::Minimize, t("Dock.Zoom Out")))
|
|
||||||
} else if toolbar_zoom {
|
|
||||||
Some(("zoom-in", IconName::Maximize, t("Dock.Zoom In")))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some((id, icon, tooltip)) = value {
|
|
||||||
this.child(
|
|
||||||
Button::new(id)
|
|
||||||
.icon(icon)
|
|
||||||
.small()
|
|
||||||
.ghost()
|
|
||||||
.tab_stop(false)
|
|
||||||
.tooltip_with_action(tooltip, &ToggleZoom, None)
|
|
||||||
.selected(zoomed)
|
|
||||||
.on_click({
|
|
||||||
let group = group.clone();
|
|
||||||
move |_, window, cx| group.toggle_zoom(window, cx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
Button::new("menu")
|
|
||||||
.icon(IconName::Ellipsis)
|
|
||||||
.small()
|
|
||||||
.ghost()
|
|
||||||
.tab_stop(false)
|
|
||||||
.dropdown_menu(move |menu, window, cx| {
|
|
||||||
menu.when_some(panel.clone(), |menu, panel| {
|
|
||||||
panel.dropdown_menu(menu, window, cx)
|
|
||||||
})
|
|
||||||
.separator()
|
|
||||||
.menu_with_disabled(
|
|
||||||
if zoomed {
|
|
||||||
t("Dock.Zoom Out")
|
|
||||||
} else {
|
|
||||||
t("Dock.Zoom In")
|
|
||||||
},
|
|
||||||
Box::new(ToggleZoom),
|
|
||||||
!menu_zoom,
|
|
||||||
)
|
|
||||||
.when(closable, |menu| {
|
|
||||||
menu.separator().menu(t("Dock.Close"), Box::new(ClosePanel))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.anchor(Anchor::TopRight),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One tab of the pill strip.
|
/// One tab of the pill strip.
|
||||||
/// While collapsed, tabs lose the active style and all interactions.
|
/// While collapsed, tabs lose the active style and all interactions.
|
||||||
/// The strip is also how a closed bottom dock is opened again.
|
/// The strip is also how a closed bottom dock is opened again.
|
||||||
@@ -384,37 +334,87 @@ impl SignedTabGroupSkin {
|
|||||||
let droppable = group.is_droppable();
|
let droppable = group.is_droppable();
|
||||||
let drag = self.tab_drag(group, ix, cx);
|
let drag = self.tab_drag(group, ix, cx);
|
||||||
let handle = PanelHandle::of(&panel);
|
let handle = PanelHandle::of(&panel);
|
||||||
|
let id = SharedString::from(format!("close-tab:{}:{ix}", group.node().as_u64()));
|
||||||
|
|
||||||
|
let closable = !collapsed
|
||||||
|
&& (group.is_closable()
|
||||||
|
|| (dock_root_group(&self.shared, group, cx).is_some() && panel.closable(cx)));
|
||||||
|
|
||||||
|
// Whether a double-click zooms this tab in.
|
||||||
|
let can_zoom_in = panel_zoom_control(&panel, cx).is_some();
|
||||||
|
|
||||||
Tab::new(ix)
|
Tab::new(ix)
|
||||||
.h_6()
|
.group(TAB_HOVER_GROUP)
|
||||||
.px_3()
|
.when(active, |this| {
|
||||||
.text_sm()
|
this.debug_selector(|| ACTIVE_TAB_SELECTOR.to_string())
|
||||||
.whitespace_nowrap()
|
})
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
|
||||||
.gap_1()
|
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
|
.h_7()
|
||||||
|
.min_w_32()
|
||||||
|
.gap_1()
|
||||||
|
.px_1p5()
|
||||||
|
.text_sm()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.text_color(cx.theme().foreground)
|
.text_color(cx.theme().tab_foreground)
|
||||||
.map(|this| match handle.and_then(|handle| handle.tab_name(cx)) {
|
// The title claims the free space and starts at the left, so a name
|
||||||
Some(tab_name) => this.child(tab_name),
|
// narrower than the tab leaves blank space before the close button.
|
||||||
None => this.child(panel_title(&panel, window, cx)),
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_1()
|
||||||
|
.h_6()
|
||||||
|
.items_center()
|
||||||
|
.overflow_hidden()
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.map(|this| match handle.and_then(|handle| handle.tab_name(cx)) {
|
||||||
|
Some(tab_name) => this.child(tab_name),
|
||||||
|
None => this.child(panel_title(&panel, window, cx)),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when(closable, |this| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.when(active, |this| {
|
||||||
|
this.debug_selector(|| ACTIVE_TAB_CLOSE_SELECTOR.to_string())
|
||||||
|
})
|
||||||
|
.when(!active, |this| {
|
||||||
|
this.invisible()
|
||||||
|
.group_hover(TAB_HOVER_GROUP, |this| this.visible())
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Button::new(id)
|
||||||
|
.icon(IconName::Close)
|
||||||
|
.tooltip(t("Dock.Close"))
|
||||||
|
.ghost()
|
||||||
|
.xsmall()
|
||||||
|
.tab_stop(false)
|
||||||
|
.on_click({
|
||||||
|
let group = group.clone();
|
||||||
|
let shared = self.shared.clone();
|
||||||
|
let panel = panel.clone();
|
||||||
|
move |_, window, cx| {
|
||||||
|
cx.stop_propagation();
|
||||||
|
close_panel(&shared, &group, &panel, window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
// Pill style, the selected tab is the filled pill, others show only on hover.
|
|
||||||
.styles(|styles| {
|
.styles(|styles| {
|
||||||
styles.selected(|style| {
|
styles.selected(|style| {
|
||||||
style
|
style
|
||||||
.text_color(cx.theme().tab_active_foreground)
|
.text_color(cx.theme().foreground)
|
||||||
.bg(cx.theme().tab_active)
|
.bg(cx.theme().secondary)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.hover(|this| {
|
.hover(|this| {
|
||||||
if active {
|
if active {
|
||||||
this
|
this
|
||||||
} else {
|
} else {
|
||||||
this.text_color(cx.theme().secondary_foreground)
|
this.text_color(cx.theme().foreground)
|
||||||
.bg(cx.theme().secondary_hover)
|
.bg(cx.theme().secondary_hover)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -422,7 +422,7 @@ impl SignedTabGroupSkin {
|
|||||||
.on_click({
|
.on_click({
|
||||||
let group = group.clone();
|
let group = group.clone();
|
||||||
let area = self.shared.area().clone();
|
let area = self.shared.area().clone();
|
||||||
move |_, window, cx| {
|
move |event, window, cx| {
|
||||||
group.select_tab(ix, window, cx);
|
group.select_tab(ix, window, cx);
|
||||||
|
|
||||||
// Clicking the strip of a collapsed bottom dock reopens it.
|
// Clicking the strip of a collapsed bottom dock reopens it.
|
||||||
@@ -430,7 +430,15 @@ impl SignedTabGroupSkin {
|
|||||||
_ = area.update(cx, |area, cx| {
|
_ = area.update(cx, |area, cx| {
|
||||||
area.toggle_dock(DockPlacement::Bottom, window, cx);
|
area.toggle_dock(DockPlacement::Bottom, window, cx);
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A double-click zooms the tab's panel
|
||||||
|
if event.click_count() < 2 || (!group.is_zoomed() && !can_zoom_in) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
group.toggle_zoom(window, cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.when(!collapsed, |this| {
|
.when(!collapsed, |this| {
|
||||||
@@ -533,10 +541,6 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
|||||||
if group.panels().is_empty() {
|
if group.panels().is_empty() {
|
||||||
return div().id("tab-panel");
|
return div().id("tab-panel");
|
||||||
}
|
}
|
||||||
// Base refuses an empty dock, so closing its only panel removes the dock.
|
|
||||||
let dock_to_remove = (group.panels().len() <= 1)
|
|
||||||
.then(|| self.is_dock_root_group(group, cx))
|
|
||||||
.flatten();
|
|
||||||
let shared = self.shared.clone();
|
let shared = self.shared.clone();
|
||||||
|
|
||||||
// `v_flex`, a plain `div` ignores `flex_grow` and the content would collapse.
|
// `v_flex`, a plain `div` ignores `flex_grow` and the content would collapse.
|
||||||
@@ -545,14 +549,10 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.bg(cx.theme().tokens.background)
|
.bg(cx.theme().tokens.background)
|
||||||
// A collapsed group has no content, so these actions are not registered.
|
|
||||||
.when(!group.is_collapsed(), |this| {
|
.when(!group.is_collapsed(), |this| {
|
||||||
this.on_action({
|
this.on_action({
|
||||||
let group = group.clone();
|
let group = group.clone();
|
||||||
move |_: &ToggleZoom, window, cx| {
|
move |_: &ToggleZoom, window, cx| {
|
||||||
// A panel with no zoom control is not zoomed in by the keybinding.
|
|
||||||
// Zooming out is never refused.
|
|
||||||
// Otherwise a zoomed panel that lost its control would strand the user.
|
|
||||||
if !group.is_zoomed() && control.is_none() {
|
if !group.is_zoomed() && control.is_none() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -566,18 +566,7 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
|||||||
let Some(panel) = group.active_panel() else {
|
let Some(panel) = group.active_panel() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !panel.closable(cx) {
|
close_panel(&shared, &group, panel, window, cx);
|
||||||
return;
|
|
||||||
}
|
|
||||||
let panel = panel.panel_id(cx);
|
|
||||||
match dock_to_remove {
|
|
||||||
Some(placement) => {
|
|
||||||
_ = shared.area().update(cx, |area, cx| {
|
|
||||||
area.remove_dock(placement, window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
None => group.close(panel, window, cx),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -615,17 +604,23 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
|||||||
let right_dock_button = self.dock_toggle_button(DockPlacement::Right, group, cx);
|
let right_dock_button = self.dock_toggle_button(DockPlacement::Right, group, cx);
|
||||||
let is_bottom_dock = bottom_dock_button.is_some();
|
let is_bottom_dock = bottom_dock_button.is_some();
|
||||||
|
|
||||||
// On macOS the traffic lights overlay the window's top-left corner.
|
// On macOS the traffic lights overlay the window's top-left corner, so the
|
||||||
// Only the tab bar that sits under them reserves the space.
|
// tab bar under them reserves the space. That is the center's top-left group
|
||||||
// That is the center's top-left group when the left dock is closed or absent.
|
// when the left dock is closed or absent, and the zoomed group otherwise:
|
||||||
|
// a zoom fills the whole area, hiding the docks the frame would draw.
|
||||||
let needs_traffic_light_padding = cfg!(target_os = "macos")
|
let needs_traffic_light_padding = cfg!(target_os = "macos")
|
||||||
&& self.shared.area().upgrade().is_some_and(|area| {
|
&& self.shared.area().upgrade().is_some_and(|area| {
|
||||||
let area = area.read(cx);
|
let area = area.read(cx);
|
||||||
!area.is_dock_open(DockPlacement::Left)
|
match area.zoomed_group() {
|
||||||
&& area
|
Some(zoomed) => zoomed == group.node(),
|
||||||
.layout(DockPlacement::Center)
|
None => {
|
||||||
.and_then(|tree| left_top_group(tree.root()))
|
!area.is_dock_open(DockPlacement::Left)
|
||||||
== Some(group.node())
|
&& area
|
||||||
|
.layout(DockPlacement::Center)
|
||||||
|
.and_then(|tree| left_top_group(tree.root()))
|
||||||
|
== Some(group.node())
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bring a newly displayed tab into view.
|
// Bring a newly displayed tab into view.
|
||||||
@@ -774,7 +769,6 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
|||||||
.and_then(PanelHandle::of)
|
.and_then(PanelHandle::of)
|
||||||
.and_then(|handle| handle.title_suffix(window, cx)),
|
.and_then(|handle| handle.title_suffix(window, cx)),
|
||||||
)
|
)
|
||||||
.child(self.render_toolbar(group, window, cx))
|
|
||||||
.children(right_dock_button),
|
.children(right_dock_button),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user