Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions crates/gpui_dock/src/dock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use gpui::{
use gpui_component::StyledExt;
use serde::{Deserialize, Serialize};

use super::{DockArea, DockItem, Panel, PanelView, TabPanel};
use super::{DockArea, DockEvent, DockItem, Panel, PanelView, TabPanel};
use crate::resizable::{PANEL_MIN_SIZE, resize_handle};

#[derive(Clone)]
Expand Down Expand Up @@ -275,12 +275,13 @@ impl Dock {
}

/// Set the size of the Dock.
pub fn set_size(&mut self, size: Pixels, _: &mut Window, cx: &mut Context<Self>) {
pub fn set_size(&mut self, size: Pixels, window: &mut Window, cx: &mut Context<Self>) {
let mut size = size.max(self.min_size);
if let Some(max) = self.max_size {
size = size.min(max);
}
self.size = size;
self.emit_layout_changed(window, cx);
cx.notify();
}

Expand All @@ -291,9 +292,17 @@ impl Dock {
cx.defer_in(window, move |_, window, cx| {
item.set_collapsed(!open, window, cx);
});
self.emit_layout_changed(window, cx);
cx.notify();
}

fn emit_layout_changed(&self, window: &mut Window, cx: &mut Context<Self>) {
let dock_area = self.dock_area.clone();
cx.defer_in(window, move |_, _window, cx| {
let _ = dock_area.update(cx, |_, cx| cx.emit(DockEvent::LayoutChanged));
});
}

/// Add item to the Dock.
pub fn add_panel(
&mut self,
Expand Down
70 changes: 70 additions & 0 deletions crates/gpui_dock/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,17 @@ impl DockArea {
.collect()
}

pub fn all_tab_panels(&self, cx: &App) -> Vec<Entity<TabPanel>> {
let mut out = self.center.tab_panels(cx);
for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock]
.into_iter()
.flatten()
{
out.extend(dock.read(cx).panel.tab_panels(cx));
}
out
}

fn render_items(&self, _window: &mut Window, _cx: &mut Context<Self>) -> AnyElement {
match &self.center {
DockItem::Split { view, .. } => view.clone().into_any_element(),
Expand Down Expand Up @@ -1343,6 +1354,65 @@ mod tests {
}
}

#[gpui::test]
fn empty_layout_containers_dump_their_container_type(cx: &mut gpui::TestAppContext) {
cx.update(|app| {
gpui_component::init(app);
crate::init(app);
});

let window_cx = cx.add_empty_window();
window_cx.update(|window, app| {
let dock_area = app.new(|cx| DockArea::new("dock", None, window, cx));
let dock_weak = dock_area.downgrade();

let tabs = DockItem::tabs(Vec::new(), &dock_weak, window, app);
assert!(matches!(tabs.view().dump(app).info, PanelInfo::Tabs { .. }));

let stack = DockItem::v_split(Vec::new(), &dock_weak, window, app);
assert!(matches!(
stack.view().dump(app).info,
PanelInfo::Stack { .. }
));
});
}

#[gpui::test]
fn bare_side_dock_panel_stays_bare_after_state_round_trip(cx: &mut gpui::TestAppContext) {
cx.update(|app| {
gpui_component::init(app);
crate::init(app);
register_panel(app, "fake.panel", |_, _, _, _, cx| {
Box::new(cx.new(|cx| FakePanel::new("restored", cx)))
});
});

let window_cx = cx.add_empty_window();
window_cx.update(|window, app| {
let source = app.new(|cx| DockArea::new("source", None, window, cx));
let panel: Arc<dyn PanelView> = Arc::new(app.new(|cx| FakePanel::new("source", cx)));
source.update(app, |dock, cx| {
dock.set_left_dock(DockItem::panel(panel), None, true, window, cx);
});
let state = source.read(app).dump(app);

let restored = app.new(|cx| DockArea::new("restored", None, window, cx));
restored
.update(app, |dock, cx| dock.load(state, window, cx))
.expect("restore dock state");

assert!(matches!(
restored
.read(app)
.left_dock()
.expect("left dock")
.read(app)
.panel(),
DockItem::Panel { .. }
));
});
}

#[gpui::test]
fn dock_area_visible_tab_panels_excludes_collapsed_docks(cx: &mut gpui::TestAppContext) {
cx.update(|app| {
Expand Down
10 changes: 10 additions & 0 deletions crates/gpui_dock/src/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
/// Once you have defined a panel name, this must not be changed.
fn panel_name(&self) -> &'static str;

/// Whether this panel should be included in persisted dock state.
fn persistable(&self, cx: &App) -> bool {
true
}

/// The name of the tab of the panel, default is `None`.
///
/// Used to display in the already collapsed tab panel.
Expand Down Expand Up @@ -225,6 +230,7 @@ pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
#[allow(unused_variables)]
pub trait PanelView: 'static + Send + Sync {
fn panel_name(&self, cx: &App) -> &'static str;
fn persistable(&self, cx: &App) -> bool;
fn panel_id(&self, cx: &App) -> EntityId;
fn tab_name(&self, cx: &App) -> Option<SharedString>;
fn tab_tooltip(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement>;
Expand Down Expand Up @@ -254,6 +260,10 @@ impl<T: Panel> PanelView for Entity<T> {
self.read(cx).panel_name()
}

fn persistable(&self, cx: &App) -> bool {
self.read(cx).persistable(cx)
}

fn panel_id(&self, _: &App) -> EntityId {
self.entity_id()
}
Expand Down
2 changes: 1 addition & 1 deletion crates/gpui_dock/src/stack_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ impl Panel for StackPanel {
fn dump(&self, cx: &App) -> PanelState {
let sizes = self.state.read(cx).sizes().clone();
let mut state = PanelState::new(self);
state.info = PanelInfo::stack(sizes, self.axis);
for panel in &self.panels {
state.add_child(panel.dump(cx));
state.info = PanelInfo::stack(sizes.clone(), self.axis);
}

state
Expand Down
23 changes: 22 additions & 1 deletion crates/gpui_dock/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,35 @@ impl DockState {
}
}

pub fn panel_state(&self) -> &PanelState {
&self.panel
}

pub fn set_panel_state(&mut self, panel: PanelState) {
self.panel = panel;
}

/// Convert the DockState to Dock
pub fn to_dock(
&self,
dock_area: WeakEntity<DockArea>,
window: &mut Window,
cx: &mut App,
) -> Entity<Dock> {
let item = self.panel.to_item(dock_area.clone(), window, cx);
let item = match &self.panel.info {
PanelInfo::Panel(_) => {
let view = PanelRegistry::build_panel(
&self.panel.panel_name,
dock_area.clone(),
&self.panel,
&self.panel.info,
window,
cx,
);
DockItem::panel(view.into())
}
_ => self.panel.to_item(dock_area.clone(), window, cx),
};
cx.new(|cx| {
Dock::from_state(
dock_area.clone(),
Expand Down
100 changes: 98 additions & 2 deletions crates/gpui_dock/src/tab_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,17 @@ impl Panel for TabPanel {

fn dump(&self, cx: &App) -> PanelState {
let mut state = PanelState::new(self);
for panel in self.panels.iter() {
let mut active_ix = 0;
for (ix, panel) in self.panels.iter().enumerate() {
if !panel.persistable(cx) {
continue;
}
if ix < self.active_ix {
active_ix += 1;
}
state.add_child(panel.dump(cx));
state.info = PanelInfo::tabs(self.active_ix);
}
state.info = PanelInfo::tabs(active_ix.min(state.children.len().saturating_sub(1)));
state
}

Expand All @@ -154,6 +161,10 @@ impl Panel for TabPanel {
}

impl TabPanel {
pub fn panels(&self) -> &[Arc<dyn PanelView>] {
&self.panels
}

pub fn new(
stack_panel: Option<WeakEntity<StackPanel>>,
dock_area: WeakEntity<DockArea>,
Expand Down Expand Up @@ -405,6 +416,32 @@ impl TabPanel {
cx.notify();
}

pub fn replace_panel(
&mut self,
old_panel: Arc<dyn PanelView>,
new_panel: Arc<dyn PanelView>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
let Some(ix) = self
.panels
.iter()
.position(|panel| panel.view() == old_panel.view())
else {
return false;
};

old_panel.on_removed(window, cx);
new_panel.on_added_to(cx.entity().downgrade(), window, cx);
self.panels[ix] = new_panel;
if self.active_ix == ix {
self.set_active_ix(ix, window, cx);
}
cx.emit(PanelEvent::LayoutChanged);
cx.notify();
true
}

fn detach_panel(
&mut self,
panel: Arc<dyn PanelView>,
Expand Down Expand Up @@ -1690,6 +1727,65 @@ mod tests {
}
}

struct FakeTransientPanel {
focus: FocusHandle,
}

impl EventEmitter<PanelEvent> for FakeTransientPanel {}

impl Focusable for FakeTransientPanel {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus.clone()
}
}

impl Panel for FakeTransientPanel {
fn panel_name(&self) -> &'static str {
"fake.panel.transient"
}

fn persistable(&self, _cx: &App) -> bool {
false
}
}

impl Render for FakeTransientPanel {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
}
}

#[gpui::test]
fn tab_panel_dump_omits_transient_panels_and_preserves_active_panel(
cx: &mut gpui::TestAppContext,
) {
cx.update(|app| {
gpui_component::init(app);
crate::init(app);
});

let (tab_panel, window_cx) = cx.add_window_view(|window, cx| {
let dock_area = cx.new(|cx| DockArea::new("dock", None, window, cx));
TabPanel::new(None, dock_area.downgrade(), window, cx)
});
window_cx.update(|window, cx| {
let transient: Arc<dyn PanelView> = Arc::new(cx.new(|cx| FakeTransientPanel {
focus: cx.focus_handle(),
}));
let active: Arc<dyn PanelView> = Arc::new(cx.new(FakePanelWithoutIcon::new));
tab_panel.update(cx, |tabs, cx| {
tabs.add_panel(transient, window, cx);
tabs.add_panel(active, window, cx);
tabs.active_ix = 1;
});

let state = tab_panel.read(cx).dump(cx);
assert_eq!(state.children.len(), 1);
assert_eq!(state.children[0].panel_name, "fake.panel.without_icon");
assert_eq!(state.info.active_index(), Some(0));
});
}

#[gpui::test]
fn tab_panel_close_all_calls_panel_on_close(cx: &mut gpui::TestAppContext) {
cx.update(|app| {
Expand Down
2 changes: 1 addition & 1 deletion crates/gpui_sftp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ mod state;
mod view;

pub use state::{Entry, EntryKind, SortColumn, SortDirection, SortSpec, TreeState, VisibleRow};
pub use view::{SftpEvent, SftpView};
pub use view::{SftpEvent, SftpStatus, SftpView};

actions!(
sftp,
Expand Down
Loading
Loading