Persist project and terminal panel sizes

This commit is contained in:
Antonio Scandurra 2023-05-22 15:55:44 +02:00
parent 146809eef0
commit 10e947cb5f
8 changed files with 139 additions and 54 deletions

1
Cargo.lock generated
View file

@ -4888,6 +4888,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"client", "client",
"context_menu", "context_menu",
"db",
"drag_and_drop", "drag_and_drop",
"editor", "editor",
"futures 0.3.28", "futures 0.3.28",

View file

@ -10,6 +10,7 @@ doctest = false
[dependencies] [dependencies]
context_menu = { path = "../context_menu" } context_menu = { path = "../context_menu" }
db = { path = "../db" }
drag_and_drop = { path = "../drag_and_drop" } drag_and_drop = { path = "../drag_and_drop" }
editor = { path = "../editor" } editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
@ -23,6 +24,7 @@ postage.workspace = true
futures.workspace = true futures.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true
unicase = "2.6" unicase = "2.6"
[dev-dependencies] [dev-dependencies]

View file

@ -1,10 +1,11 @@
use context_menu::{ContextMenu, ContextMenuItem}; use context_menu::{ContextMenu, ContextMenuItem};
use db::kvp::KEY_VALUE_STORE;
use drag_and_drop::{DragAndDrop, Draggable}; use drag_and_drop::{DragAndDrop, Draggable};
use editor::{Cancel, Editor}; use editor::{Cancel, Editor};
use futures::stream::StreamExt; use futures::stream::StreamExt;
use gpui::{ use gpui::{
actions, actions,
anyhow::{anyhow, Result}, anyhow::{self, anyhow, Result},
elements::{ elements::{
AnchorCorner, ChildView, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler, AnchorCorner, ChildView, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler,
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
@ -12,8 +13,8 @@ use gpui::{
geometry::vector::Vector2F, geometry::vector::Vector2F,
keymap_matcher::KeymapContext, keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton, PromptLevel}, platform::{CursorStyle, MouseButton, PromptLevel},
AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelHandle, Task,
ViewHandle, WeakViewHandle, WindowContext, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
}; };
use menu::{Confirm, SelectNext, SelectPrev}; use menu::{Confirm, SelectNext, SelectPrev};
use project::{ use project::{
@ -33,11 +34,13 @@ use std::{
}; };
use theme::{ui::FileName, ProjectPanelEntry}; use theme::{ui::FileName, ProjectPanelEntry};
use unicase::UniCase; use unicase::UniCase;
use util::{ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel}, dock::{DockPosition, Panel},
Workspace, Workspace,
}; };
const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -67,6 +70,7 @@ pub struct ProjectPanelSettingsContent {
} }
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProjectPanelDockPosition { pub enum ProjectPanelDockPosition {
Left, Left,
Right, Right,
@ -87,6 +91,8 @@ pub struct ProjectPanel {
dragged_entry_destination: Option<Arc<Path>>, dragged_entry_destination: Option<Arc<Path>>,
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
has_focus: bool, has_focus: bool,
width: Option<f32>,
pending_serialization: Task<Option<()>>,
} }
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
@ -183,8 +189,13 @@ pub enum Event {
Focus, Focus,
} }
#[derive(Serialize, Deserialize)]
struct SerializedProjectPanel {
width: Option<f32>,
}
impl ProjectPanel { impl ProjectPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> { fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
let project = workspace.project().clone(); let project = workspace.project().clone();
let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| { let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
cx.observe(&project, |this, _, cx| { cx.observe(&project, |this, _, cx| {
@ -258,6 +269,8 @@ impl ProjectPanel {
dragged_entry_destination: None, dragged_entry_destination: None,
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
has_focus: false, has_focus: false,
width: None,
pending_serialization: Task::ready(None),
}; };
this.update_visible_entries(None, cx); this.update_visible_entries(None, cx);
@ -311,6 +324,51 @@ impl ProjectPanel {
project_panel project_panel
} }
pub fn load(
workspace: WeakViewHandle<Workspace>,
cx: AsyncAppContext,
) -> Task<Result<ViewHandle<Self>>> {
cx.spawn(|mut cx| async move {
let serialized_panel = if let Some(panel) = cx
.background()
.spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
.await
.log_err()
.flatten()
{
Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
} else {
None
};
workspace.update(&mut cx, |workspace, cx| {
let panel = ProjectPanel::new(workspace, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
cx.notify();
});
}
panel
})
})
}
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
let width = self.width;
self.pending_serialization = cx.background().spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
PROJECT_PANEL_KEY.into(),
serde_json::to_string(&SerializedProjectPanel { width })?,
)
.await?;
anyhow::Ok(())
}
.log_err(),
);
}
fn deploy_context_menu( fn deploy_context_menu(
&mut self, &mut self,
position: Vector2F, position: Vector2F,
@ -1435,8 +1493,15 @@ impl workspace::dock::Panel for ProjectPanel {
); );
} }
fn default_size(&self, cx: &WindowContext) -> f32 { fn size(&self, cx: &WindowContext) -> f32 {
settings::get::<ProjectPanelSettings>(cx).default_width self.width
.unwrap_or_else(|| settings::get::<ProjectPanelSettings>(cx).default_width)
}
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
self.width = Some(size);
self.serialize(cx);
cx.notify();
} }
fn should_zoom_in_on_event(_: &Self::Event) -> bool { fn should_zoom_in_on_event(_: &Self::Event) -> bool {

View file

@ -120,6 +120,7 @@ pub fn init(cx: &mut AppContext) {
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TerminalDockPosition { pub enum TerminalDockPosition {
Left, Left,
Bottom, Bottom,

View file

@ -37,12 +37,14 @@ pub struct TerminalPanel {
pane: ViewHandle<Pane>, pane: ViewHandle<Pane>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
width: Option<f32>,
height: Option<f32>,
pending_serialization: Task<Option<()>>, pending_serialization: Task<Option<()>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
impl TerminalPanel { impl TerminalPanel {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self { fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let weak_self = cx.weak_handle(); let weak_self = cx.weak_handle();
let pane = cx.add_view(|cx| { let pane = cx.add_view(|cx| {
let window_id = cx.window_id(); let window_id = cx.window_id();
@ -90,6 +92,8 @@ impl TerminalPanel {
fs: workspace.app_state().fs.clone(), fs: workspace.app_state().fs.clone(),
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
pending_serialization: Task::ready(None), pending_serialization: Task::ready(None),
width: None,
height: None,
_subscriptions: subscriptions, _subscriptions: subscriptions,
}; };
let mut old_dock_position = this.position(cx); let mut old_dock_position = this.position(cx);
@ -112,7 +116,9 @@ impl TerminalPanel {
let serialized_panel = if let Some(panel) = cx let serialized_panel = if let Some(panel) = cx
.background() .background()
.spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) }) .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
.await? .await
.log_err()
.flatten()
{ {
Some(serde_json::from_str::<SerializedTerminalPanel>(&panel)?) Some(serde_json::from_str::<SerializedTerminalPanel>(&panel)?)
} else { } else {
@ -122,6 +128,9 @@ impl TerminalPanel {
let panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx)); let panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx));
let items = if let Some(serialized_panel) = serialized_panel.as_ref() { let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
cx.notify();
panel.height = serialized_panel.height;
panel.width = serialized_panel.width;
panel.pane.update(cx, |_, cx| { panel.pane.update(cx, |_, cx| {
serialized_panel serialized_panel
.items .items
@ -226,6 +235,8 @@ impl TerminalPanel {
.map(|item| item.id()) .map(|item| item.id())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let active_item_id = self.pane.read(cx).active_item().map(|item| item.id()); let active_item_id = self.pane.read(cx).active_item().map(|item| item.id());
let height = self.height;
let width = self.width;
self.pending_serialization = cx.background().spawn( self.pending_serialization = cx.background().spawn(
async move { async move {
KEY_VALUE_STORE KEY_VALUE_STORE
@ -234,6 +245,8 @@ impl TerminalPanel {
serde_json::to_string(&SerializedTerminalPanel { serde_json::to_string(&SerializedTerminalPanel {
items, items,
active_item_id, active_item_id,
height,
width,
})?, })?,
) )
.await?; .await?;
@ -288,12 +301,23 @@ impl Panel for TerminalPanel {
}); });
} }
fn default_size(&self, cx: &WindowContext) -> f32 { fn size(&self, cx: &WindowContext) -> f32 {
let settings = settings::get::<TerminalSettings>(cx); let settings = settings::get::<TerminalSettings>(cx);
match self.position(cx) { match self.position(cx) {
DockPosition::Left | DockPosition::Right => settings.default_width, DockPosition::Left | DockPosition::Right => {
DockPosition::Bottom => settings.default_height, self.width.unwrap_or_else(|| settings.default_width)
} }
DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
}
}
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
match self.position(cx) {
DockPosition::Left | DockPosition::Right => self.width = Some(size),
DockPosition::Bottom => self.height = Some(size),
}
self.serialize(cx);
cx.notify();
} }
fn should_zoom_in_on_event(event: &Event) -> bool { fn should_zoom_in_on_event(event: &Event) -> bool {
@ -360,4 +384,6 @@ impl Panel for TerminalPanel {
struct SerializedTerminalPanel { struct SerializedTerminalPanel {
items: Vec<usize>, items: Vec<usize>,
active_item_id: Option<usize>, active_item_id: Option<usize>,
width: Option<f32>,
height: Option<f32>,
} }

View file

@ -13,7 +13,8 @@ pub trait Panel: View {
fn position(&self, cx: &WindowContext) -> DockPosition; fn position(&self, cx: &WindowContext) -> DockPosition;
fn position_is_valid(&self, position: DockPosition) -> bool; fn position_is_valid(&self, position: DockPosition) -> bool;
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>); fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
fn default_size(&self, cx: &WindowContext) -> f32; fn size(&self, cx: &WindowContext) -> f32;
fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>);
fn icon_path(&self) -> &'static str; fn icon_path(&self) -> &'static str;
fn icon_tooltip(&self) -> String; fn icon_tooltip(&self) -> String;
fn icon_label(&self, _: &WindowContext) -> Option<String> { fn icon_label(&self, _: &WindowContext) -> Option<String> {
@ -39,7 +40,8 @@ pub trait PanelHandle {
fn is_zoomed(&self, cx: &WindowContext) -> bool; fn is_zoomed(&self, cx: &WindowContext) -> bool;
fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext); fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
fn set_active(&self, active: bool, cx: &mut WindowContext); fn set_active(&self, active: bool, cx: &mut WindowContext);
fn default_size(&self, cx: &WindowContext) -> f32; fn size(&self, cx: &WindowContext) -> f32;
fn set_size(&self, size: f32, cx: &mut WindowContext);
fn icon_path(&self, cx: &WindowContext) -> &'static str; fn icon_path(&self, cx: &WindowContext) -> &'static str;
fn icon_tooltip(&self, cx: &WindowContext) -> String; fn icon_tooltip(&self, cx: &WindowContext) -> String;
fn icon_label(&self, cx: &WindowContext) -> Option<String>; fn icon_label(&self, cx: &WindowContext) -> Option<String>;
@ -67,8 +69,12 @@ where
self.update(cx, |this, cx| this.set_position(position, cx)) self.update(cx, |this, cx| this.set_position(position, cx))
} }
fn default_size(&self, cx: &WindowContext) -> f32 { fn size(&self, cx: &WindowContext) -> f32 {
self.read(cx).default_size(cx) self.read(cx).size(cx)
}
fn set_size(&self, size: f32, cx: &mut WindowContext) {
self.update(cx, |this, cx| this.set_size(size, cx))
} }
fn is_zoomed(&self, cx: &WindowContext) -> bool { fn is_zoomed(&self, cx: &WindowContext) -> bool {
@ -151,7 +157,6 @@ impl DockPosition {
struct PanelEntry { struct PanelEntry {
panel: Rc<dyn PanelHandle>, panel: Rc<dyn PanelHandle>,
size: f32,
context_menu: ViewHandle<ContextMenu>, context_menu: ViewHandle<ContextMenu>,
_subscriptions: [Subscription; 2], _subscriptions: [Subscription; 2],
} }
@ -271,10 +276,8 @@ impl Dock {
]; ];
let dock_view_id = cx.view_id(); let dock_view_id = cx.view_id();
let size = panel.default_size(cx);
self.panel_entries.push(PanelEntry { self.panel_entries.push(PanelEntry {
panel: Rc::new(panel), panel: Rc::new(panel),
size,
context_menu: cx.add_view(|cx| { context_menu: cx.add_view(|cx| {
let mut menu = ContextMenu::new(dock_view_id, cx); let mut menu = ContextMenu::new(dock_view_id, cx);
menu.set_position_mode(OverlayPositionMode::Local); menu.set_position_mode(OverlayPositionMode::Local);
@ -343,28 +346,18 @@ impl Dock {
} }
} }
pub fn panel_size(&self, panel: &dyn PanelHandle) -> Option<f32> { pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
self.panel_entries self.panel_entries
.iter() .iter()
.find(|entry| entry.panel.id() == panel.id()) .find(|entry| entry.panel.id() == panel.id())
.map(|entry| entry.size) .map(|entry| entry.panel.size(cx))
} }
pub fn resize_panel(&mut self, panel: &dyn PanelHandle, size: f32) { pub fn active_panel_size(&self, cx: &WindowContext) -> Option<f32> {
let entry = self
.panel_entries
.iter_mut()
.find(|entry| entry.panel.id() == panel.id());
if let Some(entry) = entry {
entry.size = size;
}
}
pub fn active_panel_size(&self) -> Option<f32> {
if self.is_open { if self.is_open {
self.panel_entries self.panel_entries
.get(self.active_panel_index) .get(self.active_panel_index)
.map(|entry| entry.size) .map(|entry| entry.panel.size(cx))
} else { } else {
None None
} }
@ -372,7 +365,7 @@ impl Dock {
pub fn resize_active_panel(&mut self, size: f32, cx: &mut ViewContext<Self>) { pub fn resize_active_panel(&mut self, size: f32, cx: &mut ViewContext<Self>) {
if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) { if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
entry.size = size; entry.panel.set_size(size, cx);
cx.notify(); cx.notify();
} }
} }
@ -386,7 +379,7 @@ impl Dock {
.with_style(style.container) .with_style(style.container)
.resizable( .resizable(
self.position.to_resize_handle_side(), self.position.to_resize_handle_side(),
active_entry.size, active_entry.panel.size(cx),
|_, _, _| {}, |_, _, _| {},
) )
.into_any() .into_any()
@ -413,7 +406,7 @@ impl View for Dock {
.with_style(style.container) .with_style(style.container)
.resizable( .resizable(
self.position.to_resize_handle_side(), self.position.to_resize_handle_side(),
active_entry.size, active_entry.panel.size(cx),
|dock: &mut Self, size, cx| dock.resize_active_panel(size, cx), |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
) )
.into_any() .into_any()
@ -630,13 +623,17 @@ pub(crate) mod test {
unimplemented!() unimplemented!()
} }
fn default_size(&self, _: &WindowContext) -> f32 { fn size(&self, _: &WindowContext) -> f32 {
match self.position.axis() { match self.position.axis() {
Axis::Horizontal => 300., Axis::Horizontal => 300.,
Axis::Vertical => 200., Axis::Vertical => 200.,
} }
} }
fn set_size(&mut self, _: f32, _: &mut ViewContext<Self>) {
unimplemented!()
}
fn icon_path(&self) -> &'static str { fn icon_path(&self) -> &'static str {
"icons/test_panel.svg" "icons/test_panel.svg"
} }

View file

@ -853,11 +853,7 @@ impl Workspace {
if T::should_change_position_on_event(event) { if T::should_change_position_on_event(event) {
let new_position = panel.read(cx).position(cx); let new_position = panel.read(cx).position(cx);
let mut was_visible = false; let mut was_visible = false;
let mut size = None;
dock.update(cx, |dock, cx| { dock.update(cx, |dock, cx| {
if new_position.axis() == prev_position.axis() {
size = dock.panel_size(&panel);
}
prev_position = new_position; prev_position = new_position;
was_visible = dock.is_open() was_visible = dock.is_open()
@ -874,10 +870,6 @@ impl Workspace {
.clone(); .clone();
dock.update(cx, |dock, cx| { dock.update(cx, |dock, cx| {
dock.add_panel(panel.clone(), cx); dock.add_panel(panel.clone(), cx);
if let Some(size) = size {
dock.resize_panel(&panel, size);
}
if was_visible { if was_visible {
dock.set_open(true, cx); dock.set_open(true, cx);
dock.activate_panel(dock.panels_len() - 1, cx); dock.activate_panel(dock.panels_len() - 1, cx);
@ -3961,8 +3953,8 @@ mod tests {
panel_1.id() panel_1.id()
); );
assert_eq!( assert_eq!(
left_dock.read(cx).active_panel_size().unwrap(), left_dock.read(cx).active_panel_size(cx).unwrap(),
panel_1.default_size(cx) panel_1.size(cx)
); );
left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx)); left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
@ -3989,7 +3981,7 @@ mod tests {
right_dock.read(cx).active_panel().unwrap().id(), right_dock.read(cx).active_panel().unwrap().id(),
panel_1.id() panel_1.id()
); );
assert_eq!(right_dock.read(cx).active_panel_size().unwrap(), 1337.); assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
// Now we move panel_2 to the left // Now we move panel_2 to the left
panel_2.set_position(DockPosition::Left, cx); panel_2.set_position(DockPosition::Left, cx);
@ -4019,7 +4011,7 @@ mod tests {
left_dock.read(cx).active_panel().unwrap().id(), left_dock.read(cx).active_panel().unwrap().id(),
panel_1.id() panel_1.id()
); );
assert_eq!(left_dock.read(cx).active_panel_size().unwrap(), 1337.); assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
// And right the dock should be closed as it no longer has any panels. // And right the dock should be closed as it no longer has any panels.
assert!(!workspace.right_dock().read(cx).is_open()); assert!(!workspace.right_dock().read(cx).is_open());
@ -4034,8 +4026,8 @@ mod tests {
// since the panel orientation changed from vertical to horizontal. // since the panel orientation changed from vertical to horizontal.
let bottom_dock = workspace.bottom_dock(); let bottom_dock = workspace.bottom_dock();
assert_eq!( assert_eq!(
bottom_dock.read(cx).active_panel_size().unwrap(), bottom_dock.read(cx).active_panel_size(cx).unwrap(),
panel_1.default_size(cx), panel_1.size(cx),
); );
// Close bottom dock and move panel_1 back to the left. // Close bottom dock and move panel_1 back to the left.
bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx)); bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));

View file

@ -335,8 +335,12 @@ pub fn initialize_workspace(
} }
false false
}); });
})?;
let project_panel = ProjectPanel::new(workspace, cx); let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?;
workspace_handle.update(&mut cx, |workspace, cx| {
let project_panel_position = project_panel.position(cx); let project_panel_position = project_panel.position(cx);
workspace.add_panel(project_panel, cx); workspace.add_panel(project_panel, cx);
if !was_deserialized if !was_deserialized
@ -352,10 +356,7 @@ pub fn initialize_workspace(
{ {
workspace.toggle_dock(project_panel_position, cx); workspace.toggle_dock(project_panel_position, cx);
} }
})?;
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()).await?;
workspace_handle.update(&mut cx, |workspace, cx| {
workspace.add_panel(terminal_panel, cx) workspace.add_panel(terminal_panel, cx)
})?; })?;
Ok(()) Ok(())