Allow splitting the terminal panel (#21238)
Closes https://github.com/zed-industries/zed/issues/4351  Applies the same splitting mechanism, as Zed's central pane has, to the terminal panel. Similar navigation, splitting and (de)serialization capabilities are supported. Notable caveats: * zooming keeps the terminal splits' ratio, rather expanding the terminal pane * on macOs, central panel is split with `cmd-k up/down/etc.` but `cmd-k` is a "standard" terminal clearing keybinding on macOS, so terminal panel splitting is done via `ctrl-k up/down/etc.` * task terminals are "split" into regular terminals, and also not persisted (same as currently in the terminal) Seems ok for the initial version, we can revisit and polish things later. Release Notes: - Added the ability to split the terminal panel
This commit is contained in:
parent
4564da2875
commit
d0bafce86b
17 changed files with 953 additions and 348 deletions
|
@ -315,7 +315,7 @@ pub trait SerializableItem: Item {
|
|||
_workspace: WeakView<Workspace>,
|
||||
_workspace_id: WorkspaceId,
|
||||
_item_id: ItemId,
|
||||
_cx: &mut ViewContext<Pane>,
|
||||
_cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>>;
|
||||
|
||||
fn serialize(
|
||||
|
@ -1032,7 +1032,7 @@ impl<T: FollowableItem> WeakFollowableItemHandle for WeakView<T> {
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test {
|
||||
use super::{Item, ItemEvent, SerializableItem, TabContentParams};
|
||||
use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
|
||||
use crate::{ItemId, ItemNavHistory, Workspace, WorkspaceId};
|
||||
use gpui::{
|
||||
AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView,
|
||||
InteractiveElement, IntoElement, Model, Render, SharedString, Task, View, ViewContext,
|
||||
|
@ -1040,6 +1040,7 @@ pub mod test {
|
|||
};
|
||||
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
|
||||
use std::{any::Any, cell::Cell, path::Path};
|
||||
use ui::WindowContext;
|
||||
|
||||
pub struct TestProjectItem {
|
||||
pub entry_id: Option<ProjectEntryId>,
|
||||
|
@ -1339,7 +1340,7 @@ pub mod test {
|
|||
_workspace: WeakView<Workspace>,
|
||||
workspace_id: WorkspaceId,
|
||||
_item_id: ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<anyhow::Result<View<Self>>> {
|
||||
let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx));
|
||||
Task::ready(Ok(view))
|
||||
|
|
|
@ -291,7 +291,7 @@ pub struct Pane {
|
|||
can_drop_predicate: Option<Arc<dyn Fn(&dyn Any, &mut WindowContext) -> bool>>,
|
||||
custom_drop_handle:
|
||||
Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
|
||||
can_split: bool,
|
||||
can_split_predicate: Option<Arc<dyn Fn(&mut Self, &dyn Any, &mut ViewContext<Self>) -> bool>>,
|
||||
should_display_tab_bar: Rc<dyn Fn(&ViewContext<Pane>) -> bool>,
|
||||
render_tab_bar_buttons:
|
||||
Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>)>,
|
||||
|
@ -411,7 +411,7 @@ impl Pane {
|
|||
project,
|
||||
can_drop_predicate,
|
||||
custom_drop_handle: None,
|
||||
can_split: true,
|
||||
can_split_predicate: None,
|
||||
should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show),
|
||||
render_tab_bar_buttons: Rc::new(move |pane, cx| {
|
||||
if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
|
||||
|
@ -623,9 +623,13 @@ impl Pane {
|
|||
self.should_display_tab_bar = Rc::new(should_display_tab_bar);
|
||||
}
|
||||
|
||||
pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
|
||||
self.can_split = can_split;
|
||||
cx.notify();
|
||||
pub fn set_can_split(
|
||||
&mut self,
|
||||
can_split_predicate: Option<
|
||||
Arc<dyn Fn(&mut Self, &dyn Any, &mut ViewContext<Self>) -> bool + 'static>,
|
||||
>,
|
||||
) {
|
||||
self.can_split_predicate = can_split_predicate;
|
||||
}
|
||||
|
||||
pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
|
||||
|
@ -2384,8 +2388,18 @@ impl Pane {
|
|||
self.zoomed
|
||||
}
|
||||
|
||||
fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
|
||||
if !self.can_split {
|
||||
fn handle_drag_move<T: 'static>(
|
||||
&mut self,
|
||||
event: &DragMoveEvent<T>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let can_split_predicate = self.can_split_predicate.take();
|
||||
let can_split = match &can_split_predicate {
|
||||
Some(can_split_predicate) => can_split_predicate(self, event.dragged_item(), cx),
|
||||
None => false,
|
||||
};
|
||||
self.can_split_predicate = can_split_predicate;
|
||||
if !can_split {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2679,6 +2693,10 @@ impl Pane {
|
|||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn drag_split_direction(&self) -> Option<SplitDirection> {
|
||||
self.drag_split_direction
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for Pane {
|
||||
|
|
|
@ -27,11 +27,11 @@ const VERTICAL_MIN_SIZE: f32 = 100.;
|
|||
/// Single-pane group is a regular pane.
|
||||
#[derive(Clone)]
|
||||
pub struct PaneGroup {
|
||||
pub(crate) root: Member,
|
||||
pub root: Member,
|
||||
}
|
||||
|
||||
impl PaneGroup {
|
||||
pub(crate) fn with_root(root: Member) -> Self {
|
||||
pub fn with_root(root: Member) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ impl PaneGroup {
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn render(
|
||||
pub fn render(
|
||||
&self,
|
||||
project: &Model<Project>,
|
||||
follower_states: &HashMap<PeerId, FollowerState>,
|
||||
|
@ -144,19 +144,51 @@ impl PaneGroup {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn panes(&self) -> Vec<&View<Pane>> {
|
||||
pub fn panes(&self) -> Vec<&View<Pane>> {
|
||||
let mut panes = Vec::new();
|
||||
self.root.collect_panes(&mut panes);
|
||||
panes
|
||||
}
|
||||
|
||||
pub(crate) fn first_pane(&self) -> View<Pane> {
|
||||
pub fn first_pane(&self) -> View<Pane> {
|
||||
self.root.first_pane()
|
||||
}
|
||||
|
||||
pub fn find_pane_in_direction(
|
||||
&mut self,
|
||||
active_pane: &View<Pane>,
|
||||
direction: SplitDirection,
|
||||
cx: &WindowContext,
|
||||
) -> Option<&View<Pane>> {
|
||||
let bounding_box = self.bounding_box_for_pane(active_pane)?;
|
||||
let cursor = active_pane.read(cx).pixel_position_of_cursor(cx);
|
||||
let center = match cursor {
|
||||
Some(cursor) if bounding_box.contains(&cursor) => cursor,
|
||||
_ => bounding_box.center(),
|
||||
};
|
||||
|
||||
let distance_to_next = crate::HANDLE_HITBOX_SIZE;
|
||||
|
||||
let target = match direction {
|
||||
SplitDirection::Left => {
|
||||
Point::new(bounding_box.left() - distance_to_next.into(), center.y)
|
||||
}
|
||||
SplitDirection::Right => {
|
||||
Point::new(bounding_box.right() + distance_to_next.into(), center.y)
|
||||
}
|
||||
SplitDirection::Up => {
|
||||
Point::new(center.x, bounding_box.top() - distance_to_next.into())
|
||||
}
|
||||
SplitDirection::Down => {
|
||||
Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
|
||||
}
|
||||
};
|
||||
self.pane_at_pixel_position(target)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Member {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Member {
|
||||
Axis(PaneAxis),
|
||||
Pane(View<Pane>),
|
||||
}
|
||||
|
@ -359,8 +391,8 @@ impl Member {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct PaneAxis {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PaneAxis {
|
||||
pub axis: Axis,
|
||||
pub members: Vec<Member>,
|
||||
pub flexes: Arc<Mutex<Vec<f32>>>,
|
||||
|
|
|
@ -777,7 +777,7 @@ pub struct ViewId {
|
|||
pub id: u64,
|
||||
}
|
||||
|
||||
struct FollowerState {
|
||||
pub struct FollowerState {
|
||||
center_pane: View<Pane>,
|
||||
dock_pane: Option<View<Pane>>,
|
||||
active_view_id: Option<ViewId>,
|
||||
|
@ -887,14 +887,16 @@ impl Workspace {
|
|||
let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let center_pane = cx.new_view(|cx| {
|
||||
Pane::new(
|
||||
let mut center_pane = Pane::new(
|
||||
weak_handle.clone(),
|
||||
project.clone(),
|
||||
pane_history_timestamp.clone(),
|
||||
None,
|
||||
NewFile.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
);
|
||||
center_pane.set_can_split(Some(Arc::new(|_, _, _| true)));
|
||||
center_pane
|
||||
});
|
||||
cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
|
||||
|
||||
|
@ -2464,14 +2466,16 @@ impl Workspace {
|
|||
|
||||
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
|
||||
let pane = cx.new_view(|cx| {
|
||||
Pane::new(
|
||||
let mut pane = Pane::new(
|
||||
self.weak_handle(),
|
||||
self.project.clone(),
|
||||
self.pane_history_timestamp.clone(),
|
||||
None,
|
||||
NewFile.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
);
|
||||
pane.set_can_split(Some(Arc::new(|_, _, _| true)));
|
||||
pane
|
||||
});
|
||||
cx.subscribe(&pane, Self::handle_pane_event).detach();
|
||||
self.panes.push(pane.clone());
|
||||
|
@ -2955,30 +2959,9 @@ impl Workspace {
|
|||
direction: SplitDirection,
|
||||
cx: &WindowContext,
|
||||
) -> Option<View<Pane>> {
|
||||
let bounding_box = self.center.bounding_box_for_pane(&self.active_pane)?;
|
||||
let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
|
||||
let center = match cursor {
|
||||
Some(cursor) if bounding_box.contains(&cursor) => cursor,
|
||||
_ => bounding_box.center(),
|
||||
};
|
||||
|
||||
let distance_to_next = pane_group::HANDLE_HITBOX_SIZE;
|
||||
|
||||
let target = match direction {
|
||||
SplitDirection::Left => {
|
||||
Point::new(bounding_box.left() - distance_to_next.into(), center.y)
|
||||
}
|
||||
SplitDirection::Right => {
|
||||
Point::new(bounding_box.right() + distance_to_next.into(), center.y)
|
||||
}
|
||||
SplitDirection::Up => {
|
||||
Point::new(center.x, bounding_box.top() - distance_to_next.into())
|
||||
}
|
||||
SplitDirection::Down => {
|
||||
Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
|
||||
}
|
||||
};
|
||||
self.center.pane_at_pixel_position(target).cloned()
|
||||
self.center
|
||||
.find_pane_in_direction(&self.active_pane, direction, cx)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn swap_pane_in_direction(
|
||||
|
@ -4591,6 +4574,10 @@ impl Workspace {
|
|||
let window = cx.window_handle().downcast::<Workspace>()?;
|
||||
cx.read_window(&window, |workspace, _| workspace).ok()
|
||||
}
|
||||
|
||||
pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
|
||||
self.zoomed.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
fn leader_border_for_pane(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue