update dock
This commit is contained in:
+99
-168
@@ -152,8 +152,6 @@ pub struct TabPanel {
|
||||
will_split_placement: Option<Placement>,
|
||||
drop_placeholder_animation: Option<DropPlaceholderAnimation>,
|
||||
drop_placeholder_animation_name: SharedString,
|
||||
/// Is TabPanel used in Tiles.
|
||||
in_tiles: bool,
|
||||
|
||||
/// Bounds of the title bar row (the wrapper around the tab bar), in
|
||||
/// window coordinates. Measured via `on_prepaint` to position the
|
||||
@@ -183,9 +181,8 @@ impl Panel for TabPanel {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. When is the final panel in the dock, it will not able to close.
|
||||
// 2. When is in the Tiles, it will always able to close (by active panel state).
|
||||
if !self.draggable(cx) && !self.in_tiles {
|
||||
// The final panel in the dock is not closable.
|
||||
if !self.draggable(cx) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -314,18 +311,12 @@ impl TabPanel {
|
||||
zoomed: false,
|
||||
collapsed: false,
|
||||
closable: true,
|
||||
in_tiles: false,
|
||||
title_bar_bounds: None,
|
||||
title_bar_strip_bounds: None,
|
||||
title_bar_suffix_bounds: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark the TabPanel as being used in Tiles.
|
||||
pub(super) fn set_in_tiles(&mut self, in_tiles: bool) {
|
||||
self.in_tiles = in_tiles;
|
||||
}
|
||||
|
||||
pub(super) fn set_parent(&mut self, view: WeakEntity<StackPanel>) {
|
||||
self.stack_panel = Some(view);
|
||||
}
|
||||
@@ -717,6 +708,11 @@ impl TabPanel {
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Button> {
|
||||
// Only the left dock can be toggled.
|
||||
if placement != DockPlacement::Left {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.zoomed {
|
||||
return None;
|
||||
}
|
||||
@@ -733,47 +729,15 @@ impl TabPanel {
|
||||
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!(),
|
||||
} {
|
||||
if dock_area.left_dock.is_none() || toggle_button_panels.left != Some(view_entity_id) {
|
||||
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!(),
|
||||
let icon = if is_open {
|
||||
IconName::PanelLeft
|
||||
} else {
|
||||
IconName::PanelLeftOpen
|
||||
};
|
||||
|
||||
Some(
|
||||
@@ -810,11 +774,7 @@ impl TabPanel {
|
||||
};
|
||||
|
||||
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 is_bottom_dock = bottom_dock_button.is_some();
|
||||
let has_extend_dock_button = left_dock_button.is_some();
|
||||
|
||||
// macOS: the traffic lights overlay the window's top-left corner. The
|
||||
// left dock (sidebar) normally clears them; when it is closed or
|
||||
@@ -849,7 +809,6 @@ impl TabPanel {
|
||||
.pr_2()
|
||||
.when(left_dock_button.is_some(), |this| this.pl_2())
|
||||
.when(needs_traffic_light_padding, |this| this.pl(px(80.)))
|
||||
.when(right_dock_button.is_some(), |this| this.pr_2())
|
||||
.when_some(title_style, |this, theme| {
|
||||
this.bg(theme.background).text_color(theme.foreground)
|
||||
})
|
||||
@@ -859,8 +818,7 @@ impl TabPanel {
|
||||
.flex_shrink_0()
|
||||
.mr_1()
|
||||
.gap_1()
|
||||
.children(left_dock_button)
|
||||
.children(bottom_dock_button),
|
||||
.children(left_dock_button),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
@@ -889,8 +847,7 @@ impl TabPanel {
|
||||
.flex_shrink_0()
|
||||
.ml_1()
|
||||
.gap_1()
|
||||
.child(self.render_toolbar(state, window, cx))
|
||||
.children(right_dock_button),
|
||||
.child(self.render_toolbar(state, window, cx)),
|
||||
),
|
||||
window,
|
||||
cx,
|
||||
@@ -973,17 +930,13 @@ impl TabPanel {
|
||||
.top_0()
|
||||
// Right -1 for avoid border overlap with the first tab
|
||||
.right(-px(1.))
|
||||
.border_r_1()
|
||||
.border_b_1()
|
||||
.h_full()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().tokens.tab_bar)
|
||||
.px_2()
|
||||
.when(needs_traffic_light_padding, |this| {
|
||||
this.pl(px(80.))
|
||||
})
|
||||
.children(left_dock_button)
|
||||
.children(bottom_dock_button),
|
||||
.children(left_dock_button),
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -1017,21 +970,8 @@ impl TabPanel {
|
||||
})
|
||||
.selected(active)
|
||||
.on_click(cx.listener({
|
||||
let is_collapsed = self.collapsed;
|
||||
let dock_area = self.dock_area.clone();
|
||||
move |view, _, window, cx| {
|
||||
view.set_active_ix(ix, window, cx);
|
||||
|
||||
// Open dock if clicked on the collapsed bottom dock
|
||||
if is_bottom_dock && is_collapsed {
|
||||
_ = dock_area.update(cx, |dock_area, cx| {
|
||||
dock_area.toggle_dock(
|
||||
DockPlacement::Bottom,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}))
|
||||
.when(!droppable, |this| {
|
||||
@@ -1060,23 +1000,18 @@ impl TabPanel {
|
||||
this.on_drop(drag, Some(ix), true, window, cx)
|
||||
},
|
||||
))
|
||||
.when(
|
||||
!self.in_tiles,
|
||||
|this| {
|
||||
this.drag_over::<AnyDrag>(|this, _, _, cx| {
|
||||
this.rounded_l_none()
|
||||
.border_l_2()
|
||||
.border_r_0()
|
||||
.border_color(cx.theme().drag_border)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
|this, item: &AnyDrag, _, cx| {
|
||||
this.will_split_placement = None;
|
||||
this.emit_drag_drop(item, None, cx);
|
||||
},
|
||||
))
|
||||
.drag_over::<AnyDrag>(|this, _, _, cx| {
|
||||
this.rounded_l_none()
|
||||
.border_l_2()
|
||||
.border_r_0()
|
||||
.border_color(cx.theme().drag_border)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
|this, item: &AnyDrag, _, cx| {
|
||||
this.will_split_placement = None;
|
||||
this.emit_drag_drop(item, None, cx);
|
||||
},
|
||||
)
|
||||
))
|
||||
},
|
||||
)
|
||||
}),
|
||||
@@ -1120,19 +1055,14 @@ impl TabPanel {
|
||||
this.on_drop(drag, ix, false, window, cx)
|
||||
},
|
||||
))
|
||||
.when(
|
||||
!self.in_tiles,
|
||||
|this| {
|
||||
this.drag_over::<AnyDrag>(|this, _, _, cx| {
|
||||
this.bg(cx.theme().tokens.drop_target)
|
||||
})
|
||||
.on_drop(cx.listener(
|
||||
|this, item: &AnyDrag, _, cx| {
|
||||
this.will_split_placement = None;
|
||||
this.emit_drag_drop(item, None, cx);
|
||||
},
|
||||
))
|
||||
},
|
||||
.drag_over::<AnyDrag>(|this, _, _, cx| {
|
||||
this.bg(cx.theme().tokens.drop_target)
|
||||
})
|
||||
.on_drop(
|
||||
cx.listener(|this, item: &AnyDrag, _, cx| {
|
||||
this.will_split_placement = None;
|
||||
this.emit_drag_drop(item, None, cx);
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -1142,10 +1072,7 @@ impl TabPanel {
|
||||
.items_center()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.border_l_1()
|
||||
.border_b_1()
|
||||
.h_full()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().tokens.tab_bar)
|
||||
.px_2()
|
||||
.gap_1()
|
||||
@@ -1165,8 +1092,7 @@ impl TabPanel {
|
||||
self.active_panel(cx)
|
||||
.and_then(|panel| panel.title_suffix(window, cx)),
|
||||
)
|
||||
.child(self.render_toolbar(state, window, cx))
|
||||
.when_some(right_dock_button, |this, btn| this.child(btn)),
|
||||
.child(self.render_toolbar(state, window, cx)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -1214,9 +1140,7 @@ impl TabPanel {
|
||||
)
|
||||
.when(state.droppable, |this| {
|
||||
this.on_drag_move(cx.listener(Self::on_panel_drag_move))
|
||||
.when(!self.in_tiles, |this| {
|
||||
this.on_drag_move(cx.listener(Self::on_item_drag_move))
|
||||
})
|
||||
.on_drag_move(cx.listener(Self::on_item_drag_move))
|
||||
.child(
|
||||
div()
|
||||
.invisible()
|
||||
@@ -1233,13 +1157,11 @@ impl TabPanel {
|
||||
.on_drop(cx.listener(|this, drag: &DragPanel, window, cx| {
|
||||
this.on_drop(drag, None, true, window, cx)
|
||||
}))
|
||||
.when(!self.in_tiles, |this| {
|
||||
this.group_drag_over::<AnyDrag>("", |this| this.visible())
|
||||
.on_drop(cx.listener(|this, item: &AnyDrag, _, cx| {
|
||||
let placement = this.will_split_placement.take();
|
||||
this.emit_drag_drop(item, placement, cx);
|
||||
}))
|
||||
})
|
||||
.group_drag_over::<AnyDrag>("", |this| this.visible())
|
||||
.on_drop(cx.listener(|this, item: &AnyDrag, _, cx| {
|
||||
let placement = this.will_split_placement.take();
|
||||
this.emit_drag_drop(item, placement, cx);
|
||||
}))
|
||||
.when_some(placeholder, |this, animation| {
|
||||
let from = animation.from.origin - animation.to.origin;
|
||||
this.child(
|
||||
@@ -1601,9 +1523,8 @@ impl TabPanel {
|
||||
self.remove_panel(panel, window, cx);
|
||||
}
|
||||
|
||||
// Remove self from the parent DockArea.
|
||||
// This is ensure to remove from Tiles
|
||||
if self.panels.is_empty() && self.in_tiles {
|
||||
// Remove self from the parent DockArea when the last panel is closed.
|
||||
if self.panels.is_empty() {
|
||||
let tab_panel = Arc::new(cx.entity());
|
||||
window.defer(cx, {
|
||||
let dock_area = self.dock_area.clone();
|
||||
@@ -1681,12 +1602,12 @@ mod tests {
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use gpui::{TestAppContext, VisualTestContext, WindowHandle, WindowOptions, size};
|
||||
use gpui::{Axis, TestAppContext, VisualTestContext, WindowHandle, WindowOptions, size};
|
||||
use gpui_component::tab::{Tab, TabBar};
|
||||
use gpui_component::{Root, Theme, v_flex};
|
||||
|
||||
use super::*;
|
||||
use crate::{DockItem, TileMeta};
|
||||
use crate::DockItem;
|
||||
|
||||
#[test]
|
||||
fn drop_placeholder_bounds_cover_each_target_placement() {
|
||||
@@ -2018,49 +1939,6 @@ mod tests {
|
||||
assert!(cx.read(|cx| fixture.dock_area.read(cx).is_center_empty(cx)));
|
||||
}
|
||||
|
||||
/// A `TabPanel` inside `Tiles` has no parent `StackPanel` to remove itself
|
||||
/// from, so emptying it leaves the tile behind and the walk has to recurse.
|
||||
#[gpui::test]
|
||||
fn center_holding_only_empty_tiles_is_empty(cx: &mut TestAppContext) {
|
||||
let fixture = setup(cx);
|
||||
let mut cx = VisualTestContext::from_window(fixture.window.into(), cx);
|
||||
|
||||
let (tabs, tab_panel, panels) = build_tabs(&fixture, &["A"], None, &mut cx);
|
||||
let weak_dock_area = fixture.dock_area.downgrade();
|
||||
cx.update(|window, cx| {
|
||||
let tiles = DockItem::tiles(
|
||||
vec![tabs],
|
||||
vec![TileMeta::default()],
|
||||
&weak_dock_area,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
fixture
|
||||
.dock_area
|
||||
.update(cx, |area, cx| area.set_center(tiles, window, cx))
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert!(!cx.read(|cx| fixture.dock_area.read(cx).is_center_empty(cx)));
|
||||
|
||||
for panel in panels {
|
||||
cx.update(|window, cx| {
|
||||
tab_panel.update(cx, |tab_panel, cx| {
|
||||
tab_panel.remove_panel(Arc::new(panel.clone()), window, cx)
|
||||
})
|
||||
});
|
||||
}
|
||||
cx.run_until_parked();
|
||||
|
||||
let tiles = cx.read(|cx| {
|
||||
let DockItem::Tiles { view, .. } = fixture.dock_area.read(cx).center() else {
|
||||
unreachable!("the centre is a Tiles item");
|
||||
};
|
||||
view.read(cx).panels().len()
|
||||
});
|
||||
assert_eq!(tiles, 1, "the emptied TabPanel is still listed as a tile");
|
||||
assert!(cx.read(|cx| fixture.dock_area.read(cx).is_center_empty(cx)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn single_panel_group_receives_initial_active(cx: &mut TestAppContext) {
|
||||
let fixture = setup(cx);
|
||||
@@ -2287,6 +2165,59 @@ mod tests {
|
||||
assert_eq!(drain(&fixture.log), [("A", false)]);
|
||||
}
|
||||
|
||||
/// Adding a panel to the right of the center splits it horizontally;
|
||||
/// a later bottom add wraps the split in a vertical one.
|
||||
#[gpui::test]
|
||||
fn add_panel_splits_the_center(cx: &mut TestAppContext) {
|
||||
let fixture = setup(cx);
|
||||
let mut cx = VisualTestContext::from_window(fixture.window.into(), cx);
|
||||
|
||||
let (item_a, _, _) = build_tabs(&fixture, &["A"], None, &mut cx);
|
||||
let (_item_b, _, panels_b) = build_tabs(&fixture, &["B"], None, &mut cx);
|
||||
let (_item_c, _, panels_c) = build_tabs(&fixture, &["C"], None, &mut cx);
|
||||
|
||||
cx.update(|window, cx| {
|
||||
fixture.dock_area.update(cx, |dock_area, cx| {
|
||||
dock_area.set_center(item_a, window, cx);
|
||||
dock_area.add_panel(
|
||||
Arc::new(panels_b[0].clone()),
|
||||
DockPlacement::Right,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
let (axis, len) = cx.read(|cx| match fixture.dock_area.read(cx).center() {
|
||||
DockItem::Split { axis, items, .. } => (*axis, items.len()),
|
||||
other => panic!("center must be a horizontal split, got {other:?}"),
|
||||
});
|
||||
assert_eq!(axis, Axis::Horizontal);
|
||||
assert_eq!(len, 2, "the right panel must be appended to the split");
|
||||
|
||||
cx.update(|window, cx| {
|
||||
fixture.dock_area.update(cx, |dock_area, cx| {
|
||||
dock_area.add_panel(
|
||||
Arc::new(panels_c[0].clone()),
|
||||
DockPlacement::Bottom,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
cx.read(|cx| match fixture.dock_area.read(cx).center() {
|
||||
DockItem::Split {
|
||||
axis, items, sizes, ..
|
||||
} => {
|
||||
assert_eq!(*axis, Axis::Vertical);
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_eq!(sizes.len(), 2);
|
||||
}
|
||||
other => panic!("center must be wrapped in a vertical split, got {other:?}"),
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ProbeFlags {
|
||||
empty_down: AtomicBool,
|
||||
|
||||
Reference in New Issue
Block a user