Cargo.lock 🔗
@@ -4889,6 +4889,7 @@ dependencies = [
"anyhow",
"client",
"context_menu",
+ "db",
"drag_and_drop",
"editor",
"futures 0.3.28",
Antonio Scandurra created
Closes
https://linear.app/zed-industries/issue/Z-1188/allow-docks-to-be-visible-on-left-bottom-right
To prepare the way for the AI assistant, we want to revamp the way docks
and panels work. In this PR, we replace the dock as it currently exists
with 3 independent docks at the left, bottom, and right edge of the
workspace. To replace the "expanded" dock, we will introduce a zoom
feature that works on any pane and some panels. When showing a dock with
an active panel that is zoomed, it will automatically appear zoomed.
This replicates the expanded dock experience. If you unzoom, the panel
will still be visible.
### Panels only
We want to simplify these docks to only allow them to contain *panels*.
By doing this, we don't need to give each dock a tab bar, minimizing
clutter in the workspace. Each panel will remember its size, and the
dock will adjust to that size when the panel is toggled. This will allow
each panel to take up the amount of space that makes sense for its use
case.
There will be 3 kinds of panels:
* Project panel: This currently lives in the left "sidebar", which this
PR renames to the left dock. Users will be able to left click the icon
and switch the project panel to the right dock.
* Terminal panel: This is the primary user of the previous dock. Now all
terminals will live in a terminal panel, which can be docked at the
left, bottom, or right. This dock will contain tabs, but only for
terminals. Terminals will still be able to be dragged into the
workspace, but by default, the first new terminal will live in the
terminal panel which lives in one of the 3 docks.
* Feedback: Instead of opening a pane, let's explore making this a panel
instead.
### Status bar icons
A nice feature of this change is that it simplifies our status bar by
eliminating the dock button. We will only show icons for specific
panels. Panel in the left dock will appear at the left side of the
status bar. Panels in the right dock on the far right, panels in the
bottom will appear on the right, to the immediate left of the buttons
for right panels. Left clicking any panel button will allow it to be
redocked to any valid location for that panel.
### Paving the way for the AI assistant
I'm envisioning the assistant living in the far right panel by default.
So in the stock config, you'll have the project panel on the left,
terminal on the bottom, assistant on the right, and code in the middle.
Let's fucking go!
Cargo.lock | 1
assets/keymaps/atom.json | 8
assets/keymaps/default.json | 34
assets/keymaps/jetbrains.json | 11
assets/keymaps/sublime_text.json | 9
assets/keymaps/textmate.json | 6
assets/settings/default.json | 22
crates/collab/src/tests.rs | 3
crates/collab_ui/src/incoming_call_notification.rs | 1
crates/collab_ui/src/project_shared_notification.rs | 1
crates/copilot/src/sign_in.rs | 1
crates/copilot_button/src/copilot_button.rs | 4
crates/editor/src/items.rs | 12
crates/feedback/src/deploy_feedback_button.rs | 4
crates/file_finder/src/file_finder.rs | 12
crates/gpui/src/app.rs | 115 +
crates/gpui/src/app/window.rs | 7
crates/gpui/src/elements.rs | 26
crates/gpui/src/elements/list.rs | 4
crates/gpui/src/elements/mouse_event_handler.rs | 21
crates/gpui/src/elements/resizable.rs | 133
crates/gpui/src/platform.rs | 10
crates/gpui/src/platform/mac/window.rs | 2
crates/project_panel/Cargo.toml | 2
crates/project_panel/src/project_panel.rs | 190 ++
crates/project_panel/src/project_panel_settings.rs | 11
crates/sqlez/src/bindable.rs | 2
crates/sqlez/src/statement.rs | 6
crates/sqlez/src/typed_statements.rs | 6
crates/terminal/src/terminal.rs | 14
crates/terminal_view/src/terminal_button.rs | 173 --
crates/terminal_view/src/terminal_panel.rs | 389 ++++
crates/terminal_view/src/terminal_view.rs | 3
crates/theme/src/theme.rs | 32
crates/welcome/src/welcome.rs | 4
crates/workspace/src/dock.rs | 1287 ++++++--------
crates/workspace/src/dock/toggle_dock_button.rs | 125 -
crates/workspace/src/item.rs | 6
crates/workspace/src/pane.rs | 604 ++----
crates/workspace/src/pane/dragged_item_receiver.rs | 101
crates/workspace/src/pane_group.rs | 16
crates/workspace/src/persistence.rs | 221 -
crates/workspace/src/persistence/model.rs | 134
crates/workspace/src/sidebar.rs | 321 ---
crates/workspace/src/workspace.rs | 613 ++++--
crates/workspace/src/workspace_settings.rs | 45
crates/zed/src/main.rs | 5
crates/zed/src/menus.rs | 2
crates/zed/src/zed.rs | 249 +-
styles/src/styleTree/statusBar.ts | 5
styles/src/styleTree/workspace.ts | 35
51 files changed, 2,409 insertions(+), 2,639 deletions(-)
@@ -4889,6 +4889,7 @@ dependencies = [
"anyhow",
"client",
"context_menu",
+ "db",
"drag_and_drop",
"editor",
"futures 0.3.28",
@@ -39,8 +39,8 @@
{
"context": "Workspace",
"bindings": {
- "cmd-\\": "workspace::ToggleLeftSidebar",
- "cmd-k cmd-b": "workspace::ToggleLeftSidebar",
+ "cmd-\\": "workspace::ToggleLeftDock",
+ "cmd-k cmd-b": "workspace::ToggleLeftDock",
"cmd-t": "file_finder::Toggle",
"cmd-shift-r": "project_symbols::Toggle"
}
@@ -62,9 +62,5 @@
"ctrl-f": "project_panel::ExpandSelectedEntry",
"ctrl-shift-c": "project_panel::CopyPath"
}
- },
- {
- "context": "Dock",
- "bindings": {}
}
]
@@ -39,7 +39,8 @@
"cmd-shift-n": "workspace::NewWindow",
"cmd-o": "workspace::Open",
"alt-cmd-o": "projects::OpenRecent",
- "ctrl-`": "workspace::NewTerminal"
+ "ctrl-~": "workspace::NewTerminal",
+ "ctrl-`": "terminal_panel::ToggleFocus"
}
},
{
@@ -223,7 +224,8 @@
"cmd-shift-g": "search::SelectPrevMatch",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",
- "alt-cmd-r": "search::ToggleRegex"
+ "alt-cmd-r": "search::ToggleRegex",
+ "shift-escape": "workspace::ToggleZoom"
}
},
// Bindings from VS Code
@@ -365,7 +367,7 @@
"workspace::ActivatePane",
8
],
- "cmd-b": "workspace::ToggleLeftSidebar",
+ "cmd-b": "workspace::ToggleLeftDock",
"cmd-shift-f": "workspace::NewSearch",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-k cmd-s": "zed::OpenKeymap",
@@ -459,32 +461,6 @@
"cmd-enter": "project_search::SearchInNew"
}
},
- {
- "context": "Workspace",
- "bindings": {
- "shift-escape": "dock::FocusDock"
- }
- },
- {
- "bindings": {
- "cmd-shift-k cmd-shift-right": "dock::AnchorDockRight",
- "cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom",
- "cmd-shift-k cmd-shift-up": "dock::ExpandDock"
- }
- },
- {
- "context": "Pane",
- "bindings": {
- "cmd-escape": "dock::AddTabToDock"
- }
- },
- {
- "context": "Pane && docked",
- "bindings": {
- "shift-escape": "dock::HideDock",
- "cmd-escape": "dock::RemoveTabFromDock"
- }
- },
{
"context": "ProjectPanel",
"bindings": {
@@ -68,15 +68,8 @@
"cmd-shift-o": "file_finder::Toggle",
"cmd-shift-a": "command_palette::Toggle",
"cmd-alt-o": "project_symbols::Toggle",
- "cmd-1": "workspace::ToggleLeftSidebar",
- "cmd-6": "diagnostics::Deploy",
- "alt-f12": "dock::FocusDock"
- }
- },
- {
- "context": "Dock",
- "bindings": {
- "alt-f12": "dock::HideDock"
+ "cmd-1": "workspace::ToggleLeftDock",
+ "cmd-6": "diagnostics::Deploy"
}
}
]
@@ -45,18 +45,11 @@
{
"context": "Workspace",
"bindings": {
- "ctrl-`": "dock::FocusDock",
- "cmd-k cmd-b": "workspace::ToggleLeftSidebar",
+ "cmd-k cmd-b": "workspace::ToggleLeftDock",
"cmd-t": "file_finder::Toggle",
"shift-cmd-r": "project_symbols::Toggle",
// Currently busted: https://github.com/zed-industries/feedback/issues/898
"ctrl-0": "project_panel::ToggleFocus"
}
- },
- {
- "context": "Dock",
- "bindings": {
- "ctrl-`": "dock::HideDock"
- }
}
]
@@ -68,7 +68,7 @@
{
"context": "Workspace",
"bindings": {
- "cmd-alt-ctrl-d": "workspace::ToggleLeftSidebar",
+ "cmd-alt-ctrl-d": "workspace::ToggleLeftDock",
"cmd-t": "file_finder::Toggle",
"cmd-shift-t": "project_symbols::Toggle"
}
@@ -83,9 +83,5 @@
{
"context": "ProjectPanel",
"bindings": {}
- },
- {
- "context": "Dock",
- "bindings": {}
}
]
@@ -72,7 +72,11 @@
},
"project_panel": {
// Whether to show the git status in the project panel.
- "git_status": true
+ "git_status": true,
+ // Where to dock project panel. Can be 'left' or 'right'.
+ "dock": "left",
+ // Default width of the project panel.
+ "default_width": 240
},
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
@@ -90,16 +94,6 @@
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
- // Where to place the dock by default. This setting can take three
- // values:
- //
- // 1. Position the dock attached to the bottom of the workspace
- // "default_dock_anchor": "bottom"
- // 2. Position the dock to the right of the workspace like a side panel
- // "default_dock_anchor": "right"
- // 3. Position the dock full screen over the entire workspace"
- // "default_dock_anchor": "expanded"
- "default_dock_anchor": "bottom",
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
"remove_trailing_whitespace_on_save": true,
@@ -190,6 +184,12 @@
// }
// }
"shell": "system",
+ // Where to dock terminals panel. Can be 'left', 'right', 'bottom'.
+ "dock": "bottom",
+ // Default width when the terminal is docked to the left or right.
+ "default_width": 640,
+ // Default height when the terminal is docked to the bottom.
+ "default_height": 320,
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
@@ -192,8 +192,7 @@ impl TestServer {
languages: Arc::new(LanguageRegistry::test()),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
- initialize_workspace: |_, _, _| unimplemented!(),
- dock_default_item_factory: |_, _| None,
+ initialize_workspace: |_, _, _, _| unimplemented!(),
background_actions: || &[],
});
@@ -41,6 +41,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
titlebar: None,
center: false,
focus: false,
+ show: true,
kind: WindowKind::PopUp,
is_movable: false,
screen: Some(screen),
@@ -35,6 +35,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
titlebar: None,
center: false,
focus: false,
+ show: true,
kind: WindowKind::PopUp,
is_movable: false,
screen: Some(screen),
@@ -73,6 +73,7 @@ fn create_copilot_auth_window(
titlebar: None,
center: true,
focus: true,
+ show: true,
kind: WindowKind::Normal,
is_movable: true,
screen: None,
@@ -66,8 +66,8 @@ impl View for CopilotButton {
let style = theme
.workspace
.status_bar
- .sidebar_buttons
- .item
+ .panel_buttons
+ .button
.style_for(state, active);
Flex::row()
@@ -1231,27 +1231,27 @@ mod tests {
}
fn as_local(&self) -> Option<&dyn language::LocalFile> {
- todo!()
+ unimplemented!()
}
fn mtime(&self) -> SystemTime {
- todo!()
+ unimplemented!()
}
fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
- todo!()
+ unimplemented!()
}
fn is_deleted(&self) -> bool {
- todo!()
+ unimplemented!()
}
fn as_any(&self) -> &dyn std::any::Any {
- todo!()
+ unimplemented!()
}
fn to_proto(&self) -> rpc::proto::File {
- todo!()
+ unimplemented!()
}
}
}
@@ -39,8 +39,8 @@ impl View for DeployFeedbackButton {
let style = &theme
.workspace
.status_bar
- .sidebar_buttons
- .item
+ .panel_buttons
+ .button
.style_for(state, active);
Svg::new("icons/feedback_16.svg")
@@ -380,7 +380,7 @@ mod tests {
use gpui::{TestAppContext, ViewHandle};
use menu::{Confirm, SelectNext};
use serde_json::json;
- use workspace::{AppState, Pane, Workspace};
+ use workspace::{AppState, Workspace};
#[ctor::ctor]
fn init_logger() {
@@ -1161,9 +1161,13 @@ mod tests {
assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
}
});
- workspace.update(cx, |workspace, cx| {
- Pane::close_active_item(workspace, &workspace::CloseActiveItem, cx);
- });
+ active_pane
+ .update(cx, |pane, cx| {
+ pane.close_active_item(&workspace::CloseActiveItem, cx)
+ .unwrap()
+ })
+ .await
+ .unwrap();
deterministic.run_until_parked();
cx.read(|cx| {
for pane in workspace.read(cx).panes() {
@@ -1460,27 +1460,13 @@ impl AppContext {
self.views_metadata.remove(&(window_id, view_id));
let mut view = self.views.remove(&(window_id, view_id)).unwrap();
view.release(self);
- let change_focus_to = self.windows.get_mut(&window_id).and_then(|window| {
+ if let Some(window) = self.windows.get_mut(&window_id) {
window.parents.remove(&view_id);
window
.invalidation
.get_or_insert_with(Default::default)
.removed
.push(view_id);
- if window.focused_view_id == Some(view_id) {
- Some(window.root_view().id())
- } else {
- None
- }
- });
-
- if let Some(view_id) = change_focus_to {
- self.pending_effects
- .push_back(Effect::Focus(FocusEffect::View {
- window_id,
- view_id: Some(view_id),
- is_forced: false,
- }));
}
self.pending_effects
@@ -1717,8 +1703,69 @@ impl AppContext {
if let Some(invalidation) = invalidation {
let appearance = cx.window.platform_window.appearance();
cx.invalidate(invalidation, appearance);
- if cx.layout(refreshing).log_err().is_some() {
+ if let Some(old_parents) = cx.layout(refreshing).log_err() {
updated_windows.insert(window_id);
+
+ if let Some(focused_view_id) = cx.focused_view_id() {
+ let old_ancestors = std::iter::successors(
+ Some(focused_view_id),
+ |&view_id| old_parents.get(&view_id).copied(),
+ )
+ .collect::<HashSet<_>>();
+ let new_ancestors =
+ cx.ancestors(focused_view_id).collect::<HashSet<_>>();
+
+ // Notify the old ancestors of the focused view when they don't contain it anymore.
+ for old_ancestor in old_ancestors.iter().copied() {
+ if !new_ancestors.contains(&old_ancestor) {
+ if let Some(mut view) =
+ cx.views.remove(&(window_id, old_ancestor))
+ {
+ view.focus_out(
+ focused_view_id,
+ cx,
+ old_ancestor,
+ );
+ cx.views
+ .insert((window_id, old_ancestor), view);
+ }
+ }
+ }
+
+ // Notify the new ancestors of the focused view if they contain it now.
+ for new_ancestor in new_ancestors.iter().copied() {
+ if !old_ancestors.contains(&new_ancestor) {
+ if let Some(mut view) =
+ cx.views.remove(&(window_id, new_ancestor))
+ {
+ view.focus_in(
+ focused_view_id,
+ cx,
+ new_ancestor,
+ );
+ cx.views
+ .insert((window_id, new_ancestor), view);
+ }
+ }
+ }
+
+ // When the previously-focused view has been dropped and
+ // there isn't any pending focus, focus the root view.
+ let root_view_id = cx.window.root_view().id();
+ if focused_view_id != root_view_id
+ && !cx.views.contains_key(&(window_id, focused_view_id))
+ && !focus_effects.contains_key(&window_id)
+ {
+ focus_effects.insert(
+ window_id,
+ FocusEffect::View {
+ window_id,
+ view_id: Some(root_view_id),
+ is_forced: false,
+ },
+ );
+ }
+ }
}
}
});
@@ -1895,9 +1942,27 @@ impl AppContext {
fn handle_focus_effect(&mut self, effect: FocusEffect) {
let window_id = effect.window_id();
self.update_window(window_id, |cx| {
+ // Ensure the newly-focused view still exists, otherwise focus
+ // the root view instead.
let focused_id = match effect {
- FocusEffect::View { view_id, .. } => view_id,
- FocusEffect::ViewParent { view_id, .. } => cx.ancestors(view_id).skip(1).next(),
+ FocusEffect::View { view_id, .. } => {
+ if let Some(view_id) = view_id {
+ if cx.views.contains_key(&(window_id, view_id)) {
+ Some(view_id)
+ } else {
+ Some(cx.root_view().id())
+ }
+ } else {
+ None
+ }
+ }
+ FocusEffect::ViewParent { view_id, .. } => Some(
+ cx.window
+ .parents
+ .get(&view_id)
+ .copied()
+ .unwrap_or(cx.root_view().id()),
+ ),
};
let focus_changed = cx.window.focused_view_id != focused_id;
@@ -3802,6 +3867,12 @@ impl<T> PartialEq for ViewHandle<T> {
}
}
+impl<T> PartialEq<AnyViewHandle> for ViewHandle<T> {
+ fn eq(&self, other: &AnyViewHandle) -> bool {
+ self.window_id == other.window_id && self.view_id == other.view_id
+ }
+}
+
impl<T> PartialEq<WeakViewHandle<T>> for ViewHandle<T> {
fn eq(&self, other: &WeakViewHandle<T>) -> bool {
self.window_id == other.window_id && self.view_id == other.view_id
@@ -3952,6 +4023,12 @@ impl Clone for AnyViewHandle {
}
}
+impl PartialEq for AnyViewHandle {
+ fn eq(&self, other: &Self) -> bool {
+ self.window_id == other.window_id && self.view_id == other.view_id
+ }
+}
+
impl<T> PartialEq<ViewHandle<T>> for AnyViewHandle {
fn eq(&self, other: &ViewHandle<T>) -> bool {
self.window_id == other.window_id && self.view_id == other.view_id
@@ -4198,7 +4275,7 @@ impl<T> Hash for WeakViewHandle<T> {
}
}
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct AnyWeakViewHandle {
window_id: usize,
view_id: usize,
@@ -29,6 +29,7 @@ use sqlez::{
};
use std::{
any::TypeId,
+ mem,
ops::{Deref, DerefMut, Range},
};
use util::ResultExt;
@@ -890,7 +891,7 @@ impl<'a> WindowContext<'a> {
Ok(element)
}
- pub(crate) fn layout(&mut self, refreshing: bool) -> Result<()> {
+ pub(crate) fn layout(&mut self, refreshing: bool) -> Result<HashMap<usize, usize>> {
let window_size = self.window.platform_window.content_size();
let root_view_id = self.window.root_view().id();
let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
@@ -923,11 +924,11 @@ impl<'a> WindowContext<'a> {
}
}
- self.window.parents = new_parents;
+ let old_parents = mem::replace(&mut self.window.parents, new_parents);
self.window
.rendered_views
.insert(root_view_id, rendered_root);
- Ok(())
+ Ok(old_parents)
}
pub(crate) fn paint(&mut self) -> Result<Scene> {
@@ -187,25 +187,23 @@ pub trait Element<V: View>: 'static {
Tooltip::new::<Tag, V>(id, text, action, style, self.into_any(), cx)
}
- fn with_resize_handle<Tag: 'static>(
+ fn resizable(
self,
- element_id: usize,
- side: Side,
- handle_size: f32,
- initial_size: f32,
- cx: &mut ViewContext<V>,
+ side: HandleSide,
+ size: f32,
+ on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
) -> Resizable<V>
where
Self: 'static + Sized,
{
- Resizable::new::<Tag, V>(
- self.into_any(),
- element_id,
- side,
- handle_size,
- initial_size,
- cx,
- )
+ Resizable::new(self.into_any(), side, size, on_resize)
+ }
+
+ fn mouse<Tag>(self, region_id: usize) -> MouseEventHandler<Tag, V>
+ where
+ Self: Sized,
+ {
+ MouseEventHandler::for_child(self.into_any(), region_id)
}
}
@@ -990,7 +990,7 @@ mod tests {
_: &mut V,
_: &mut ViewContext<V>,
) {
- todo!()
+ unimplemented!()
}
fn rect_for_text_range(
@@ -1003,7 +1003,7 @@ mod tests {
_: &V,
_: &ViewContext<V>,
) -> Option<RectF> {
- todo!()
+ unimplemented!()
}
fn debug(&self, _: RectF, _: &(), _: &(), _: &V, _: &ViewContext<V>) -> serde_json::Value {
@@ -32,10 +32,25 @@ pub struct MouseEventHandler<Tag: 'static, V: View> {
/// Element which provides a render_child callback with a MouseState and paints a mouse
/// region under (or above) it for easy mouse event handling.
impl<Tag, V: View> MouseEventHandler<Tag, V> {
- pub fn new<D, F>(region_id: usize, cx: &mut ViewContext<V>, render_child: F) -> Self
+ pub fn for_child(child: impl Element<V>, region_id: usize) -> Self {
+ Self {
+ child: child.into_any(),
+ region_id,
+ cursor_style: None,
+ handlers: Default::default(),
+ notify_on_hover: false,
+ notify_on_click: false,
+ hoverable: false,
+ above: false,
+ padding: Default::default(),
+ _tag: PhantomData,
+ }
+ }
+
+ pub fn new<E, F>(region_id: usize, cx: &mut ViewContext<V>, render_child: F) -> Self
where
- D: Element<V>,
- F: FnOnce(&mut MouseState, &mut ViewContext<V>) -> D,
+ E: Element<V>,
+ F: FnOnce(&mut MouseState, &mut ViewContext<V>) -> E,
{
let mut mouse_state = cx.mouse_state::<Tag>(region_id);
let child = render_child(&mut mouse_state, cx).into_any();
@@ -1,4 +1,4 @@
-use std::{cell::Cell, rc::Rc};
+use std::{cell::RefCell, rc::Rc};
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json;
@@ -7,25 +7,23 @@ use crate::{
geometry::rect::RectF,
platform::{CursorStyle, MouseButton},
scene::MouseDrag,
- AnyElement, Axis, Element, ElementStateHandle, LayoutContext, MouseRegion, SceneBuilder, View,
+ AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
ViewContext,
};
-use super::{ConstrainedBox, Hook};
-
#[derive(Copy, Clone, Debug)]
-pub enum Side {
+pub enum HandleSide {
Top,
Bottom,
Left,
Right,
}
-impl Side {
+impl HandleSide {
fn axis(&self) -> Axis {
match self {
- Side::Left | Side::Right => Axis::Horizontal,
- Side::Top | Side::Bottom => Axis::Vertical,
+ HandleSide::Left | HandleSide::Right => Axis::Horizontal,
+ HandleSide::Top | HandleSide::Bottom => Axis::Vertical,
}
}
@@ -33,8 +31,8 @@ impl Side {
/// then top-to-bottom
fn before_content(self) -> bool {
match self {
- Side::Left | Side::Top => true,
- Side::Right | Side::Bottom => false,
+ HandleSide::Left | HandleSide::Top => true,
+ HandleSide::Right | HandleSide::Bottom => false,
}
}
@@ -55,14 +53,14 @@ impl Side {
fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
match self {
- Side::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
- Side::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
- Side::Bottom => {
+ HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
+ HandleSide::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
+ HandleSide::Bottom => {
let mut origin = bounds.lower_left();
origin.set_y(origin.y() - handle_size);
RectF::new(origin, vec2f(bounds.width(), handle_size))
}
- Side::Right => {
+ HandleSide::Right => {
let mut origin = bounds.upper_right();
origin.set_x(origin.x() - handle_size);
RectF::new(origin, vec2f(handle_size, bounds.height()))
@@ -71,69 +69,44 @@ impl Side {
}
}
-struct ResizeHandleState {
- actual_dimension: Cell<f32>,
- custom_dimension: Cell<f32>,
-}
-
pub struct Resizable<V: View> {
- side: Side,
- handle_size: f32,
child: AnyElement<V>,
- state: Rc<ResizeHandleState>,
- _state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
+ handle_side: HandleSide,
+ handle_size: f32,
+ on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>,
}
+const DEFAULT_HANDLE_SIZE: f32 = 4.0;
+
impl<V: View> Resizable<V> {
- pub fn new<Tag: 'static, T: View>(
+ pub fn new(
child: AnyElement<V>,
- element_id: usize,
- side: Side,
- handle_size: f32,
- initial_size: f32,
- cx: &mut ViewContext<V>,
+ handle_side: HandleSide,
+ size: f32,
+ on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
) -> Self {
- let state_handle = cx.element_state::<Tag, Rc<ResizeHandleState>>(
- element_id,
- Rc::new(ResizeHandleState {
- actual_dimension: Cell::new(initial_size),
- custom_dimension: Cell::new(initial_size),
- }),
- );
-
- let state = state_handle.read(cx).clone();
-
- let child = Hook::new({
- let constrained = ConstrainedBox::new(child);
- match side.axis() {
- Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()),
- Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()),
- }
- })
- .on_after_layout({
- let state = state.clone();
- move |size, _| {
- state.actual_dimension.set(side.relevant_component(size));
- }
- })
+ let child = match handle_side.axis() {
+ Axis::Horizontal => child.constrained().with_max_width(size),
+ Axis::Vertical => child.constrained().with_max_height(size),
+ }
.into_any();
Self {
- side,
child,
- handle_size,
- state,
- _state_handle: state_handle,
+ handle_side,
+ handle_size: DEFAULT_HANDLE_SIZE,
+ on_resize: Rc::new(RefCell::new(on_resize)),
}
}
- pub fn current_size(&self) -> f32 {
- self.state.actual_dimension.get()
+ pub fn with_handle_size(mut self, handle_size: f32) -> Self {
+ self.handle_size = handle_size;
+ self
}
}
impl<V: View> Element<V> for Resizable<V> {
- type LayoutState = ();
+ type LayoutState = SizeConstraint;
type PaintState = ();
fn layout(
@@ -142,7 +115,7 @@ impl<V: View> Element<V> for Resizable<V> {
view: &mut V,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
- (self.child.layout(constraint, view, cx), ())
+ (self.child.layout(constraint, view, cx), constraint)
}
fn paint(
@@ -150,34 +123,44 @@ impl<V: View> Element<V> for Resizable<V> {
scene: &mut SceneBuilder,
bounds: pathfinder_geometry::rect::RectF,
visible_bounds: pathfinder_geometry::rect::RectF,
- _child_size: &mut Self::LayoutState,
+ constraint: &mut SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
scene.push_stacking_context(None, None);
- let handle_region = self.side.of_rect(bounds, self.handle_size);
+ let handle_region = self.handle_side.of_rect(bounds, self.handle_size);
enum ResizeHandle {}
scene.push_mouse_region(
- MouseRegion::new::<ResizeHandle>(cx.view_id(), self.side as usize, handle_region)
- .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
- .on_drag(MouseButton::Left, {
- let state = self.state.clone();
- let side = self.side;
- move |e, _: &mut V, cx| {
- let prev_width = state.actual_dimension.get();
- state
- .custom_dimension
- .set(0f32.max(prev_width + side.compute_delta(e)).round());
- cx.notify();
+ MouseRegion::new::<ResizeHandle>(
+ cx.view_id(),
+ self.handle_side as usize,
+ handle_region,
+ )
+ .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
+ .on_drag(MouseButton::Left, {
+ let bounds = bounds.clone();
+ let side = self.handle_side;
+ let prev_size = side.relevant_component(bounds.size());
+ let min_size = side.relevant_component(constraint.min);
+ let max_size = side.relevant_component(constraint.max);
+ let on_resize = self.on_resize.clone();
+ move |event, view: &mut V, cx| {
+ let new_size = min_size
+ .max(prev_size + side.compute_delta(event))
+ .min(max_size)
+ .round();
+ if new_size != prev_size {
+ on_resize.borrow_mut()(view, new_size, cx);
}
- }),
+ }
+ }),
);
scene.push_cursor_region(crate::CursorRegion {
bounds: handle_region,
- style: match self.side.axis() {
+ style: match self.handle_side.axis() {
Axis::Horizontal => CursorStyle::ResizeLeftRight,
Axis::Vertical => CursorStyle::ResizeUpDown,
},
@@ -173,6 +173,7 @@ pub struct WindowOptions<'a> {
pub titlebar: Option<TitlebarOptions<'a>>,
pub center: bool,
pub focus: bool,
+ pub show: bool,
pub kind: WindowKind,
pub is_movable: bool,
pub screen: Option<Rc<dyn Screen>>,
@@ -222,21 +223,21 @@ impl Bind for WindowBounds {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let (region, next_index) = match self {
WindowBounds::Fullscreen => {
- let next_index = statement.bind("Fullscreen", start_index)?;
+ let next_index = statement.bind(&"Fullscreen", start_index)?;
(None, next_index)
}
WindowBounds::Maximized => {
- let next_index = statement.bind("Maximized", start_index)?;
+ let next_index = statement.bind(&"Maximized", start_index)?;
(None, next_index)
}
WindowBounds::Fixed(region) => {
- let next_index = statement.bind("Fixed", start_index)?;
+ let next_index = statement.bind(&"Fixed", start_index)?;
(Some(*region), next_index)
}
};
statement.bind(
- region.map(|region| {
+ ®ion.map(|region| {
(
region.min_x(),
region.min_y(),
@@ -376,6 +377,7 @@ impl<'a> Default for WindowOptions<'a> {
}),
center: false,
focus: true,
+ show: true,
kind: WindowKind::Normal,
is_movable: true,
screen: None,
@@ -614,7 +614,7 @@ impl Window {
}
if options.focus {
native_window.makeKeyAndOrderFront_(nil);
- } else {
+ } else if options.show {
native_window.orderFront_(nil);
}
@@ -10,6 +10,7 @@ doctest = false
[dependencies]
context_menu = { path = "../context_menu" }
+db = { path = "../db" }
drag_and_drop = { path = "../drag_and_drop" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
@@ -26,7 +27,6 @@ serde_derive.workspace = true
serde_json.workspace = true
anyhow.workspace = true
schemars.workspace = true
-
unicase = "2.6"
[dev-dependencies]
@@ -1,12 +1,13 @@
mod project_panel_settings;
use context_menu::{ContextMenu, ContextMenuItem};
+use db::kvp::KEY_VALUE_STORE;
use drag_and_drop::{DragAndDrop, Draggable};
use editor::{Cancel, Editor};
use futures::stream::StreamExt;
use gpui::{
actions,
- anyhow::{anyhow, Result},
+ anyhow::{self, anyhow, Result},
elements::{
AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler,
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
@@ -14,15 +15,17 @@ use gpui::{
geometry::vector::Vector2F,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton, PromptLevel},
- AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext,
- ViewHandle, WeakViewHandle,
+ AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelHandle, Task,
+ View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::{
- repository::GitFileStatus, Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree,
- WorktreeId,
+ repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath,
+ Worktree, WorktreeId,
};
-use project_panel_settings::ProjectPanelSettings;
+use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
+use serde::{Deserialize, Serialize};
+use settings::SettingsStore;
use std::{
cmp::Ordering,
collections::{hash_map, HashMap},
@@ -33,12 +36,18 @@ use std::{
};
use theme::ProjectPanelEntry;
use unicase::UniCase;
-use workspace::Workspace;
+use util::{ResultExt, TryFutureExt};
+use workspace::{
+ dock::{DockPosition, Panel},
+ Workspace,
+};
+const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
pub struct ProjectPanel {
project: ModelHandle<Project>,
+ fs: Arc<dyn Fs>,
list: UniformListState,
visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
last_worktree_root_id: Option<ProjectEntryId>,
@@ -50,6 +59,9 @@ pub struct ProjectPanel {
context_menu: ViewHandle<ContextMenu>,
dragged_entry_destination: Option<Arc<Path>>,
workspace: WeakViewHandle<Workspace>,
+ has_focus: bool,
+ width: Option<f32>,
+ pending_serialization: Task<Option<()>>,
}
#[derive(Copy, Clone)]
@@ -146,10 +158,17 @@ pub enum Event {
entry_id: ProjectEntryId,
focus_opened_item: bool,
},
+ DockPositionChanged,
+ Focus,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedProjectPanel {
+ width: Option<f32>,
}
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_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
cx.observe(&project, |this, _, cx| {
@@ -210,6 +229,7 @@ impl ProjectPanel {
let view_id = cx.view_id();
let mut this = Self {
project: project.clone(),
+ fs: workspace.app_state().fs.clone(),
list: Default::default(),
visible_entries: Default::default(),
last_worktree_root_id: Default::default(),
@@ -221,8 +241,23 @@ impl ProjectPanel {
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
dragged_entry_destination: None,
workspace: workspace.weak_handle(),
+ has_focus: false,
+ width: None,
+ pending_serialization: Task::ready(None),
};
this.update_visible_entries(None, cx);
+
+ // Update the dock position when the setting changes.
+ let mut old_dock_position = this.position(cx);
+ cx.observe_global::<SettingsStore, _>(move |this, cx| {
+ let new_dock_position = this.position(cx);
+ if new_dock_position != old_dock_position {
+ old_dock_position = new_dock_position;
+ cx.emit(Event::DockPositionChanged);
+ }
+ })
+ .detach();
+
this
});
@@ -254,6 +289,7 @@ impl ProjectPanel {
}
}
}
+ _ => {}
}
})
.detach();
@@ -261,6 +297,51 @@ impl ProjectPanel {
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(
&mut self,
position: Vector2F,
@@ -1352,16 +1433,103 @@ impl View for ProjectPanel {
Self::reset_to_default_keymap_context(keymap);
keymap.add_identifier("menu");
}
+
+ fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+ if !self.has_focus {
+ self.has_focus = true;
+ cx.emit(Event::Focus);
+ }
+ }
+
+ fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+ self.has_focus = false;
+ }
}
impl Entity for ProjectPanel {
type Event = Event;
}
-impl workspace::sidebar::SidebarItem for ProjectPanel {
- fn should_show_badge(&self, _: &AppContext) -> bool {
+impl workspace::dock::Panel for ProjectPanel {
+ fn position(&self, cx: &WindowContext) -> DockPosition {
+ match settings::get::<ProjectPanelSettings>(cx).dock {
+ ProjectPanelDockPosition::Left => DockPosition::Left,
+ ProjectPanelDockPosition::Right => DockPosition::Right,
+ }
+ }
+
+ fn position_is_valid(&self, position: DockPosition) -> bool {
+ matches!(position, DockPosition::Left | DockPosition::Right)
+ }
+
+ fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+ settings::update_settings_file::<ProjectPanelSettings>(
+ self.fs.clone(),
+ cx,
+ move |settings| {
+ let dock = match position {
+ DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
+ DockPosition::Right => ProjectPanelDockPosition::Right,
+ };
+ settings.dock = Some(dock);
+ },
+ );
+ }
+
+ fn size(&self, cx: &WindowContext) -> f32 {
+ 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 {
false
}
+
+ fn should_zoom_out_on_event(_: &Self::Event) -> bool {
+ false
+ }
+
+ fn is_zoomed(&self, _: &WindowContext) -> bool {
+ false
+ }
+
+ fn set_zoomed(&mut self, _: bool, _: &mut ViewContext<Self>) {}
+
+ fn set_active(&mut self, _: bool, _: &mut ViewContext<Self>) {}
+
+ fn icon_path(&self) -> &'static str {
+ "icons/folder_tree_16.svg"
+ }
+
+ fn icon_tooltip(&self) -> String {
+ "Project Panel".into()
+ }
+
+ fn should_change_position_on_event(event: &Self::Event) -> bool {
+ matches!(event, Event::DockPositionChanged)
+ }
+
+ fn should_activate_on_event(_: &Self::Event) -> bool {
+ false
+ }
+
+ fn should_close_on_event(_: &Self::Event) -> bool {
+ false
+ }
+
+ fn has_focus(&self, _: &WindowContext) -> bool {
+ self.has_focus
+ }
+
+ fn is_focus_event(event: &Self::Event) -> bool {
+ matches!(event, Event::Focus)
+ }
}
impl ClipboardEntry {
@@ -2059,6 +2227,7 @@ mod tests {
theme::init((), cx);
language::init(cx);
editor::init_settings(cx);
+ crate::init(cx);
workspace::init_settings(cx);
});
}
@@ -2072,6 +2241,7 @@ mod tests {
language::init(cx);
editor::init(cx);
pane::init(cx);
+ crate::init(cx);
workspace::init(app_state.clone(), cx);
});
}
@@ -3,14 +3,25 @@ use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Setting;
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ProjectPanelDockPosition {
+ Left,
+ Right,
+}
+
#[derive(Deserialize, Debug)]
pub struct ProjectPanelSettings {
pub git_status: bool,
+ pub dock: ProjectPanelDockPosition,
+ pub default_width: f32,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct ProjectPanelSettingsContent {
pub git_status: Option<bool>,
+ pub dock: Option<ProjectPanelDockPosition>,
+ pub default_width: Option<f32>,
}
impl Setting for ProjectPanelSettings {
@@ -27,7 +27,7 @@ impl StaticColumnCount for bool {}
impl Bind for bool {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
- .bind(self.then_some(1).unwrap_or(0), start_index)
+ .bind(&self.then_some(1).unwrap_or(0), start_index)
.with_context(|| format!("Failed to bind bool at index {start_index}"))
}
}
@@ -236,7 +236,7 @@ impl<'a> Statement<'a> {
Ok(str::from_utf8(slice)?)
}
- pub fn bind<T: Bind>(&self, value: T, index: i32) -> Result<i32> {
+ pub fn bind<T: Bind>(&self, value: &T, index: i32) -> Result<i32> {
debug_assert!(index > 0);
Ok(value.bind(self, index)?)
}
@@ -258,7 +258,7 @@ impl<'a> Statement<'a> {
}
}
- pub fn with_bindings(&mut self, bindings: impl Bind) -> Result<&mut Self> {
+ pub fn with_bindings(&mut self, bindings: &impl Bind) -> Result<&mut Self> {
self.bind(bindings, 1)?;
Ok(self)
}
@@ -464,7 +464,7 @@ mod test {
connection
.exec(indoc! {"
CREATE TABLE texts (
- text TEXT
+ text TEXT
)"})
.unwrap()()
.unwrap();
@@ -29,7 +29,7 @@ impl Connection {
query: &str,
) -> Result<impl 'a + FnMut(B) -> Result<()>> {
let mut statement = Statement::prepare(self, query)?;
- Ok(move |bindings| statement.with_bindings(bindings)?.exec())
+ Ok(move |bindings| statement.with_bindings(&bindings)?.exec())
}
/// Prepare a statement which has no bindings and returns a `Vec<C>`.
@@ -55,7 +55,7 @@ impl Connection {
query: &str,
) -> Result<impl 'a + FnMut(B) -> Result<Vec<C>>> {
let mut statement = Statement::prepare(self, query)?;
- Ok(move |bindings| statement.with_bindings(bindings)?.rows::<C>())
+ Ok(move |bindings| statement.with_bindings(&bindings)?.rows::<C>())
}
/// Prepare a statement that selects a single row from the database.
@@ -87,7 +87,7 @@ impl Connection {
let mut statement = Statement::prepare(self, query)?;
Ok(move |bindings| {
statement
- .with_bindings(bindings)
+ .with_bindings(&bindings)
.context("Bindings failed")?
.maybe_row::<C>()
.context("Maybe row failed")
@@ -119,6 +119,14 @@ pub fn init(cx: &mut AppContext) {
settings::register::<TerminalSettings>(cx);
}
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalDockPosition {
+ Left,
+ Bottom,
+ Right,
+}
+
#[derive(Deserialize)]
pub struct TerminalSettings {
pub shell: Shell,
@@ -132,6 +140,9 @@ pub struct TerminalSettings {
pub alternate_scroll: AlternateScroll,
pub option_as_meta: bool,
pub copy_on_select: bool,
+ pub dock: TerminalDockPosition,
+ pub default_width: f32,
+ pub default_height: f32,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -147,6 +158,9 @@ pub struct TerminalSettingsContent {
pub alternate_scroll: Option<AlternateScroll>,
pub option_as_meta: Option<bool>,
pub copy_on_select: Option<bool>,
+ pub dock: Option<TerminalDockPosition>,
+ pub default_width: Option<f32>,
+ pub default_height: Option<f32>,
}
impl TerminalSettings {
@@ -1,173 +0,0 @@
-use crate::TerminalView;
-use context_menu::{ContextMenu, ContextMenuItem};
-use gpui::{
- elements::*,
- platform::{CursorStyle, MouseButton},
- AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle,
-};
-use std::any::TypeId;
-use workspace::{
- dock::{Dock, FocusDock},
- item::ItemHandle,
- NewTerminal, StatusItemView, Workspace,
-};
-
-pub struct TerminalButton {
- workspace: WeakViewHandle<Workspace>,
- popup_menu: ViewHandle<ContextMenu>,
-}
-
-impl Entity for TerminalButton {
- type Event = ();
-}
-
-impl View for TerminalButton {
- fn ui_name() -> &'static str {
- "TerminalButton"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let workspace = self.workspace.upgrade(cx);
- let project = match workspace {
- Some(workspace) => workspace.read(cx).project().read(cx),
- None => return Empty::new().into_any(),
- };
-
- let focused_view = cx.focused_view_id();
- let active = focused_view
- .map(|view_id| {
- cx.view_type_id(cx.window_id(), view_id) == Some(TypeId::of::<TerminalView>())
- })
- .unwrap_or(false);
-
- let has_terminals = !project.local_terminal_handles().is_empty();
- let terminal_count = project.local_terminal_handles().len() as i32;
- let theme = theme::current(cx).clone();
-
- Stack::new()
- .with_child(
- MouseEventHandler::<Self, _>::new(0, cx, {
- let theme = theme.clone();
- move |state, _cx| {
- let style = theme
- .workspace
- .status_bar
- .sidebar_buttons
- .item
- .style_for(state, active);
-
- Flex::row()
- .with_child(
- Svg::new("icons/terminal_12.svg")
- .with_color(style.icon_color)
- .constrained()
- .with_width(style.icon_size)
- .aligned()
- .into_any_named("terminals-icon"),
- )
- .with_children(has_terminals.then(|| {
- Label::new(terminal_count.to_string(), style.label.text.clone())
- .contained()
- .with_style(style.label.container)
- .aligned()
- }))
- .constrained()
- .with_height(style.icon_size)
- .contained()
- .with_style(style.container)
- }
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if has_terminals {
- this.deploy_terminal_menu(cx);
- } else {
- if !active {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- Dock::focus_dock(workspace, &Default::default(), cx)
- })
- }
- }
- };
- })
- .with_tooltip::<Self>(
- 0,
- "Show Terminal".into(),
- Some(Box::new(FocusDock)),
- theme.tooltip.clone(),
- cx,
- ),
- )
- .with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right())
- .into_any_named("terminal button")
- }
-}
-
-impl TerminalButton {
- pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
- let button_view_id = cx.view_id();
- cx.observe(&workspace, |_, _, cx| cx.notify()).detach();
- Self {
- workspace: workspace.downgrade(),
- popup_menu: cx.add_view(|cx| {
- let mut menu = ContextMenu::new(button_view_id, cx);
- menu.set_position_mode(OverlayPositionMode::Local);
- menu
- }),
- }
- }
-
- pub fn deploy_terminal_menu(&mut self, cx: &mut ViewContext<Self>) {
- let mut menu_options = vec![ContextMenuItem::action("New Terminal", NewTerminal)];
-
- if let Some(workspace) = self.workspace.upgrade(cx) {
- let project = workspace.read(cx).project().read(cx);
- let local_terminal_handles = project.local_terminal_handles();
-
- if !local_terminal_handles.is_empty() {
- menu_options.push(ContextMenuItem::Separator)
- }
-
- for local_terminal_handle in local_terminal_handles {
- if let Some(terminal) = local_terminal_handle.upgrade(cx) {
- let workspace = self.workspace.clone();
- let local_terminal_handle = local_terminal_handle.clone();
- menu_options.push(ContextMenuItem::handler(
- terminal.read(cx).title(),
- move |cx| {
- if let Some(workspace) = workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- let terminal = workspace
- .items_of_type::<TerminalView>(cx)
- .find(|terminal| {
- terminal.read(cx).model().downgrade()
- == local_terminal_handle
- });
- if let Some(terminal) = terminal {
- workspace.activate_item(&terminal, cx);
- }
- });
- }
- },
- ))
- }
- }
- }
-
- self.popup_menu.update(cx, |menu, cx| {
- menu.show(
- Default::default(),
- AnchorCorner::BottomRight,
- menu_options,
- cx,
- );
- });
- }
-}
-
-impl StatusItemView for TerminalButton {
- fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
- cx.notify();
- }
-}
@@ -0,0 +1,389 @@
+use std::sync::Arc;
+
+use crate::TerminalView;
+use db::kvp::KEY_VALUE_STORE;
+use gpui::{
+ actions, anyhow::Result, elements::*, serde_json, AppContext, AsyncAppContext, Entity,
+ Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+};
+use project::Fs;
+use serde::{Deserialize, Serialize};
+use settings::SettingsStore;
+use terminal::{TerminalDockPosition, TerminalSettings};
+use util::{ResultExt, TryFutureExt};
+use workspace::{
+ dock::{DockPosition, Panel},
+ item::Item,
+ pane, DraggedItem, Pane, Workspace,
+};
+
+const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
+
+actions!(terminal_panel, [ToggleFocus]);
+
+pub fn init(cx: &mut AppContext) {
+ cx.add_action(TerminalPanel::add_terminal);
+}
+
+pub enum Event {
+ Close,
+ DockPositionChanged,
+ ZoomIn,
+ ZoomOut,
+ Focus,
+}
+
+pub struct TerminalPanel {
+ pane: ViewHandle<Pane>,
+ fs: Arc<dyn Fs>,
+ workspace: WeakViewHandle<Workspace>,
+ width: Option<f32>,
+ height: Option<f32>,
+ pending_serialization: Task<Option<()>>,
+ _subscriptions: Vec<Subscription>,
+}
+
+impl TerminalPanel {
+ fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
+ let weak_self = cx.weak_handle();
+ let pane = cx.add_view(|cx| {
+ let window_id = cx.window_id();
+ let mut pane = Pane::new(
+ workspace.weak_handle(),
+ workspace.app_state().background_actions,
+ Default::default(),
+ cx,
+ );
+ pane.set_can_split(false, cx);
+ pane.on_can_drop(move |drag_and_drop, cx| {
+ drag_and_drop
+ .currently_dragged::<DraggedItem>(window_id)
+ .map_or(false, |(_, item)| {
+ item.handle.act_as::<TerminalView>(cx).is_some()
+ })
+ });
+ pane.set_render_tab_bar_buttons(cx, move |_, cx| {
+ let this = weak_self.clone();
+ Pane::render_tab_bar_button(
+ 0,
+ "icons/plus_12.svg",
+ cx,
+ move |_, cx| {
+ let this = this.clone();
+ cx.window_context().defer(move |cx| {
+ if let Some(this) = this.upgrade(cx) {
+ this.update(cx, |this, cx| {
+ this.add_terminal(&Default::default(), cx);
+ });
+ }
+ })
+ },
+ None,
+ )
+ });
+ pane
+ });
+ let subscriptions = vec![
+ cx.observe(&pane, |_, _, cx| cx.notify()),
+ cx.subscribe(&pane, Self::handle_pane_event),
+ ];
+ let this = Self {
+ pane,
+ fs: workspace.app_state().fs.clone(),
+ workspace: workspace.weak_handle(),
+ pending_serialization: Task::ready(None),
+ width: None,
+ height: None,
+ _subscriptions: subscriptions,
+ };
+ let mut old_dock_position = this.position(cx);
+ cx.observe_global::<SettingsStore, _>(move |this, cx| {
+ let new_dock_position = this.position(cx);
+ if new_dock_position != old_dock_position {
+ old_dock_position = new_dock_position;
+ cx.emit(Event::DockPositionChanged);
+ }
+ })
+ .detach();
+ this
+ }
+
+ 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(TERMINAL_PANEL_KEY) })
+ .await
+ .log_err()
+ .flatten()
+ {
+ Some(serde_json::from_str::<SerializedTerminalPanel>(&panel)?)
+ } else {
+ None
+ };
+ let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
+ let panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx));
+ let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
+ panel.update(cx, |panel, cx| {
+ cx.notify();
+ panel.height = serialized_panel.height;
+ panel.width = serialized_panel.width;
+ panel.pane.update(cx, |_, cx| {
+ serialized_panel
+ .items
+ .iter()
+ .map(|item_id| {
+ TerminalView::deserialize(
+ workspace.project().clone(),
+ workspace.weak_handle(),
+ workspace.database_id(),
+ *item_id,
+ cx,
+ )
+ })
+ .collect::<Vec<_>>()
+ })
+ })
+ } else {
+ Default::default()
+ };
+ let pane = panel.read(cx).pane.clone();
+ (panel, pane, items)
+ })?;
+
+ let items = futures::future::join_all(items).await;
+ workspace.update(&mut cx, |workspace, cx| {
+ let active_item_id = serialized_panel
+ .as_ref()
+ .and_then(|panel| panel.active_item_id);
+ let mut active_ix = None;
+ for item in items {
+ if let Some(item) = item.log_err() {
+ let item_id = item.id();
+ Pane::add_item(workspace, &pane, Box::new(item), false, false, None, cx);
+ if Some(item_id) == active_item_id {
+ active_ix = Some(pane.read(cx).items_len() - 1);
+ }
+ }
+ }
+
+ if let Some(active_ix) = active_ix {
+ pane.update(cx, |pane, cx| {
+ pane.activate_item(active_ix, false, false, cx)
+ });
+ }
+ })?;
+
+ Ok(panel)
+ })
+ }
+
+ fn handle_pane_event(
+ &mut self,
+ _pane: ViewHandle<Pane>,
+ event: &pane::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ match event {
+ pane::Event::ActivateItem { .. } => self.serialize(cx),
+ pane::Event::RemoveItem { .. } => self.serialize(cx),
+ pane::Event::Remove => cx.emit(Event::Close),
+ pane::Event::ZoomIn => cx.emit(Event::ZoomIn),
+ pane::Event::ZoomOut => cx.emit(Event::ZoomOut),
+ pane::Event::Focus => cx.emit(Event::Focus),
+ _ => {}
+ }
+ }
+
+ fn add_terminal(&mut self, _: &workspace::NewTerminal, cx: &mut ViewContext<Self>) {
+ let workspace = self.workspace.clone();
+ cx.spawn(|this, mut cx| async move {
+ let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
+ workspace.update(&mut cx, |workspace, cx| {
+ let working_directory_strategy = settings::get::<TerminalSettings>(cx)
+ .working_directory
+ .clone();
+ let working_directory =
+ crate::get_working_directory(workspace, cx, working_directory_strategy);
+ let window_id = cx.window_id();
+ if let Some(terminal) = workspace.project().update(cx, |project, cx| {
+ project
+ .create_terminal(working_directory, window_id, cx)
+ .log_err()
+ }) {
+ let terminal =
+ Box::new(cx.add_view(|cx| {
+ TerminalView::new(terminal, workspace.database_id(), cx)
+ }));
+ Pane::add_item(workspace, &pane, terminal, true, true, None, cx);
+ }
+ })?;
+ this.update(&mut cx, |this, cx| this.serialize(cx))?;
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+ let items = self
+ .pane
+ .read(cx)
+ .items()
+ .map(|item| item.id())
+ .collect::<Vec<_>>();
+ 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(
+ async move {
+ KEY_VALUE_STORE
+ .write_kvp(
+ TERMINAL_PANEL_KEY.into(),
+ serde_json::to_string(&SerializedTerminalPanel {
+ items,
+ active_item_id,
+ height,
+ width,
+ })?,
+ )
+ .await?;
+ anyhow::Ok(())
+ }
+ .log_err(),
+ );
+ }
+}
+
+impl Entity for TerminalPanel {
+ type Event = Event;
+}
+
+impl View for TerminalPanel {
+ fn ui_name() -> &'static str {
+ "TerminalPanel"
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
+ ChildView::new(&self.pane, cx).into_any()
+ }
+
+ fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+ if cx.is_self_focused() {
+ cx.focus(&self.pane);
+ }
+ }
+}
+
+impl Panel for TerminalPanel {
+ fn position(&self, cx: &WindowContext) -> DockPosition {
+ match settings::get::<TerminalSettings>(cx).dock {
+ TerminalDockPosition::Left => DockPosition::Left,
+ TerminalDockPosition::Bottom => DockPosition::Bottom,
+ TerminalDockPosition::Right => DockPosition::Right,
+ }
+ }
+
+ fn position_is_valid(&self, _: DockPosition) -> bool {
+ true
+ }
+
+ fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+ settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
+ let dock = match position {
+ DockPosition::Left => TerminalDockPosition::Left,
+ DockPosition::Bottom => TerminalDockPosition::Bottom,
+ DockPosition::Right => TerminalDockPosition::Right,
+ };
+ settings.dock = Some(dock);
+ });
+ }
+
+ fn size(&self, cx: &WindowContext) -> f32 {
+ let settings = settings::get::<TerminalSettings>(cx);
+ match self.position(cx) {
+ DockPosition::Left | DockPosition::Right => {
+ 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 {
+ matches!(event, Event::ZoomIn)
+ }
+
+ fn should_zoom_out_on_event(event: &Event) -> bool {
+ matches!(event, Event::ZoomOut)
+ }
+
+ fn is_zoomed(&self, cx: &WindowContext) -> bool {
+ self.pane.read(cx).is_zoomed()
+ }
+
+ fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
+ self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
+ }
+
+ fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+ if active && self.pane.read(cx).items_len() == 0 {
+ self.add_terminal(&Default::default(), cx)
+ }
+ }
+
+ fn icon_path(&self) -> &'static str {
+ "icons/terminal_12.svg"
+ }
+
+ fn icon_tooltip(&self) -> String {
+ "Terminals".to_string()
+ }
+
+ fn icon_label(&self, cx: &WindowContext) -> Option<String> {
+ let count = self.pane.read(cx).items_len();
+ if count == 0 {
+ None
+ } else {
+ Some(count.to_string())
+ }
+ }
+
+ fn should_change_position_on_event(event: &Self::Event) -> bool {
+ matches!(event, Event::DockPositionChanged)
+ }
+
+ fn should_activate_on_event(_: &Self::Event) -> bool {
+ false
+ }
+
+ fn should_close_on_event(event: &Event) -> bool {
+ matches!(event, Event::Close)
+ }
+
+ fn has_focus(&self, cx: &WindowContext) -> bool {
+ self.pane.read(cx).has_focus()
+ }
+
+ fn is_focus_event(event: &Self::Event) -> bool {
+ matches!(event, Event::Focus)
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedTerminalPanel {
+ items: Vec<usize>,
+ active_item_id: Option<usize>,
+ width: Option<f32>,
+ height: Option<f32>,
+}
@@ -1,6 +1,6 @@
mod persistence;
-pub mod terminal_button;
pub mod terminal_element;
+pub mod terminal_panel;
use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
use context_menu::{ContextMenu, ContextMenuItem};
@@ -63,6 +63,7 @@ actions!(
impl_actions!(terminal, [SendText, SendKeystroke]);
pub fn init(cx: &mut AppContext) {
+ terminal_panel::init(cx);
terminal::init(cx);
cx.add_action(TerminalView::deploy);
@@ -82,19 +82,20 @@ pub struct Workspace {
pub pane_divider: Border,
pub leader_border_opacity: f32,
pub leader_border_width: f32,
- pub sidebar: Sidebar,
+ pub dock: Dock,
pub status_bar: StatusBar,
pub toolbar: Toolbar,
pub breadcrumb_height: f32,
pub breadcrumbs: Interactive<ContainedText>,
pub disconnected_overlay: ContainedText,
pub modal: ContainerStyle,
+ pub zoomed_foreground: ContainerStyle,
+ pub zoomed_background: ContainerStyle,
pub notification: ContainerStyle,
pub notifications: Notifications,
pub joining_project_avatar: ImageStyle,
pub joining_project_message: ContainedText,
pub external_location_message: ContainedText,
- pub dock: Dock,
pub drop_target_overlay_color: Color,
}
@@ -317,15 +318,6 @@ pub struct Toolbar {
pub nav_button: Interactive<IconButton>,
}
-#[derive(Clone, Deserialize, Default)]
-pub struct Dock {
- pub initial_size_right: f32,
- pub initial_size_bottom: f32,
- pub wash_color: Color,
- pub panel: ContainerStyle,
- pub maximized: ContainerStyle,
-}
-
#[derive(Clone, Deserialize, Default)]
pub struct Notifications {
#[serde(flatten)]
@@ -369,17 +361,17 @@ pub struct StatusBar {
pub auto_update_progress_message: TextStyle,
pub auto_update_done_message: TextStyle,
pub lsp_status: Interactive<StatusBarLspStatus>,
- pub sidebar_buttons: StatusBarSidebarButtons,
+ pub panel_buttons: StatusBarPanelButtons,
pub diagnostic_summary: Interactive<StatusBarDiagnosticSummary>,
pub diagnostic_message: Interactive<ContainedText>,
}
#[derive(Deserialize, Default)]
-pub struct StatusBarSidebarButtons {
+pub struct StatusBarPanelButtons {
pub group_left: ContainerStyle,
+ pub group_bottom: ContainerStyle,
pub group_right: ContainerStyle,
- pub item: Interactive<SidebarItem>,
- pub badge: ContainerStyle,
+ pub button: Interactive<PanelButton>,
}
#[derive(Deserialize, Default)]
@@ -409,14 +401,14 @@ pub struct StatusBarLspStatus {
}
#[derive(Deserialize, Default)]
-pub struct Sidebar {
- pub initial_size: f32,
- #[serde(flatten)]
- pub container: ContainerStyle,
+pub struct Dock {
+ pub left: ContainerStyle,
+ pub bottom: ContainerStyle,
+ pub right: ContainerStyle,
}
#[derive(Clone, Deserialize, Default)]
-pub struct SidebarItem {
+pub struct PanelButton {
#[serde(flatten)]
pub container: ContainerStyle,
pub icon_color: Color,
@@ -11,7 +11,7 @@ use gpui::{
use settings::{update_settings_file, SettingsStore};
use std::{borrow::Cow, sync::Arc};
use workspace::{
- item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
+ dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
WorkspaceId,
};
@@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) {
pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
open_new(&app_state, cx, |workspace, cx| {
- workspace.toggle_sidebar(SidebarSide::Left, cx);
+ workspace.toggle_dock(DockPosition::Left, cx);
let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
cx.focus(&welcome_page);
@@ -1,826 +1,707 @@
-mod toggle_dock_button;
-
-use crate::{
- sidebar::SidebarSide, BackgroundActions, DockAnchor, ItemHandle, Pane, Workspace,
- WorkspaceSettings,
-};
-use collections::HashMap;
+use crate::{StatusItemView, Workspace};
+use context_menu::{ContextMenu, ContextMenuItem};
use gpui::{
- actions,
- elements::{ChildView, Empty, MouseEventHandler, ParentElement, Side, Stack},
- geometry::vector::Vector2F,
- platform::{CursorStyle, MouseButton},
- AnyElement, AppContext, Border, Element, SizeConstraint, ViewContext, ViewHandle,
+ elements::*, impl_actions, platform::CursorStyle, platform::MouseButton, AnyViewHandle,
+ AppContext, Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
+ WindowContext,
};
-use std::sync::{atomic::AtomicUsize, Arc};
-use theme::Theme;
-pub use toggle_dock_button::ToggleDockButton;
-
-actions!(
- dock,
- [
- FocusDock,
- HideDock,
- AnchorDockRight,
- AnchorDockBottom,
- ExpandDock,
- AddTabToDock,
- RemoveTabFromDock,
- ]
-);
-
-pub fn init(cx: &mut AppContext) {
- cx.add_action(Dock::focus_dock);
- cx.add_action(Dock::hide_dock);
- cx.add_action(
- |workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext<Workspace>| {
- Dock::move_dock(workspace, DockAnchor::Right, true, cx);
- },
- );
- cx.add_action(
- |workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext<Workspace>| {
- Dock::move_dock(workspace, DockAnchor::Bottom, true, cx)
- },
- );
- cx.add_action(
- |workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext<Workspace>| {
- Dock::move_dock(workspace, DockAnchor::Expanded, true, cx)
- },
- );
- cx.add_action(
- |workspace: &mut Workspace, _: &AddTabToDock, cx: &mut ViewContext<Workspace>| {
- if let Some(active_item) = workspace.active_item(cx) {
- let item_id = active_item.id();
-
- let from = workspace.active_pane();
- let to = workspace.dock_pane();
- if from.id() == to.id() {
- return;
- }
+use serde::Deserialize;
+use std::rc::Rc;
+use theme::ThemeSettings;
+
+pub trait Panel: View {
+ fn position(&self, cx: &WindowContext) -> DockPosition;
+ fn position_is_valid(&self, position: DockPosition) -> bool;
+ fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
+ 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_tooltip(&self) -> String;
+ fn icon_label(&self, _: &WindowContext) -> Option<String> {
+ None
+ }
+ fn should_change_position_on_event(_: &Self::Event) -> bool;
+ fn should_zoom_in_on_event(_: &Self::Event) -> bool;
+ fn should_zoom_out_on_event(_: &Self::Event) -> bool;
+ fn is_zoomed(&self, cx: &WindowContext) -> bool;
+ fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>);
+ fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>);
+ fn should_activate_on_event(_: &Self::Event) -> bool;
+ fn should_close_on_event(_: &Self::Event) -> bool;
+ fn has_focus(&self, cx: &WindowContext) -> bool;
+ fn is_focus_event(_: &Self::Event) -> bool;
+}
- let destination_index = to.read(cx).items_len() + 1;
+pub trait PanelHandle {
+ fn id(&self) -> usize;
+ fn position(&self, cx: &WindowContext) -> DockPosition;
+ fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
+ fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
+ fn is_zoomed(&self, cx: &WindowContext) -> bool;
+ fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
+ fn set_active(&self, active: bool, cx: &mut WindowContext);
+ 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_tooltip(&self, cx: &WindowContext) -> String;
+ fn icon_label(&self, cx: &WindowContext) -> Option<String>;
+ fn has_focus(&self, cx: &WindowContext) -> bool;
+ fn as_any(&self) -> &AnyViewHandle;
+}
- Pane::move_item(
- workspace,
- from.clone(),
- to.clone(),
- item_id,
- destination_index,
- cx,
- );
- }
- },
- );
- cx.add_action(
- |workspace: &mut Workspace, _: &RemoveTabFromDock, cx: &mut ViewContext<Workspace>| {
- if let Some(active_item) = workspace.active_item(cx) {
- let item_id = active_item.id();
-
- let from = workspace.dock_pane();
- let to = workspace
- .last_active_center_pane
- .as_ref()
- .and_then(|pane| pane.upgrade(cx))
- .unwrap_or_else(|| {
- workspace
- .panes
- .first()
- .expect("There must be a pane")
- .clone()
- });
-
- if from.id() == to.id() {
- return;
- }
+impl<T> PanelHandle for ViewHandle<T>
+where
+ T: Panel,
+{
+ fn id(&self) -> usize {
+ self.id()
+ }
- let destination_index = to.read(cx).items_len() + 1;
+ fn position(&self, cx: &WindowContext) -> DockPosition {
+ self.read(cx).position(cx)
+ }
- Pane::move_item(
- workspace,
- from.clone(),
- to.clone(),
- item_id,
- destination_index,
- cx,
- );
- }
- },
- );
-}
+ fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool {
+ self.read(cx).position_is_valid(position)
+ }
-#[derive(Copy, Clone, PartialEq, Eq, Debug)]
-pub enum DockPosition {
- Shown(DockAnchor),
- Hidden(DockAnchor),
-}
+ fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {
+ self.update(cx, |this, cx| this.set_position(position, cx))
+ }
+
+ fn size(&self, cx: &WindowContext) -> f32 {
+ 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 {
+ self.read(cx).is_zoomed(cx)
+ }
-impl Default for DockPosition {
- fn default() -> Self {
- DockPosition::Hidden(Default::default())
+ fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) {
+ self.update(cx, |this, cx| this.set_zoomed(zoomed, cx))
+ }
+
+ fn set_active(&self, active: bool, cx: &mut WindowContext) {
+ self.update(cx, |this, cx| this.set_active(active, cx))
+ }
+
+ fn icon_path(&self, cx: &WindowContext) -> &'static str {
+ self.read(cx).icon_path()
+ }
+
+ fn icon_tooltip(&self, cx: &WindowContext) -> String {
+ self.read(cx).icon_tooltip()
+ }
+
+ fn icon_label(&self, cx: &WindowContext) -> Option<String> {
+ self.read(cx).icon_label(cx)
+ }
+
+ fn has_focus(&self, cx: &WindowContext) -> bool {
+ self.read(cx).has_focus(cx)
+ }
+
+ fn as_any(&self) -> &AnyViewHandle {
+ self
}
}
-pub fn icon_for_dock_anchor(anchor: DockAnchor) -> &'static str {
- match anchor {
- DockAnchor::Right => "icons/dock_right_12.svg",
- DockAnchor::Bottom => "icons/dock_bottom_12.svg",
- DockAnchor::Expanded => "icons/dock_modal_12.svg",
+impl From<&dyn PanelHandle> for AnyViewHandle {
+ fn from(val: &dyn PanelHandle) -> Self {
+ val.as_any().clone()
}
}
+pub struct Dock {
+ position: DockPosition,
+ panel_entries: Vec<PanelEntry>,
+ is_open: bool,
+ active_panel_index: usize,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
+pub enum DockPosition {
+ Left,
+ Bottom,
+ Right,
+}
+
impl DockPosition {
- pub fn is_visible(&self) -> bool {
+ fn to_label(&self) -> &'static str {
match self {
- DockPosition::Shown(_) => true,
- DockPosition::Hidden(_) => false,
+ Self::Left => "left",
+ Self::Bottom => "bottom",
+ Self::Right => "right",
}
}
- pub fn anchor(&self) -> DockAnchor {
+ fn to_resize_handle_side(self) -> HandleSide {
match self {
- DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor,
+ Self::Left => HandleSide::Right,
+ Self::Bottom => HandleSide::Top,
+ Self::Right => HandleSide::Left,
}
}
- fn hide(self) -> Self {
+ pub fn axis(&self) -> Axis {
match self {
- DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
- DockPosition::Hidden(_) => self,
+ Self::Left | Self::Right => Axis::Horizontal,
+ Self::Bottom => Axis::Vertical,
}
}
+}
- fn show(self) -> Self {
- match self {
- DockPosition::Hidden(anchor) => DockPosition::Shown(anchor),
- DockPosition::Shown(_) => self,
- }
- }
+struct PanelEntry {
+ panel: Rc<dyn PanelHandle>,
+ context_menu: ViewHandle<ContextMenu>,
+ _subscriptions: [Subscription; 2],
}
-pub type DockDefaultItemFactory =
- fn(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Box<dyn ItemHandle>>;
+pub struct PanelButtons {
+ dock: ViewHandle<Dock>,
+ workspace: WeakViewHandle<Workspace>,
+}
-pub struct Dock {
- position: DockPosition,
- panel_sizes: HashMap<DockAnchor, f32>,
- pane: ViewHandle<Pane>,
- default_item_factory: DockDefaultItemFactory,
+#[derive(Clone, Debug, Deserialize, PartialEq)]
+pub struct TogglePanel {
+ pub dock_position: DockPosition,
+ pub panel_index: usize,
}
-impl Dock {
- pub fn new(
- default_item_factory: DockDefaultItemFactory,
- background_actions: BackgroundActions,
- pane_history_timestamp: Arc<AtomicUsize>,
- cx: &mut ViewContext<Workspace>,
- ) -> Self {
- let position =
- DockPosition::Hidden(settings::get::<WorkspaceSettings>(cx).default_dock_anchor);
- let workspace = cx.weak_handle();
- let pane = cx.add_view(|cx| {
- Pane::new(
- workspace,
- Some(position.anchor()),
- background_actions,
- pane_history_timestamp,
- cx,
- )
- });
- pane.update(cx, |pane, cx| {
- pane.set_active(false, cx);
- });
- cx.subscribe(&pane, Workspace::handle_pane_event).detach();
+impl_actions!(workspace, [TogglePanel]);
+impl Dock {
+ pub fn new(position: DockPosition) -> Self {
Self {
- pane,
- panel_sizes: Default::default(),
position,
- default_item_factory,
+ panel_entries: Default::default(),
+ active_panel_index: 0,
+ is_open: false,
}
}
- pub fn pane(&self) -> &ViewHandle<Pane> {
- &self.pane
+ pub fn is_open(&self) -> bool {
+ self.is_open
}
- pub fn visible_pane(&self) -> Option<&ViewHandle<Pane>> {
- self.position.is_visible().then(|| self.pane())
+ pub fn has_focus(&self, cx: &WindowContext) -> bool {
+ self.active_panel()
+ .map_or(false, |panel| panel.has_focus(cx))
}
- pub fn is_anchored_at(&self, anchor: DockAnchor) -> bool {
- self.position.is_visible() && self.position.anchor() == anchor
+ pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
+ self.panel_entries
+ .iter()
+ .position(|entry| entry.panel.as_any().is::<T>())
}
- pub(crate) fn set_dock_position(
- workspace: &mut Workspace,
- new_position: DockPosition,
- focus: bool,
- cx: &mut ViewContext<Workspace>,
- ) {
- workspace.dock.position = new_position;
- // Tell the pane about the new anchor position
- workspace.dock.pane.update(cx, |pane, cx| {
- pane.set_docked(Some(new_position.anchor()), cx)
- });
+ pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option<usize> {
+ self.panel_entries.iter().position(|entry| {
+ let panel = entry.panel.as_any();
+ cx.view_ui_name(panel.window_id(), panel.id()) == Some(ui_name)
+ })
+ }
- if workspace.dock.position.is_visible() {
- // Close the right sidebar if the dock is on the right side and the right sidebar is open
- if workspace.dock.position.anchor() == DockAnchor::Right {
- if workspace.right_sidebar().read(cx).is_open() {
- workspace.toggle_sidebar(SidebarSide::Right, cx);
- }
+ pub fn active_panel_index(&self) -> usize {
+ self.active_panel_index
+ }
+
+ pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
+ if open != self.is_open {
+ self.is_open = open;
+ if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
+ active_panel.panel.set_active(open, cx);
}
- // Ensure that the pane has at least one item or construct a default item to put in it
- let pane = workspace.dock.pane.clone();
- if pane.read(cx).items().next().is_none() {
- if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) {
- Pane::add_item(workspace, &pane, item_to_add, focus, focus, None, cx);
- } else {
- workspace.dock.position = workspace.dock.position.hide();
- }
- } else {
- if focus {
- cx.focus(&pane);
+ cx.notify();
+ }
+ }
+
+ pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
+ self.set_open(!self.is_open, cx);
+ cx.notify();
+ }
+
+ pub fn set_panel_zoomed(
+ &mut self,
+ panel: &AnyViewHandle,
+ zoomed: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
+ for entry in &mut self.panel_entries {
+ if entry.panel.as_any() == panel {
+ if zoomed != entry.panel.is_zoomed(cx) {
+ entry.panel.set_zoomed(zoomed, cx);
}
- }
- } else if let Some(last_active_center_pane) = workspace
- .last_active_center_pane
- .as_ref()
- .and_then(|pane| pane.upgrade(cx))
- {
- if focus {
- cx.focus(&last_active_center_pane);
+ } else if entry.panel.is_zoomed(cx) {
+ entry.panel.set_zoomed(false, cx);
}
}
- cx.emit(crate::Event::DockAnchorChanged);
- workspace.serialize_workspace(cx);
+
cx.notify();
}
- pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
- Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
+ pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
+ for entry in &mut self.panel_entries {
+ if entry.panel.is_zoomed(cx) {
+ entry.panel.set_zoomed(false, cx);
+ }
+ }
}
- pub fn show(workspace: &mut Workspace, focus: bool, cx: &mut ViewContext<Workspace>) {
- Self::set_dock_position(workspace, workspace.dock.position.show(), focus, cx);
+ pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
+ let subscriptions = [
+ cx.observe(&panel, |_, _, cx| cx.notify()),
+ cx.subscribe(&panel, |this, panel, event, cx| {
+ if T::should_activate_on_event(event) {
+ if let Some(ix) = this
+ .panel_entries
+ .iter()
+ .position(|entry| entry.panel.id() == panel.id())
+ {
+ this.set_open(true, cx);
+ this.activate_panel(ix, cx);
+ cx.focus(&panel);
+ }
+ } else if T::should_close_on_event(event)
+ && this.active_panel().map_or(false, |p| p.id() == panel.id())
+ {
+ this.set_open(false, cx);
+ }
+ }),
+ ];
+
+ let dock_view_id = cx.view_id();
+ self.panel_entries.push(PanelEntry {
+ panel: Rc::new(panel),
+ context_menu: cx.add_view(|cx| {
+ let mut menu = ContextMenu::new(dock_view_id, cx);
+ menu.set_position_mode(OverlayPositionMode::Local);
+ menu
+ }),
+ _subscriptions: subscriptions,
+ });
+ cx.notify()
}
- pub fn hide_on_sidebar_shown(
- workspace: &mut Workspace,
- sidebar_side: SidebarSide,
- cx: &mut ViewContext<Workspace>,
- ) {
- if (sidebar_side == SidebarSide::Right && workspace.dock.is_anchored_at(DockAnchor::Right))
- || workspace.dock.is_anchored_at(DockAnchor::Expanded)
+ pub fn remove_panel<T: Panel>(&mut self, panel: &ViewHandle<T>, cx: &mut ViewContext<Self>) {
+ if let Some(panel_ix) = self
+ .panel_entries
+ .iter()
+ .position(|entry| entry.panel.id() == panel.id())
{
- Self::hide(workspace, cx);
+ if panel_ix == self.active_panel_index {
+ self.active_panel_index = 0;
+ self.set_open(false, cx);
+ } else if panel_ix < self.active_panel_index {
+ self.active_panel_index -= 1;
+ }
+ self.panel_entries.remove(panel_ix);
+ cx.notify();
}
}
- pub fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
- Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx);
+ pub fn panels_len(&self) -> usize {
+ self.panel_entries.len()
}
- pub fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
- Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
+ pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
+ if panel_ix != self.active_panel_index {
+ if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
+ active_panel.panel.set_active(false, cx);
+ }
+
+ self.active_panel_index = panel_ix;
+ if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
+ active_panel.panel.set_active(true, cx);
+ }
+
+ cx.notify();
+ }
}
- pub fn move_dock(
- workspace: &mut Workspace,
- new_anchor: DockAnchor,
- focus: bool,
- cx: &mut ViewContext<Workspace>,
- ) {
- Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), focus, cx);
- }
-
- pub fn render(
- &self,
- theme: &Theme,
- anchor: DockAnchor,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<AnyElement<Workspace>> {
- let style = &theme.workspace.dock;
-
- self.position
- .is_visible()
- .then(|| self.position.anchor())
- .filter(|current_anchor| *current_anchor == anchor)
- .map(|anchor| match anchor {
- DockAnchor::Bottom | DockAnchor::Right => {
- let mut panel_style = style.panel.clone();
- let (resize_side, initial_size) = if anchor == DockAnchor::Bottom {
- panel_style.border = Border {
- top: true,
- bottom: false,
- left: false,
- right: false,
- ..panel_style.border
- };
+ pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
+ let entry = self.active_entry()?;
+ Some(&entry.panel)
+ }
- (Side::Top, style.initial_size_bottom)
- } else {
- panel_style.border = Border {
- top: false,
- bottom: false,
- left: true,
- right: false,
- ..panel_style.border
- };
- (Side::Left, style.initial_size_right)
- };
-
- enum DockResizeHandle {}
-
- let resizable = ChildView::new(&self.pane, cx)
- .contained()
- .with_style(panel_style)
- .with_resize_handle::<DockResizeHandle>(
- resize_side as usize,
- resize_side,
- 4.,
- self.panel_sizes
- .get(&anchor)
- .copied()
- .unwrap_or(initial_size),
- cx,
- );
-
- let size = resizable.current_size();
- cx.defer(move |workspace, _| {
- workspace.dock.panel_sizes.insert(anchor, size);
- });
-
- if anchor == DockAnchor::Right {
- resizable.constrained().dynamically(|constraint, _, cx| {
- SizeConstraint::new(
- Vector2F::new(20., constraint.min.y()),
- Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
- )
- })
- } else {
- resizable.constrained().dynamically(|constraint, _, cx| {
- SizeConstraint::new(
- Vector2F::new(constraint.min.x(), 50.),
- Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
- )
- })
- }
- .into_any()
- }
- DockAnchor::Expanded => {
- enum ExpandedDockWash {}
- enum ExpandedDockPane {}
- Stack::new()
- .with_child(
- // Render wash under the dock which when clicked hides it
- MouseEventHandler::<ExpandedDockWash, _>::new(0, cx, |_, _| {
- Empty::new()
- .contained()
- .with_background_color(style.wash_color)
- })
- .capture_all()
- .on_down(MouseButton::Left, |_, workspace, cx| {
- Dock::hide_dock(workspace, &Default::default(), cx)
- })
- .with_cursor_style(CursorStyle::Arrow),
- )
- .with_child(
- MouseEventHandler::<ExpandedDockPane, _>::new(0, cx, |_state, cx| {
- ChildView::new(&self.pane, cx)
- })
- // Make sure all events directly under the dock pane
- // are captured
- .capture_all()
- .contained()
- .with_style(style.maximized),
- )
- .into_any()
- }
- })
+ fn active_entry(&self) -> Option<&PanelEntry> {
+ if self.is_open {
+ self.panel_entries.get(self.active_panel_index)
+ } else {
+ None
+ }
}
- pub fn position(&self) -> DockPosition {
- self.position
+ pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> {
+ let entry = self.active_entry()?;
+ if entry.panel.is_zoomed(cx) {
+ Some(entry.panel.clone())
+ } else {
+ None
+ }
+ }
+
+ pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
+ self.panel_entries
+ .iter()
+ .find(|entry| entry.panel.id() == panel.id())
+ .map(|entry| entry.panel.size(cx))
+ }
+
+ pub fn active_panel_size(&self, cx: &WindowContext) -> Option<f32> {
+ if self.is_open {
+ self.panel_entries
+ .get(self.active_panel_index)
+ .map(|entry| entry.panel.size(cx))
+ } else {
+ None
+ }
+ }
+
+ 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) {
+ entry.panel.set_size(size, cx);
+ cx.notify();
+ }
+ }
+
+ pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
+ if let Some(active_entry) = self.active_entry() {
+ Empty::new()
+ .into_any()
+ .contained()
+ .with_style(self.style(cx))
+ .resizable(
+ self.position.to_resize_handle_side(),
+ active_entry.panel.size(cx),
+ |_, _, _| {},
+ )
+ .into_any()
+ } else {
+ Empty::new().into_any()
+ }
+ }
+
+ fn style(&self, cx: &WindowContext) -> ContainerStyle {
+ let theme = &settings::get::<ThemeSettings>(cx).theme;
+ let style = match self.position {
+ DockPosition::Left => theme.workspace.dock.left,
+ DockPosition::Bottom => theme.workspace.dock.bottom,
+ DockPosition::Right => theme.workspace.dock.right,
+ };
+ style
}
}
-#[cfg(test)]
-mod tests {
- use std::{
- ops::{Deref, DerefMut},
- path::PathBuf,
- sync::Arc,
- };
+impl Entity for Dock {
+ type Event = ();
+}
- use gpui::{AppContext, BorrowWindowContext, TestAppContext, ViewContext, WindowContext};
- use project::{FakeFs, Project};
+impl View for Dock {
+ fn ui_name() -> &'static str {
+ "Dock"
+ }
- use super::*;
- use crate::{
- dock,
- item::{self, test::TestItem},
- persistence::model::{
- SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
- },
- register_deserializable_item,
- sidebar::Sidebar,
- tests::init_test,
- AppState, ItemHandle, Workspace,
- };
-
- pub fn default_item_factory(
- _workspace: &mut Workspace,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<Box<dyn ItemHandle>> {
- Some(Box::new(cx.add_view(|_| TestItem::new())))
- }
-
- #[gpui::test]
- async fn test_dock_workspace_infinite_loop(cx: &mut TestAppContext) {
- init_test(cx);
-
- cx.update(|cx| {
- register_deserializable_item::<item::test::TestItem>(cx);
- });
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ if let Some(active_entry) = self.active_entry() {
+ let style = self.style(cx);
+ ChildView::new(active_entry.panel.as_any(), cx)
+ .contained()
+ .with_style(style)
+ .resizable(
+ self.position.to_resize_handle_side(),
+ active_entry.panel.size(cx),
+ |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
+ )
+ .into_any()
+ } else {
+ Empty::new().into_any()
+ }
+ }
+}
- let serialized_workspace = SerializedWorkspace {
- id: 0,
- location: Vec::<PathBuf>::new().into(),
- dock_position: dock::DockPosition::Shown(DockAnchor::Expanded),
- center_group: SerializedPaneGroup::Pane(SerializedPane {
- active: false,
- children: vec![],
- }),
- dock_pane: SerializedPane {
- active: true,
- children: vec![SerializedItem {
- active: true,
- item_id: 0,
- kind: "TestItem".into(),
- }],
- },
- left_sidebar_open: false,
- bounds: Default::default(),
- display: Default::default(),
+impl PanelButtons {
+ pub fn new(
+ dock: ViewHandle<Dock>,
+ workspace: WeakViewHandle<Workspace>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ cx.observe(&dock, |_, _, cx| cx.notify()).detach();
+ Self { dock, workspace }
+ }
+}
+
+impl Entity for PanelButtons {
+ type Event = ();
+}
+
+impl View for PanelButtons {
+ fn ui_name() -> &'static str {
+ "PanelButtons"
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ let theme = &settings::get::<ThemeSettings>(cx).theme;
+ let tooltip_style = theme.tooltip.clone();
+ let theme = &theme.workspace.status_bar.panel_buttons;
+ let button_style = theme.button.clone();
+ let dock = self.dock.read(cx);
+ let active_ix = dock.active_panel_index;
+ let is_open = dock.is_open;
+ let dock_position = dock.position;
+ let group_style = match dock_position {
+ DockPosition::Left => theme.group_left,
+ DockPosition::Bottom => theme.group_bottom,
+ DockPosition::Right => theme.group_right,
+ };
+ let menu_corner = match dock_position {
+ DockPosition::Left => AnchorCorner::BottomLeft,
+ DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
};
- let fs = FakeFs::new(cx.background());
- let project = Project::test(fs, [], cx).await;
-
- let (_, _workspace) = cx.add_window(|cx| {
- Workspace::new(
- 0,
- project.clone(),
- Arc::new(AppState {
- languages: project.read(cx).languages().clone(),
- client: project.read(cx).client(),
- user_store: project.read(cx).user_store(),
- fs: project.read(cx).fs().clone(),
- build_window_options: |_, _, _| Default::default(),
- initialize_workspace: |_, _, _| {},
- dock_default_item_factory: default_item_factory,
- background_actions: || &[],
- }),
- cx,
- )
- });
+ let panels = dock
+ .panel_entries
+ .iter()
+ .map(|item| (item.panel.clone(), item.context_menu.clone()))
+ .collect::<Vec<_>>();
+ Flex::row()
+ .with_children(
+ panels
+ .into_iter()
+ .enumerate()
+ .map(|(ix, (view, context_menu))| {
+ let action = TogglePanel {
+ dock_position,
+ panel_index: ix,
+ };
- cx.update(|cx| {
- Workspace::load_workspace(_workspace.downgrade(), serialized_workspace, Vec::new(), cx)
- })
- .await;
-
- cx.foreground().run_until_parked();
- //Should terminate
- }
-
- #[gpui::test]
- async fn test_dock_hides_when_pane_empty(cx: &mut TestAppContext) {
- let mut cx = DockTestContext::new(cx).await;
-
- // Closing the last item in the dock hides the dock
- cx.move_dock(DockAnchor::Right);
- let old_items = cx.dock_items();
- assert!(!old_items.is_empty());
- cx.close_dock_items().await;
- cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
-
- // Reopening the dock adds a new item
- cx.move_dock(DockAnchor::Right);
- let new_items = cx.dock_items();
- assert!(!new_items.is_empty());
- assert!(new_items
- .into_iter()
- .all(|new_item| !old_items.contains(&new_item)));
- }
-
- #[gpui::test]
- async fn test_dock_panel_collisions(cx: &mut TestAppContext) {
- let mut cx = DockTestContext::new(cx).await;
-
- // Dock closes when expanded for either panel
- cx.move_dock(DockAnchor::Expanded);
- cx.open_sidebar(SidebarSide::Left);
- cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
- cx.close_sidebar(SidebarSide::Left);
- cx.move_dock(DockAnchor::Expanded);
- cx.open_sidebar(SidebarSide::Right);
- cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
-
- // Dock closes in the right position if the right sidebar is opened
- cx.move_dock(DockAnchor::Right);
- cx.open_sidebar(SidebarSide::Left);
- cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
- cx.open_sidebar(SidebarSide::Right);
- cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
- cx.close_sidebar(SidebarSide::Right);
-
- // Dock in bottom position ignores sidebars
- cx.move_dock(DockAnchor::Bottom);
- cx.open_sidebar(SidebarSide::Left);
- cx.open_sidebar(SidebarSide::Right);
- cx.assert_dock_position(DockPosition::Shown(DockAnchor::Bottom));
-
- // Opening the dock in the right position closes the right sidebar
- cx.move_dock(DockAnchor::Right);
- cx.assert_sidebar_closed(SidebarSide::Right);
- }
-
- #[gpui::test]
- async fn test_focusing_panes_shows_and_hides_dock(cx: &mut TestAppContext) {
- let mut cx = DockTestContext::new(cx).await;
-
- // Focusing an item not in the dock when expanded hides the dock
- let center_item = cx.add_item_to_center_pane();
- cx.move_dock(DockAnchor::Expanded);
- let dock_item = cx
- .dock_items()
- .get(0)
- .cloned()
- .expect("Dock should have an item at this point");
- center_item.update(&mut cx, |_, cx| cx.focus_self());
- cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
-
- // Focusing an item not in the dock when not expanded, leaves the dock open but inactive
- cx.move_dock(DockAnchor::Right);
- center_item.update(&mut cx, |_, cx| cx.focus_self());
- cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
- cx.assert_dock_pane_inactive();
- cx.assert_workspace_pane_active();
-
- // Focusing an item in the dock activates it's pane
- dock_item.update(&mut cx, |_, cx| cx.focus_self());
- cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
- cx.assert_dock_pane_active();
- cx.assert_workspace_pane_inactive();
- }
-
- #[gpui::test]
- async fn test_toggle_dock_focus(cx: &mut TestAppContext) {
- let mut cx = DockTestContext::new(cx).await;
-
- cx.move_dock(DockAnchor::Right);
- cx.assert_dock_pane_active();
- cx.hide_dock();
- cx.move_dock(DockAnchor::Right);
- cx.assert_dock_pane_active();
- }
-
- #[gpui::test]
- async fn test_activate_next_and_prev_pane(cx: &mut TestAppContext) {
- let mut cx = DockTestContext::new(cx).await;
-
- cx.move_dock(DockAnchor::Right);
- cx.assert_dock_pane_active();
-
- cx.update_workspace(|workspace, cx| workspace.activate_next_pane(cx));
- cx.assert_dock_pane_active();
-
- cx.update_workspace(|workspace, cx| workspace.activate_previous_pane(cx));
- cx.assert_dock_pane_active();
- }
-
- struct DockTestContext<'a> {
- pub cx: &'a mut TestAppContext,
- pub window_id: usize,
- pub workspace: ViewHandle<Workspace>,
- }
-
- impl<'a> DockTestContext<'a> {
- pub async fn new(cx: &'a mut TestAppContext) -> DockTestContext<'a> {
- init_test(cx);
- let fs = FakeFs::new(cx.background());
-
- cx.update(|cx| init(cx));
- let project = Project::test(fs, [], cx).await;
- let (window_id, workspace) = cx.add_window(|cx| {
- Workspace::new(
- 0,
- project.clone(),
- Arc::new(AppState {
- languages: project.read(cx).languages().clone(),
- client: project.read(cx).client(),
- user_store: project.read(cx).user_store(),
- fs: project.read(cx).fs().clone(),
- build_window_options: |_, _, _| Default::default(),
- initialize_workspace: |_, _, _| {},
- dock_default_item_factory: default_item_factory,
- background_actions: || &[],
+ Stack::new()
+ .with_child(
+ MouseEventHandler::<Self, _>::new(ix, cx, |state, cx| {
+ let is_active = is_open && ix == active_ix;
+ let style = button_style.style_for(state, is_active);
+ Flex::row()
+ .with_child(
+ Svg::new(view.icon_path(cx))
+ .with_color(style.icon_color)
+ .constrained()
+ .with_width(style.icon_size)
+ .aligned(),
+ )
+ .with_children(if let Some(label) = view.icon_label(cx) {
+ Some(
+ Label::new(label, style.label.text.clone())
+ .contained()
+ .with_style(style.label.container)
+ .aligned(),
+ )
+ } else {
+ None
+ })
+ .constrained()
+ .with_height(style.icon_size)
+ .contained()
+ .with_style(style.container)
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, {
+ let action = action.clone();
+ move |_, this, cx| {
+ if let Some(workspace) = this.workspace.upgrade(cx) {
+ let action = action.clone();
+ cx.window_context().defer(move |cx| {
+ workspace.update(cx, |workspace, cx| {
+ workspace.toggle_panel(&action, cx)
+ });
+ });
+ }
+ }
+ })
+ .on_click(MouseButton::Right, {
+ let view = view.clone();
+ let menu = context_menu.clone();
+ move |_, _, cx| {
+ const POSITIONS: [DockPosition; 3] = [
+ DockPosition::Left,
+ DockPosition::Right,
+ DockPosition::Bottom,
+ ];
+
+ menu.update(cx, |menu, cx| {
+ let items = POSITIONS
+ .into_iter()
+ .filter(|position| {
+ *position != dock_position
+ && view.position_is_valid(*position, cx)
+ })
+ .map(|position| {
+ let view = view.clone();
+ ContextMenuItem::handler(
+ format!("Dock {}", position.to_label()),
+ move |cx| view.set_position(position, cx),
+ )
+ })
+ .collect();
+ menu.show(Default::default(), menu_corner, items, cx);
+ })
+ }
+ })
+ .with_tooltip::<Self>(
+ ix,
+ view.icon_tooltip(cx),
+ Some(Box::new(action)),
+ tooltip_style.clone(),
+ cx,
+ ),
+ )
+ .with_child(ChildView::new(&context_menu, cx))
}),
- cx,
- )
- });
-
- workspace.update(cx, |workspace, cx| {
- let left_panel = cx.add_view(|_| TestItem::new());
- workspace.left_sidebar().update(cx, |sidebar, cx| {
- sidebar.add_item(
- "icons/folder_tree_16.svg",
- "Left Test Panel".to_string(),
- left_panel.clone(),
- cx,
- );
- });
-
- let right_panel = cx.add_view(|_| TestItem::new());
- workspace.right_sidebar().update(cx, |sidebar, cx| {
- sidebar.add_item(
- "icons/folder_tree_16.svg",
- "Right Test Panel".to_string(),
- right_panel.clone(),
- cx,
- );
- });
- });
+ )
+ .contained()
+ .with_style(group_style)
+ .into_any()
+ }
+}
+
+impl StatusItemView for PanelButtons {
+ fn set_active_pane_item(
+ &mut self,
+ _: Option<&dyn crate::ItemHandle>,
+ _: &mut ViewContext<Self>,
+ ) {
+ }
+}
+
+#[cfg(test)]
+pub(crate) mod test {
+ use super::*;
+ use gpui::{ViewContext, WindowContext};
+
+ pub enum TestPanelEvent {
+ PositionChanged,
+ Activated,
+ Closed,
+ ZoomIn,
+ ZoomOut,
+ Focus,
+ }
+
+ pub struct TestPanel {
+ pub position: DockPosition,
+ pub zoomed: bool,
+ pub active: bool,
+ pub has_focus: bool,
+ pub size: f32,
+ }
+ impl TestPanel {
+ pub fn new(position: DockPosition) -> Self {
Self {
- cx,
- window_id,
- workspace,
+ position,
+ zoomed: false,
+ active: false,
+ has_focus: false,
+ size: 300.,
}
}
+ }
- pub fn workspace<F, T>(&self, read: F) -> T
- where
- F: FnOnce(&Workspace, &ViewContext<Workspace>) -> T,
- {
- self.workspace.read_with(self.cx, read)
+ impl Entity for TestPanel {
+ type Event = TestPanelEvent;
+ }
+
+ impl View for TestPanel {
+ fn ui_name() -> &'static str {
+ "TestPanel"
}
- pub fn update_workspace<F, T>(&mut self, update: F) -> T
- where
- F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
- {
- self.workspace.update(self.cx, update)
+ fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
+ Empty::new().into_any()
}
- pub fn sidebar<F, T>(&self, sidebar_side: SidebarSide, read: F) -> T
- where
- F: FnOnce(&Sidebar, &AppContext) -> T,
- {
- self.workspace(|workspace, cx| {
- let sidebar = match sidebar_side {
- SidebarSide::Left => workspace.left_sidebar(),
- SidebarSide::Right => workspace.right_sidebar(),
- }
- .read(cx);
-
- read(sidebar, cx)
- })
- }
-
- pub fn center_pane_handle(&self) -> ViewHandle<Pane> {
- self.workspace(|workspace, cx| {
- workspace
- .last_active_center_pane
- .clone()
- .and_then(|pane| pane.upgrade(cx))
- .unwrap_or_else(|| workspace.center.panes()[0].clone())
- })
- }
-
- pub fn add_item_to_center_pane(&mut self) -> ViewHandle<TestItem> {
- self.update_workspace(|workspace, cx| {
- let item = cx.add_view(|_| TestItem::new());
- let pane = workspace
- .last_active_center_pane
- .clone()
- .and_then(|pane| pane.upgrade(cx))
- .unwrap_or_else(|| workspace.center.panes()[0].clone());
- Pane::add_item(
- workspace,
- &pane,
- Box::new(item.clone()),
- true,
- true,
- None,
- cx,
- );
- item
- })
- }
-
- pub fn dock_pane<F, T>(&self, read: F) -> T
- where
- F: FnOnce(&Pane, &AppContext) -> T,
- {
- self.workspace(|workspace, cx| {
- let dock_pane = workspace.dock_pane().read(cx);
- read(dock_pane, cx)
- })
+ fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ self.has_focus = true;
+ cx.emit(TestPanelEvent::Focus);
}
- pub fn move_dock(&mut self, anchor: DockAnchor) {
- self.update_workspace(|workspace, cx| Dock::move_dock(workspace, anchor, true, cx));
+ fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+ self.has_focus = false;
}
+ }
- pub fn hide_dock(&mut self) {
- self.cx.dispatch_action(self.window_id, HideDock);
+ impl Panel for TestPanel {
+ fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
+ self.position
}
- pub fn open_sidebar(&mut self, sidebar_side: SidebarSide) {
- if !self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
- self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
- }
+ fn position_is_valid(&self, _: super::DockPosition) -> bool {
+ true
}
- pub fn close_sidebar(&mut self, sidebar_side: SidebarSide) {
- if self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
- self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
- }
+ fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+ self.position = position;
+ cx.emit(TestPanelEvent::PositionChanged);
}
- pub fn dock_items(&self) -> Vec<ViewHandle<TestItem>> {
- self.dock_pane(|pane, cx| {
- pane.items()
- .map(|item| {
- item.act_as::<TestItem>(cx)
- .expect("Dock Test Context uses TestItems in the dock")
- })
- .collect()
- })
+ fn is_zoomed(&self, _: &WindowContext) -> bool {
+ self.zoomed
}
- pub async fn close_dock_items(&mut self) {
- self.update_workspace(|workspace, cx| {
- Pane::close_items(workspace, workspace.dock_pane().clone(), cx, |_| true)
- })
- .await
- .expect("Could not close dock items")
+ fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
+ self.zoomed = zoomed;
}
- pub fn assert_dock_position(&self, expected_position: DockPosition) {
- self.workspace(|workspace, _| assert_eq!(workspace.dock.position, expected_position));
+ fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
+ self.active = active;
}
- pub fn assert_sidebar_closed(&self, sidebar_side: SidebarSide) {
- assert!(!self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()));
+ fn size(&self, _: &WindowContext) -> f32 {
+ self.size
}
- pub fn assert_workspace_pane_active(&self) {
- assert!(self
- .center_pane_handle()
- .read_with(self.cx, |pane, _| pane.is_active()));
+ fn set_size(&mut self, size: f32, _: &mut ViewContext<Self>) {
+ self.size = size;
}
- pub fn assert_workspace_pane_inactive(&self) {
- assert!(!self
- .center_pane_handle()
- .read_with(self.cx, |pane, _| pane.is_active()));
+ fn icon_path(&self) -> &'static str {
+ "icons/test_panel.svg"
}
- pub fn assert_dock_pane_active(&self) {
- assert!(self.dock_pane(|pane, _| pane.is_active()))
+ fn icon_tooltip(&self) -> String {
+ "Test Panel".into()
}
- pub fn assert_dock_pane_inactive(&self) {
- assert!(!self.dock_pane(|pane, _| pane.is_active()))
+ fn should_change_position_on_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::PositionChanged)
}
- }
- impl<'a> Deref for DockTestContext<'a> {
- type Target = gpui::TestAppContext;
+ fn should_zoom_in_on_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::ZoomIn)
+ }
- fn deref(&self) -> &Self::Target {
- self.cx
+ fn should_zoom_out_on_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::ZoomOut)
}
- }
- impl<'a> DerefMut for DockTestContext<'a> {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.cx
+ fn should_activate_on_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::Activated)
+ }
+
+ fn should_close_on_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::Closed)
}
- }
- impl BorrowWindowContext for DockTestContext<'_> {
- fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T {
- BorrowWindowContext::read_with(self.cx, window_id, f)
+ fn has_focus(&self, _cx: &WindowContext) -> bool {
+ self.has_focus
}
- fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T {
- BorrowWindowContext::update(self.cx, window_id, f)
+ fn is_focus_event(event: &Self::Event) -> bool {
+ matches!(event, TestPanelEvent::Focus)
}
}
}
@@ -1,125 +0,0 @@
-use super::{icon_for_dock_anchor, Dock, FocusDock, HideDock};
-use crate::{handle_dropped_item, StatusItemView, Workspace};
-use gpui::{
- elements::{Empty, MouseEventHandler, Svg},
- platform::CursorStyle,
- platform::MouseButton,
- AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle,
-};
-
-pub struct ToggleDockButton {
- workspace: WeakViewHandle<Workspace>,
-}
-
-impl ToggleDockButton {
- pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
- // When dock moves, redraw so that the icon and toggle status matches.
- cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
-
- Self {
- workspace: workspace.downgrade(),
- }
- }
-}
-
-impl Entity for ToggleDockButton {
- type Event = ();
-}
-
-impl View for ToggleDockButton {
- fn ui_name() -> &'static str {
- "Dock Toggle"
- }
-
- fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
- let workspace = self.workspace.upgrade(cx);
-
- if workspace.is_none() {
- return Empty::new().into_any();
- }
-
- let workspace = workspace.unwrap();
- let dock_position = workspace.read(cx).dock.position;
- let dock_pane = workspace.read(cx).dock_pane().clone();
-
- let theme = theme::current(cx).clone();
-
- let button = MouseEventHandler::<Self, _>::new(0, cx, {
- let theme = theme.clone();
- move |state, _| {
- let style = theme
- .workspace
- .status_bar
- .sidebar_buttons
- .item
- .style_for(state, dock_position.is_visible());
-
- Svg::new(icon_for_dock_anchor(dock_position.anchor()))
- .with_color(style.icon_color)
- .constrained()
- .with_width(style.icon_size)
- .with_height(style.icon_size)
- .contained()
- .with_style(style.container)
- }
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_up(MouseButton::Left, move |event, this, cx| {
- let drop_index = dock_pane.read(cx).items_len() + 1;
- handle_dropped_item(
- event,
- this.workspace.clone(),
- &dock_pane.downgrade(),
- drop_index,
- false,
- None,
- cx,
- );
- });
-
- if dock_position.is_visible() {
- button
- .on_click(MouseButton::Left, |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- Dock::hide_dock(workspace, &Default::default(), cx)
- })
- }
- })
- .with_tooltip::<Self>(
- 0,
- "Hide Dock".into(),
- Some(Box::new(HideDock)),
- theme.tooltip.clone(),
- cx,
- )
- } else {
- button
- .on_click(MouseButton::Left, |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- Dock::focus_dock(workspace, &Default::default(), cx)
- })
- }
- })
- .with_tooltip::<Self>(
- 0,
- "Focus Dock".into(),
- Some(Box::new(FocusDock)),
- theme.tooltip.clone(),
- cx,
- )
- }
- .into_any()
- }
-}
-
-impl StatusItemView for ToggleDockButton {
- fn set_active_pane_item(
- &mut self,
- _active_pane_item: Option<&dyn crate::ItemHandle>,
- _cx: &mut ViewContext<Self>,
- ) {
- //Not applicable
- }
-}
@@ -437,7 +437,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
for item_event in T::to_item_events(event).into_iter() {
match item_event {
ItemEvent::CloseItem => {
- Pane::close_item_by_id(workspace, pane, item.id(), cx)
+ pane.update(cx, |pane, cx| pane.close_item_by_id(item.id(), cx))
.detach_and_log_err(cx);
return;
}
@@ -769,7 +769,7 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
#[cfg(test)]
pub(crate) mod test {
use super::{Item, ItemEvent};
- use crate::{sidebar::SidebarItem, ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
+ use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
use gpui::{
elements::Empty, AnyElement, AppContext, Element, Entity, ModelHandle, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
@@ -1062,6 +1062,4 @@ pub(crate) mod test {
Task::Ready(Some(anyhow::Ok(view)))
}
}
-
- impl SidebarItem for TestItem {}
}
@@ -2,17 +2,14 @@ mod dragged_item_receiver;
use super::{ItemHandle, SplitDirection};
use crate::{
- dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, Dock, ExpandDock},
- item::WeakItemHandle,
- toolbar::Toolbar,
- AutosaveSetting, DockAnchor, Item, NewFile, NewSearch, NewTerminal, Workspace,
- WorkspaceSettings,
+ item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewFile, NewSearch, NewTerminal,
+ ToggleZoom, Workspace, WorkspaceSettings,
};
use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet, VecDeque};
use context_menu::{ContextMenu, ContextMenuItem};
-use drag_and_drop::Draggable;
-pub use dragged_item_receiver::{dragged_item_receiver, handle_dropped_item};
+use drag_and_drop::{DragAndDrop, Draggable};
+use dragged_item_receiver::dragged_item_receiver;
use futures::StreamExt;
use gpui::{
actions,
@@ -41,7 +38,7 @@ use std::{
Arc,
},
};
-use theme::Theme;
+use theme::{Theme, ThemeSettings};
use util::ResultExt;
#[derive(Clone, Deserialize, PartialEq)]
@@ -104,6 +101,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)];
pub fn init(cx: &mut AppContext) {
+ cx.add_action(Pane::toggle_zoom);
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
pane.activate_item(action.0, true, true, cx);
});
@@ -145,12 +143,15 @@ pub enum Event {
Split(SplitDirection),
ChangeItemTitle,
Focus,
+ ZoomIn,
+ ZoomOut,
}
pub struct Pane {
items: Vec<Box<dyn ItemHandle>>,
activation_history: Vec<usize>,
is_active: bool,
+ zoomed: bool,
active_item_index: usize,
last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
autoscroll: bool,
@@ -158,10 +159,12 @@ pub struct Pane {
toolbar: ViewHandle<Toolbar>,
tab_bar_context_menu: TabBarContextMenu,
tab_context_menu: ViewHandle<ContextMenu>,
- docked: Option<DockAnchor>,
_background_actions: BackgroundActions,
workspace: WeakViewHandle<Workspace>,
has_focus: bool,
+ can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
+ can_split: bool,
+ render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
}
pub struct ItemNavHistory {
@@ -203,9 +206,9 @@ pub struct NavigationEntry {
pub timestamp: usize,
}
-struct DraggedItem {
- item: Box<dyn ItemHandle>,
- pane: WeakViewHandle<Pane>,
+pub struct DraggedItem {
+ pub handle: Box<dyn ItemHandle>,
+ pub pane: WeakViewHandle<Pane>,
}
pub enum ReorderBehavior {
@@ -218,7 +221,6 @@ pub enum ReorderBehavior {
enum TabBarContextMenuKind {
New,
Split,
- Dock,
}
struct TabBarContextMenu {
@@ -238,7 +240,6 @@ impl TabBarContextMenu {
impl Pane {
pub fn new(
workspace: WeakViewHandle<Workspace>,
- docked: Option<DockAnchor>,
background_actions: BackgroundActions,
next_timestamp: Arc<AtomicUsize>,
cx: &mut ViewContext<Self>,
@@ -254,6 +255,7 @@ impl Pane {
items: Vec::new(),
activation_history: Vec::new(),
is_active: true,
+ zoomed: false,
active_item_index: 0,
last_focused_view_by_item: Default::default(),
autoscroll: false,
@@ -272,10 +274,32 @@ impl Pane {
handle: context_menu,
},
tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)),
- docked,
_background_actions: background_actions,
workspace,
has_focus: false,
+ can_drop: Rc::new(|_, _| true),
+ can_split: true,
+ render_tab_bar_buttons: Rc::new(|pane, cx| {
+ Flex::row()
+ // New menu
+ .with_child(Self::render_tab_bar_button(
+ 0,
+ "icons/plus_12.svg",
+ cx,
+ |pane, cx| pane.deploy_new_menu(cx),
+ pane.tab_bar_context_menu
+ .handle_if_kind(TabBarContextMenuKind::New),
+ ))
+ .with_child(Self::render_tab_bar_button(
+ 2,
+ "icons/split_12.svg",
+ cx,
+ |pane, cx| pane.deploy_split_menu(cx),
+ pane.tab_bar_context_menu
+ .handle_if_kind(TabBarContextMenuKind::Split),
+ ))
+ .into_any()
+ }),
}
}
@@ -296,8 +320,23 @@ impl Pane {
self.has_focus
}
- pub fn set_docked(&mut self, docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) {
- self.docked = docked;
+ pub fn on_can_drop<F>(&mut self, can_drop: F)
+ where
+ F: 'static + Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool,
+ {
+ self.can_drop = Rc::new(can_drop);
+ }
+
+ pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
+ self.can_split = can_split;
+ cx.notify();
+ }
+
+ pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
+ where
+ F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
+ {
+ self.render_tab_bar_buttons = Rc::new(render);
cx.notify();
}
@@ -515,7 +554,7 @@ impl Pane {
}
}
- pub(crate) fn add_item(
+ pub fn add_item(
workspace: &mut Workspace,
pane: &ViewHandle<Pane>,
item: Box<dyn ItemHandle>,
@@ -641,6 +680,14 @@ impl Pane {
self.items.iter().position(|i| i.id() == item.id())
}
+ pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+ if self.zoomed {
+ cx.emit(Event::ZoomOut);
+ } else {
+ cx.emit(Event::ZoomIn);
+ }
+ }
+
pub fn activate_item(
&mut self,
index: usize,
@@ -704,185 +751,118 @@ impl Pane {
}
pub fn close_active_item(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseActiveItem,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
- let pane = pane_handle.read(cx);
-
- let active_item_id = pane.items.get(pane.active_item_index)?.id();
-
- let task = Self::close_item_by_id(workspace, pane_handle, active_item_id, cx);
-
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ if self.items.is_empty() {
+ return None;
+ }
+ let active_item_id = self.items[self.active_item_index].id();
+ Some(self.close_item_by_id(active_item_id, cx))
}
pub fn close_item_by_id(
- workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
+ &mut self,
item_id_to_close: usize,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
- Self::close_items(workspace, pane, cx, move |view_id| {
- view_id == item_id_to_close
- })
+ self.close_items(cx, move |view_id| view_id == item_id_to_close)
}
pub fn close_inactive_items(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseInactiveItems,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
- let pane = pane_handle.read(cx);
-
- let active_item_id = pane.items.get(pane.active_item_index)?.id();
-
- let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
- item_id != active_item_id
- });
+ if self.items.is_empty() {
+ return None;
+ }
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ let active_item_id = self.items[self.active_item_index].id();
+ Some(self.close_items(cx, move |item_id| item_id != active_item_id))
}
pub fn close_clean_items(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseCleanItems,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
- let pane = pane_handle.read(cx);
-
- let item_ids: Vec<_> = pane
+ let item_ids: Vec<_> = self
.items()
.filter(|item| !item.is_dirty(cx))
.map(|item| item.id())
.collect();
-
- let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
- item_ids.contains(&item_id)
- });
-
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ Some(self.close_items(cx, move |item_id| item_ids.contains(&item_id)))
}
pub fn close_items_to_the_left(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseItemsToTheLeft,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
- let pane = pane_handle.read(cx);
- let active_item_id = pane.items.get(pane.active_item_index)?.id();
-
- let task = Self::close_items_to_the_left_by_id(workspace, pane_handle, active_item_id, cx);
-
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ if self.items.is_empty() {
+ return None;
+ }
+ let active_item_id = self.items[self.active_item_index].id();
+ Some(self.close_items_to_the_left_by_id(active_item_id, cx))
}
pub fn close_items_to_the_left_by_id(
- workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
+ &mut self,
item_id: usize,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
- let item_ids: Vec<_> = pane
- .read(cx)
+ let item_ids: Vec<_> = self
.items()
.take_while(|item| item.id() != item_id)
.map(|item| item.id())
.collect();
-
- let task = Self::close_items(workspace, pane, cx, move |item_id| {
- item_ids.contains(&item_id)
- });
-
- cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- })
+ self.close_items(cx, move |item_id| item_ids.contains(&item_id))
}
pub fn close_items_to_the_right(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseItemsToTheRight,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
- let pane = pane_handle.read(cx);
- let active_item_id = pane.items.get(pane.active_item_index)?.id();
-
- let task = Self::close_items_to_the_right_by_id(workspace, pane_handle, active_item_id, cx);
-
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ if self.items.is_empty() {
+ return None;
+ }
+ let active_item_id = self.items[self.active_item_index].id();
+ Some(self.close_items_to_the_right_by_id(active_item_id, cx))
}
pub fn close_items_to_the_right_by_id(
- workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
+ &mut self,
item_id: usize,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
- let item_ids: Vec<_> = pane
- .read(cx)
+ let item_ids: Vec<_> = self
.items()
.rev()
.take_while(|item| item.id() != item_id)
.map(|item| item.id())
.collect();
-
- let task = Self::close_items(workspace, pane, cx, move |item_id| {
- item_ids.contains(&item_id)
- });
-
- cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- })
+ self.close_items(cx, move |item_id| item_ids.contains(&item_id))
}
pub fn close_all_items(
- workspace: &mut Workspace,
+ &mut self,
_: &CloseAllItems,
- cx: &mut ViewContext<Workspace>,
+ cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
-
- let task = Self::close_items(workspace, pane_handle, cx, move |_| true);
-
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ Some(self.close_items(cx, move |_| true))
}
pub fn close_items(
- workspace: &mut Workspace,
- pane: ViewHandle<Pane>,
- cx: &mut ViewContext<Workspace>,
+ &mut self,
+ cx: &mut ViewContext<Pane>,
should_close: impl 'static + Fn(usize) -> bool,
) -> Task<Result<()>> {
- let project = workspace.project().clone();
-
// Find the items to close.
let mut items_to_close = Vec::new();
- for item in &pane.read(cx).items {
+ for item in &self.items {
if should_close(item.id()) {
items_to_close.push(item.boxed_clone());
}
@@ -894,8 +874,8 @@ impl Pane {
// of what content they would be saving.
items_to_close.sort_by_key(|item| !item.is_singleton(cx));
- let pane = pane.downgrade();
- cx.spawn(|workspace, mut cx| async move {
+ let workspace = self.workspace.clone();
+ cx.spawn(|pane, mut cx| async move {
let mut saved_project_items_ids = HashSet::default();
for item in items_to_close.clone() {
// Find the item's current index and its set of project item models. Avoid
@@ -913,7 +893,7 @@ impl Pane {
// Check if this view has any project items that are not open anywhere else
// in the workspace, AND that the user has not already been prompted to save.
// If there are any such project entries, prompt the user to save this item.
- workspace.read_with(&cx, |workspace, cx| {
+ let project = workspace.read_with(&cx, |workspace, cx| {
for item in workspace.items(cx) {
if !items_to_close
.iter()
@@ -923,6 +903,7 @@ impl Pane {
project_item_ids.retain(|id| !other_project_item_ids.contains(id));
}
}
+ workspace.project().clone()
})?;
let should_save = project_item_ids
.iter()
@@ -965,7 +946,8 @@ impl Pane {
// to activating the item to the left
.unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
- self.activate_item(index_to_activate, activate_pane, activate_pane, cx);
+ let should_activate = activate_pane || self.has_focus;
+ self.activate_item(index_to_activate, should_activate, should_activate, cx);
}
let item = self.items.remove(item_index);
@@ -1175,23 +1157,6 @@ impl Pane {
self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
}
- fn deploy_dock_menu(&mut self, cx: &mut ViewContext<Self>) {
- self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
- menu.show(
- Default::default(),
- AnchorCorner::TopRight,
- vec![
- ContextMenuItem::action("Anchor Dock Right", AnchorDockRight),
- ContextMenuItem::action("Anchor Dock Bottom", AnchorDockBottom),
- ContextMenuItem::action("Expand Dock", ExpandDock),
- ],
- cx,
- );
- });
-
- self.tab_bar_context_menu.kind = TabBarContextMenuKind::Dock;
- }
-
fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
menu.show(
@@ -1238,14 +1203,11 @@ impl Pane {
// In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
vec![
ContextMenuItem::handler("Close Inactive Item", {
- let workspace = self.workspace.clone();
let pane = target_pane.clone();
move |cx| {
- if let Some((workspace, pane)) =
- workspace.upgrade(cx).zip(pane.upgrade(cx))
- {
- workspace.update(cx, |workspace, cx| {
- Self::close_item_by_id(workspace, pane, target_item_id, cx)
+ if let Some(pane) = pane.upgrade(cx) {
+ pane.update(cx, |pane, cx| {
+ pane.close_item_by_id(target_item_id, cx)
.detach_and_log_err(cx);
})
}
@@ -1254,39 +1216,23 @@ impl Pane {
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
ContextMenuItem::handler("Close Items To The Left", {
- let workspace = self.workspace.clone();
let pane = target_pane.clone();
move |cx| {
- if let Some((workspace, pane)) =
- workspace.upgrade(cx).zip(pane.upgrade(cx))
- {
- workspace.update(cx, |workspace, cx| {
- Self::close_items_to_the_left_by_id(
- workspace,
- pane,
- target_item_id,
- cx,
- )
- .detach_and_log_err(cx);
+ if let Some(pane) = pane.upgrade(cx) {
+ pane.update(cx, |pane, cx| {
+ pane.close_items_to_the_left_by_id(target_item_id, cx)
+ .detach_and_log_err(cx);
})
}
}
}),
ContextMenuItem::handler("Close Items To The Right", {
- let workspace = self.workspace.clone();
let pane = target_pane.clone();
move |cx| {
- if let Some((workspace, pane)) =
- workspace.upgrade(cx).zip(pane.upgrade(cx))
- {
- workspace.update(cx, |workspace, cx| {
- Self::close_items_to_the_right_by_id(
- workspace,
- pane,
- target_item_id,
- cx,
- )
- .detach_and_log_err(cx);
+ if let Some(pane) = pane.upgrade(cx) {
+ pane.update(cx, |pane, cx| {
+ pane.close_items_to_the_right_by_id(target_item_id, cx)
+ .detach_and_log_err(cx);
})
}
}
@@ -1359,7 +1305,7 @@ impl Pane {
row.add_child({
enum TabDragReceiver {}
let mut receiver =
- dragged_item_receiver::<TabDragReceiver, _, _>(ix, ix, true, None, cx, {
+ dragged_item_receiver::<TabDragReceiver, _, _>(self, ix, ix, true, None, cx, {
let item = item.clone();
let pane = pane.clone();
let detail = detail.clone();
@@ -1393,20 +1339,7 @@ impl Pane {
.on_click(MouseButton::Middle, {
let item_id = item.id();
move |_, pane, cx| {
- let workspace = pane.workspace.clone();
- let pane = cx.weak_handle();
- cx.window_context().defer(move |cx| {
- if let Some((workspace, pane)) =
- workspace.upgrade(cx).zip(pane.upgrade(cx))
- {
- workspace.update(cx, |workspace, cx| {
- Self::close_item_by_id(
- workspace, pane, item_id, cx,
- )
- .detach_and_log_err(cx);
- });
- }
- });
+ pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
}
})
.on_down(
@@ -1438,7 +1371,7 @@ impl Pane {
receiver.as_draggable(
DraggedItem {
- item,
+ handle: item,
pane: pane.clone(),
},
{
@@ -1448,7 +1381,7 @@ impl Pane {
move |dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
let tab_style = &theme.workspace.tab_bar.dragged_tab;
Self::render_dragged_tab(
- &dragged_item.item,
+ &dragged_item.handle,
dragged_item.pane.clone(),
false,
detail,
@@ -1468,7 +1401,7 @@ impl Pane {
let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
enum Filler {}
row.add_child(
- dragged_item_receiver::<Filler, _, _>(0, filler_index, true, None, cx, |_, _| {
+ dragged_item_receiver::<Filler, _, _>(self, 0, filler_index, true, None, cx, |_, _| {
Empty::new()
.contained()
.with_style(filler_style.container)
@@ -1613,12 +1546,9 @@ impl Pane {
let pane = pane.clone();
cx.window_context().defer(move |cx| {
if let Some(pane) = pane.upgrade(cx) {
- if let Some(workspace) = pane.read(cx).workspace.upgrade(cx) {
- workspace.update(cx, |workspace, cx| {
- Self::close_item_by_id(workspace, pane, item_id, cx)
- .detach_and_log_err(cx);
- });
- }
+ pane.update(cx, |pane, cx| {
+ pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
+ });
}
});
}
@@ -1638,83 +1568,55 @@ impl Pane {
.into_any()
}
- fn render_tab_bar_buttons(
- &mut self,
- theme: &Theme,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- Flex::row()
- // New menu
- .with_child(render_tab_bar_button(
- 0,
- "icons/plus_12.svg",
- cx,
- |pane, cx| pane.deploy_new_menu(cx),
- self.tab_bar_context_menu
- .handle_if_kind(TabBarContextMenuKind::New),
- ))
+ pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
+ index: usize,
+ icon: &'static str,
+ cx: &mut ViewContext<Pane>,
+ on_click: F,
+ context_menu: Option<ViewHandle<ContextMenu>>,
+ ) -> AnyElement<Pane> {
+ enum TabBarButton {}
+
+ Stack::new()
.with_child(
- self.docked
- .map(|anchor| {
- // Add the dock menu button if this pane is a dock
- let dock_icon = icon_for_dock_anchor(anchor);
-
- render_tab_bar_button(
- 1,
- dock_icon,
- cx,
- |pane, cx| pane.deploy_dock_menu(cx),
- self.tab_bar_context_menu
- .handle_if_kind(TabBarContextMenuKind::Dock),
- )
- })
- .unwrap_or_else(|| {
- // Add the split menu if this pane is not a dock
- render_tab_bar_button(
- 2,
- "icons/split_12.svg",
- cx,
- |pane, cx| pane.deploy_split_menu(cx),
- self.tab_bar_context_menu
- .handle_if_kind(TabBarContextMenuKind::Split),
- )
- }),
+ MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
+ let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
+ let style = theme.pane_button.style_for(mouse_state, false);
+ Svg::new(icon)
+ .with_color(style.color)
+ .constrained()
+ .with_width(style.icon_width)
+ .aligned()
+ .constrained()
+ .with_width(style.button_width)
+ .with_height(style.button_width)
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)),
+ )
+ .with_children(
+ context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
)
- // Add the close dock button if this pane is a dock
- .with_children(self.docked.map(|_| {
- render_tab_bar_button(
- 3,
- "icons/x_mark_8.svg",
- cx,
- |this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- cx.window_context().defer(move |cx| {
- workspace.update(cx, |workspace, cx| {
- Dock::hide_dock(workspace, &Default::default(), cx)
- })
- });
- }
- },
- None,
- )
- }))
- .contained()
- .with_style(theme.workspace.tab_bar.pane_button_container)
.flex(1., false)
- .into_any()
+ .into_any_named("tab bar button")
}
- fn render_blank_pane(
- &mut self,
- theme: &Theme,
- _cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
+ fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let background = theme.workspace.background;
Empty::new()
.contained()
.with_background_color(background)
.into_any()
}
+
+ pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
+ self.zoomed = zoomed;
+ cx.notify();
+ }
+
+ pub fn is_zoomed(&self) -> bool {
+ self.zoomed
+ }
}
impl Entity for Pane {
@@ -1758,7 +1660,14 @@ impl View for Pane {
.with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
if self.is_active {
- tab_row.add_child(self.render_tab_bar_buttons(&theme, cx))
+ let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
+ tab_row.add_child(
+ (render_tab_bar_buttons)(self, cx)
+ .contained()
+ .with_style(theme.workspace.tab_bar.pane_button_container)
+ .flex(1., false)
+ .into_any(),
+ )
}
stack.add_child(tab_row);
@@ -1771,14 +1680,11 @@ impl View for Pane {
.with_child({
enum PaneContentTabDropTarget {}
dragged_item_receiver::<PaneContentTabDropTarget, _, _>(
+ self,
0,
self.active_item_index + 1,
- false,
- if self.docked.is_some() {
- None
- } else {
- Some(100.)
- },
+ !self.can_split,
+ if self.can_split { Some(100.) } else { None },
cx,
{
let toolbar = self.toolbar.clone();
@@ -1803,7 +1709,7 @@ impl View for Pane {
enum EmptyPane {}
let theme = theme::current(cx).clone();
- dragged_item_receiver::<EmptyPane, _, _>(0, 0, false, None, cx, |_, cx| {
+ dragged_item_receiver::<EmptyPane, _, _>(self, 0, 0, false, None, cx, |_, cx| {
self.render_blank_pane(&theme, cx)
})
.on_down(MouseButton::Left, |_, _, cx| {
@@ -1841,7 +1747,11 @@ impl View for Pane {
}
fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.has_focus = true;
+ if !self.has_focus {
+ self.has_focus = true;
+ cx.emit(Event::Focus);
+ }
+
self.toolbar.update(cx, |toolbar, cx| {
toolbar.pane_focus_update(true, cx);
});
@@ -1867,8 +1777,6 @@ impl View for Pane {
.insert(active_item.id(), focused.downgrade());
}
}
-
- cx.emit(Event::Focus);
}
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -1880,45 +1788,9 @@ impl View for Pane {
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
- if self.docked.is_some() {
- keymap.add_identifier("docked");
- }
}
}
-fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
- index: usize,
- icon: &'static str,
- cx: &mut ViewContext<Pane>,
- on_click: F,
- context_menu: Option<ViewHandle<ContextMenu>>,
-) -> AnyElement<Pane> {
- enum TabBarButton {}
-
- Stack::new()
- .with_child(
- MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
- let theme = &theme::current(cx).workspace.tab_bar;
- let style = theme.pane_button.style_for(mouse_state, false);
- Svg::new(icon)
- .with_color(style.color)
- .constrained()
- .with_width(style.icon_width)
- .aligned()
- .constrained()
- .with_width(style.button_width)
- .with_height(style.button_width)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)),
- )
- .with_children(
- context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()),
- )
- .flex(1., false)
- .into_any_named("tab bar button")
-}
-
impl ItemNavHistory {
pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut WindowContext) {
self.history.borrow_mut().push(data, self.item.clone(), cx);
@@ -2156,11 +2028,9 @@ impl<V: View> Element<V> for PaneBackdrop<V> {
#[cfg(test)]
mod tests {
- use std::sync::Arc;
-
use super::*;
use crate::item::test::{TestItem, TestProjectItem};
- use gpui::{executor::Deterministic, TestAppContext};
+ use gpui::TestAppContext;
use project::FakeFs;
use settings::SettingsStore;
@@ -2171,9 +2041,10 @@ mod tests {
let project = Project::test(fs, None, cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
- workspace.update(cx, |workspace, cx| {
- assert!(Pane::close_active_item(workspace, &CloseActiveItem, cx).is_none())
+ pane.update(cx, |pane, cx| {
+ assert!(pane.close_active_item(&CloseActiveItem, cx).is_none())
});
}
@@ -2452,7 +2323,7 @@ mod tests {
}
#[gpui::test]
- async fn test_remove_item_ordering(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+ async fn test_remove_item_ordering(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
@@ -2470,36 +2341,36 @@ mod tests {
add_labeled_item(&workspace, &pane, "1", false, cx);
assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
- workspace.update(cx, |workspace, cx| {
- Pane::close_active_item(workspace, &CloseActiveItem, cx);
- });
- deterministic.run_until_parked();
+ pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
+ .unwrap()
+ .await
+ .unwrap();
assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
- workspace.update(cx, |workspace, cx| {
- Pane::close_active_item(workspace, &CloseActiveItem, cx);
- });
- deterministic.run_until_parked();
+ pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
+ .unwrap()
+ .await
+ .unwrap();
assert_item_labels(&pane, ["A", "B*", "C"], cx);
- workspace.update(cx, |workspace, cx| {
- Pane::close_active_item(workspace, &CloseActiveItem, cx);
- });
- deterministic.run_until_parked();
+ pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
+ .unwrap()
+ .await
+ .unwrap();
assert_item_labels(&pane, ["A", "C*"], cx);
- workspace.update(cx, |workspace, cx| {
- Pane::close_active_item(workspace, &CloseActiveItem, cx);
- });
- deterministic.run_until_parked();
+ pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
+ .unwrap()
+ .await
+ .unwrap();
assert_item_labels(&pane, ["A*"], cx);
}
#[gpui::test]
- async fn test_close_inactive_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+ async fn test_close_inactive_items(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
@@ -2509,16 +2380,17 @@ mod tests {
set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
- workspace.update(cx, |workspace, cx| {
- Pane::close_inactive_items(workspace, &CloseInactiveItems, cx);
- });
-
- deterministic.run_until_parked();
+ pane.update(cx, |pane, cx| {
+ pane.close_inactive_items(&CloseInactiveItems, cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
assert_item_labels(&pane, ["C*"], cx);
}
#[gpui::test]
- async fn test_close_clean_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+ async fn test_close_clean_items(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
@@ -2533,19 +2405,15 @@ mod tests {
add_labeled_item(&workspace, &pane, "E", false, cx);
assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx);
- workspace.update(cx, |workspace, cx| {
- Pane::close_clean_items(workspace, &CloseCleanItems, cx);
- });
-
- deterministic.run_until_parked();
+ pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx))
+ .unwrap()
+ .await
+ .unwrap();
assert_item_labels(&pane, ["A^", "C*^"], cx);
}
#[gpui::test]
- async fn test_close_items_to_the_left(
- deterministic: Arc<Deterministic>,
- cx: &mut TestAppContext,
- ) {
+ async fn test_close_items_to_the_left(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
@@ -2555,19 +2423,17 @@ mod tests {
set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx);
- workspace.update(cx, |workspace, cx| {
- Pane::close_items_to_the_left(workspace, &CloseItemsToTheLeft, cx);
- });
-
- deterministic.run_until_parked();
+ pane.update(cx, |pane, cx| {
+ pane.close_items_to_the_left(&CloseItemsToTheLeft, cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
assert_item_labels(&pane, ["C*", "D", "E"], cx);
}
#[gpui::test]
- async fn test_close_items_to_the_right(
- deterministic: Arc<Deterministic>,
- cx: &mut TestAppContext,
- ) {
+ async fn test_close_items_to_the_right(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
@@ -12,6 +12,7 @@ use gpui::{
use project::ProjectEntryId;
pub fn dragged_item_receiver<Tag, D, F>(
+ pane: &Pane,
region_id: usize,
drop_index: usize,
allow_same_pane: bool,
@@ -24,22 +25,24 @@ where
D: Element<Pane>,
F: FnOnce(&mut MouseState, &mut ViewContext<Pane>) -> D,
{
- MouseEventHandler::<Tag, _>::above(region_id, cx, |state, cx| {
+ let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
+ let drag_position = if (pane.can_drop)(drag_and_drop, cx) {
+ drag_and_drop
+ .currently_dragged::<DraggedItem>(cx.window_id())
+ .map(|(drag_position, _)| drag_position)
+ .or_else(|| {
+ drag_and_drop
+ .currently_dragged::<ProjectEntryId>(cx.window_id())
+ .map(|(drag_position, _)| drag_position)
+ })
+ } else {
+ None
+ };
+
+ let mut handler = MouseEventHandler::<Tag, _>::above(region_id, cx, |state, cx| {
// Observing hovered will cause a render when the mouse enters regardless
// of if mouse position was accessed before
- let drag_position = if state.hovered() {
- cx.global::<DragAndDrop<Workspace>>()
- .currently_dragged::<DraggedItem>(cx.window_id())
- .map(|(drag_position, _)| drag_position)
- .or_else(|| {
- cx.global::<DragAndDrop<Workspace>>()
- .currently_dragged::<ProjectEntryId>(cx.window_id())
- .map(|(drag_position, _)| drag_position)
- })
- } else {
- None
- };
-
+ let drag_position = if state.hovered() { drag_position } else { None };
Stack::new()
.with_child(render_child(state, cx))
.with_children(drag_position.map(|drag_position| {
@@ -64,38 +67,44 @@ where
}
})
}))
- })
- .on_up(MouseButton::Left, {
- move |event, pane, cx| {
- let workspace = pane.workspace.clone();
- let pane = cx.weak_handle();
- handle_dropped_item(
- event,
- workspace,
- &pane,
- drop_index,
- allow_same_pane,
- split_margin,
- cx,
- );
- cx.notify();
- }
- })
- .on_move(|_, _, cx| {
- let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
+ });
- if drag_and_drop
- .currently_dragged::<DraggedItem>(cx.window_id())
- .is_some()
- || drag_and_drop
- .currently_dragged::<ProjectEntryId>(cx.window_id())
- .is_some()
- {
- cx.notify();
- } else {
- cx.propagate_event();
- }
- })
+ if drag_position.is_some() {
+ handler = handler
+ .on_up(MouseButton::Left, {
+ move |event, pane, cx| {
+ let workspace = pane.workspace.clone();
+ let pane = cx.weak_handle();
+ handle_dropped_item(
+ event,
+ workspace,
+ &pane,
+ drop_index,
+ allow_same_pane,
+ split_margin,
+ cx,
+ );
+ cx.notify();
+ }
+ })
+ .on_move(|_, _, cx| {
+ let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
+
+ if drag_and_drop
+ .currently_dragged::<DraggedItem>(cx.window_id())
+ .is_some()
+ || drag_and_drop
+ .currently_dragged::<ProjectEntryId>(cx.window_id())
+ .is_some()
+ {
+ cx.notify();
+ } else {
+ cx.propagate_event();
+ }
+ })
+ }
+
+ handler
}
pub fn handle_dropped_item<V: View>(
@@ -115,7 +124,7 @@ pub fn handle_dropped_item<V: View>(
let action = if let Some((_, dragged_item)) =
drag_and_drop.currently_dragged::<DraggedItem>(cx.window_id())
{
- Action::Move(dragged_item.pane.clone(), dragged_item.item.id())
+ Action::Move(dragged_item.pane.clone(), dragged_item.handle.id())
} else if let Some((_, project_entry)) =
drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window_id())
{
@@ -7,7 +7,7 @@ use gpui::{
elements::*,
geometry::{rect::RectF, vector::Vector2F},
platform::{CursorStyle, MouseButton},
- Axis, Border, ModelHandle, ViewContext, ViewHandle,
+ AnyViewHandle, Axis, Border, ModelHandle, ViewContext, ViewHandle,
};
use project::Project;
use serde::Deserialize;
@@ -71,6 +71,7 @@ impl PaneGroup {
follower_states: &FollowerStatesByLeader,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
+ zoomed: Option<&AnyViewHandle>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> AnyElement<Workspace> {
@@ -80,6 +81,7 @@ impl PaneGroup {
follower_states,
active_call,
active_pane,
+ zoomed,
app_state,
cx,
)
@@ -134,6 +136,7 @@ impl Member {
follower_states: &FollowerStatesByLeader,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
+ zoomed: Option<&AnyViewHandle>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> AnyElement<Workspace> {
@@ -141,6 +144,12 @@ impl Member {
match self {
Member::Pane(pane) => {
+ let pane_element = if Some(&**pane) == zoomed {
+ Empty::new().into_any()
+ } else {
+ ChildView::new(pane, cx).into_any()
+ };
+
let leader = follower_states
.iter()
.find_map(|(leader_id, follower_states)| {
@@ -257,7 +266,7 @@ impl Member {
};
Stack::new()
- .with_child(ChildView::new(pane, cx).contained().with_border(border))
+ .with_child(pane_element.contained().with_border(border))
.with_children(leader_status_box)
.into_any()
}
@@ -267,6 +276,7 @@ impl Member {
follower_states,
active_call,
active_pane,
+ zoomed,
app_state,
cx,
),
@@ -371,6 +381,7 @@ impl PaneAxis {
follower_state: &FollowerStatesByLeader,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
+ zoomed: Option<&AnyViewHandle>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> AnyElement<Workspace> {
@@ -388,6 +399,7 @@ impl PaneAxis {
follower_state,
active_call,
active_pane,
+ zoomed,
app_state,
cx,
);
@@ -11,7 +11,6 @@ use gpui::{platform::WindowBounds, Axis};
use util::{unzip_option, ResultExt};
use uuid::Uuid;
-use crate::dock::DockPosition;
use crate::WorkspaceId;
use model::{
@@ -19,15 +18,17 @@ use model::{
WorkspaceLocation,
};
+use self::model::DockStructure;
+
define_connection! {
// Current schema shape using pseudo-rust syntax:
//
// workspaces(
// workspace_id: usize, // Primary key for workspaces
// workspace_location: Bincode<Vec<PathBuf>>,
- // dock_visible: bool,
- // dock_anchor: DockAnchor, // 'Bottom' / 'Right' / 'Expanded'
- // dock_pane: Option<usize>, // PaneId
+ // dock_visible: bool, // Deprecated
+ // dock_anchor: DockAnchor, // Deprecated
+ // dock_pane: Option<usize>, // Deprecated
// left_sidebar_open: boolean,
// timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
// window_state: String, // WindowBounds Discriminant
@@ -71,10 +72,10 @@ define_connection! {
CREATE TABLE workspaces(
workspace_id INTEGER PRIMARY KEY,
workspace_location BLOB UNIQUE,
- dock_visible INTEGER, // Boolean
- dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded'
- dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet
- left_sidebar_open INTEGER, //Boolean
+ dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
+ dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
+ dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
+ left_sidebar_open INTEGER, // Boolean
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
) STRICT;
@@ -131,6 +132,36 @@ define_connection! {
ALTER TABLE workspaces ADD COLUMN window_width REAL;
ALTER TABLE workspaces ADD COLUMN window_height REAL;
ALTER TABLE workspaces ADD COLUMN display BLOB;
+ ),
+ // Drop foreign key constraint from workspaces.dock_pane to panes table.
+ sql!(
+ CREATE TABLE workspaces_2(
+ workspace_id INTEGER PRIMARY KEY,
+ workspace_location BLOB UNIQUE,
+ dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
+ dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
+ dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
+ left_sidebar_open INTEGER, // Boolean
+ timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ window_state TEXT,
+ window_x REAL,
+ window_y REAL,
+ window_width REAL,
+ window_height REAL,
+ display BLOB
+ ) STRICT;
+ INSERT INTO workspaces_2 SELECT * FROM workspaces;
+ DROP TABLE workspaces;
+ ALTER TABLE workspaces_2 RENAME TO workspaces;
+ ),
+ // Add panels related information
+ sql!(
+ ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
+ ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
+ ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
+ ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
+ ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
+ ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
)];
}
@@ -146,27 +177,29 @@ impl WorkspaceDb {
// Note that we re-assign the workspace_id here in case it's empty
// and we've grabbed the most recent workspace
- let (workspace_id, workspace_location, left_sidebar_open, dock_position, bounds, display): (
+ let (workspace_id, workspace_location, bounds, display, docks): (
WorkspaceId,
WorkspaceLocation,
- bool,
- DockPosition,
Option<WindowBounds>,
Option<Uuid>,
+ DockStructure,
) = self
.select_row_bound(sql! {
SELECT
workspace_id,
workspace_location,
- left_sidebar_open,
- dock_visible,
- dock_anchor,
window_state,
window_x,
window_y,
window_width,
window_height,
- display
+ display,
+ left_dock_visible,
+ left_dock_active_panel,
+ right_dock_visible,
+ right_dock_active_panel,
+ bottom_dock_visible,
+ bottom_dock_active_panel
FROM workspaces
WHERE workspace_location = ?
})
@@ -178,18 +211,13 @@ impl WorkspaceDb {
Some(SerializedWorkspace {
id: workspace_id,
location: workspace_location.clone(),
- dock_pane: self
- .get_dock_pane(workspace_id)
- .context("Getting dock pane")
- .log_err()?,
center_group: self
.get_center_pane_group(workspace_id)
.context("Getting center group")
.log_err()?,
- dock_position,
- left_sidebar_open,
bounds,
display,
+ docks,
})
}
@@ -200,7 +228,6 @@ impl WorkspaceDb {
conn.with_savepoint("update_worktrees", || {
// Clear out panes and pane_groups
conn.exec_bound(sql!(
- UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1;
DELETE FROM pane_groups WHERE workspace_id = ?1;
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
.expect("Clearing old panes");
@@ -215,42 +242,32 @@ impl WorkspaceDb {
INSERT INTO workspaces(
workspace_id,
workspace_location,
- left_sidebar_open,
- dock_visible,
- dock_anchor,
+ left_dock_visible,
+ left_dock_active_panel,
+ right_dock_visible,
+ right_dock_active_panel,
+ bottom_dock_visible,
+ bottom_dock_active_panel,
timestamp
)
- VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP)
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, CURRENT_TIMESTAMP)
ON CONFLICT DO
UPDATE SET
workspace_location = ?2,
- left_sidebar_open = ?3,
- dock_visible = ?4,
- dock_anchor = ?5,
+ left_dock_visible = ?3,
+ left_dock_active_panel = ?4,
+ right_dock_visible = ?5,
+ right_dock_active_panel = ?6,
+ bottom_dock_visible = ?7,
+ bottom_dock_active_panel = ?8,
timestamp = CURRENT_TIMESTAMP
- ))?((
- workspace.id,
- &workspace.location,
- workspace.left_sidebar_open,
- workspace.dock_position,
- ))
+ ))?((workspace.id, &workspace.location, workspace.docks))
.context("Updating workspace")?;
- // Save center pane group and dock pane
+ // Save center pane group
Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
.context("save pane group in save workspace")?;
- let dock_id = Self::save_pane(conn, workspace.id, &workspace.dock_pane, None, true)
- .context("save pane in save workspace")?;
-
- // Complete workspace initialization
- conn.exec_bound(sql!(
- UPDATE workspaces
- SET dock_pane = ?
- WHERE workspace_id = ?
- ))?((dock_id, workspace.id))
- .context("Finishing initialization with dock pane")?;
-
Ok(())
})
.log_err();
@@ -402,32 +419,17 @@ impl WorkspaceDb {
Ok(())
}
SerializedPaneGroup::Pane(pane) => {
- Self::save_pane(conn, workspace_id, &pane, parent, false)?;
+ Self::save_pane(conn, workspace_id, &pane, parent)?;
Ok(())
}
}
}
- fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
- let (pane_id, active) = self.select_row_bound(sql!(
- SELECT pane_id, active
- FROM panes
- WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
- ))?(workspace_id)?
- .context("No dock pane for workspace")?;
-
- Ok(SerializedPane::new(
- self.get_items(pane_id).context("Reading items")?,
- active,
- ))
- }
-
fn save_pane(
conn: &Connection,
workspace_id: WorkspaceId,
pane: &SerializedPane,
- parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
- dock: bool,
+ parent: Option<(GroupId, usize)>,
) -> Result<PaneId> {
let pane_id = conn.select_row_bound::<_, i64>(sql!(
INSERT INTO panes(workspace_id, active)
@@ -436,13 +438,11 @@ impl WorkspaceDb {
))?((workspace_id, pane.active))?
.ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
- if !dock {
- let (parent_id, order) = unzip_option(parent);
- conn.exec_bound(sql!(
- INSERT INTO center_panes(pane_id, parent_group_id, position)
- VALUES (?, ?, ?)
- ))?((pane_id, parent_id, order))?;
- }
+ let (parent_id, order) = unzip_option(parent);
+ conn.exec_bound(sql!(
+ INSERT INTO center_panes(pane_id, parent_group_id, position)
+ VALUES (?, ?, ?)
+ ))?((pane_id, parent_id, order))?;
Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
@@ -498,9 +498,7 @@ impl WorkspaceDb {
#[cfg(test)]
mod tests {
use super::*;
- use crate::DockAnchor;
use db::open_test_db;
- use std::sync::Arc;
#[gpui::test]
async fn test_next_id_stability() {
@@ -575,23 +573,19 @@ mod tests {
let mut workspace_1 = SerializedWorkspace {
id: 1,
location: (["/tmp", "/tmp2"]).into(),
- dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
center_group: Default::default(),
- dock_pane: Default::default(),
- left_sidebar_open: true,
bounds: Default::default(),
display: Default::default(),
+ docks: Default::default(),
};
- let mut workspace_2 = SerializedWorkspace {
+ let workspace_2 = SerializedWorkspace {
id: 2,
location: (["/tmp"]).into(),
- dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
center_group: Default::default(),
- dock_pane: Default::default(),
- left_sidebar_open: false,
bounds: Default::default(),
display: Default::default(),
+ docks: Default::default(),
};
db.save_workspace(workspace_1.clone()).await;
@@ -615,12 +609,6 @@ mod tests {
workspace_1.location = (["/tmp", "/tmp3"]).into();
db.save_workspace(workspace_1.clone()).await;
db.save_workspace(workspace_1).await;
-
- workspace_2.dock_pane.children.push(SerializedItem {
- kind: Arc::from("Test"),
- item_id: 10,
- active: true,
- });
db.save_workspace(workspace_2).await;
let test_text_2 = db
@@ -644,16 +632,6 @@ mod tests {
let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
- let dock_pane = crate::persistence::model::SerializedPane {
- children: vec![
- SerializedItem::new("Terminal", 1, false),
- SerializedItem::new("Terminal", 2, false),
- SerializedItem::new("Terminal", 3, true),
- SerializedItem::new("Terminal", 4, false),
- ],
- active: false,
- };
-
// -----------------
// | 1,2 | 5,6 |
// | - - - | |
@@ -694,12 +672,10 @@ mod tests {
let workspace = SerializedWorkspace {
id: 5,
location: (["/tmp", "/tmp2"]).into(),
- dock_position: DockPosition::Shown(DockAnchor::Bottom),
center_group,
- dock_pane,
- left_sidebar_open: true,
bounds: Default::default(),
display: Default::default(),
+ docks: Default::default(),
};
db.save_workspace(workspace.clone()).await;
@@ -724,23 +700,19 @@ mod tests {
let workspace_1 = SerializedWorkspace {
id: 1,
location: (["/tmp", "/tmp2"]).into(),
- dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
center_group: Default::default(),
- dock_pane: Default::default(),
- left_sidebar_open: true,
bounds: Default::default(),
display: Default::default(),
+ docks: Default::default(),
};
let mut workspace_2 = SerializedWorkspace {
id: 2,
location: (["/tmp"]).into(),
- dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
center_group: Default::default(),
- dock_pane: Default::default(),
- left_sidebar_open: false,
bounds: Default::default(),
display: Default::default(),
+ docks: Default::default(),
};
db.save_workspace(workspace_1.clone()).await;
@@ -773,12 +745,10 @@ mod tests {
let mut workspace_3 = SerializedWorkspace {
id: 3,
location: (&["/tmp", "/tmp2"]).into(),
- dock_position: DockPosition::Shown(DockAnchor::Right),
center_group: Default::default(),
- dock_pane: Default::default(),
- left_sidebar_open: false,
bounds: Default::default(),
display: Default::default(),
+ docks: Default::default(),
};
db.save_workspace(workspace_3.clone()).await;
@@ -798,52 +768,23 @@ mod tests {
);
}
- use crate::dock::DockPosition;
use crate::persistence::model::SerializedWorkspace;
use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
fn default_workspace<P: AsRef<Path>>(
workspace_id: &[P],
- dock_pane: SerializedPane,
center_group: &SerializedPaneGroup,
) -> SerializedWorkspace {
SerializedWorkspace {
id: 4,
location: workspace_id.into(),
- dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
center_group: center_group.clone(),
- dock_pane,
- left_sidebar_open: true,
bounds: Default::default(),
display: Default::default(),
+ docks: Default::default(),
}
}
- #[gpui::test]
- async fn test_basic_dock_pane() {
- env_logger::try_init().ok();
-
- let db = WorkspaceDb(open_test_db("basic_dock_pane").await);
-
- let dock_pane = crate::persistence::model::SerializedPane::new(
- vec![
- SerializedItem::new("Terminal", 1, false),
- SerializedItem::new("Terminal", 4, false),
- SerializedItem::new("Terminal", 2, false),
- SerializedItem::new("Terminal", 3, true),
- ],
- false,
- );
-
- let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
-
- db.save_workspace(workspace.clone()).await;
-
- let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
-
- assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
- }
-
#[gpui::test]
async fn test_simple_split() {
env_logger::try_init().ok();
@@ -887,7 +828,7 @@ mod tests {
],
};
- let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane);
+ let workspace = default_workspace(&["/tmp"], ¢er_pane);
db.save_workspace(workspace.clone()).await;
@@ -936,7 +877,7 @@ mod tests {
let id = &["/tmp"];
- let mut workspace = default_workspace(id, Default::default(), ¢er_pane);
+ let mut workspace = default_workspace(id, ¢er_pane);
db.save_workspace(workspace.clone()).await;
@@ -1,7 +1,4 @@
-use crate::{
- dock::DockPosition, item::ItemHandle, DockAnchor, ItemDeserializers, Member, Pane, PaneAxis,
- Workspace, WorkspaceId,
-};
+use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
use anyhow::{anyhow, Context, Result};
use async_recursion::async_recursion;
use db::sqlez::{
@@ -62,12 +59,68 @@ impl Column for WorkspaceLocation {
pub struct SerializedWorkspace {
pub id: WorkspaceId,
pub location: WorkspaceLocation,
- pub dock_position: DockPosition,
pub center_group: SerializedPaneGroup,
- pub dock_pane: SerializedPane,
- pub left_sidebar_open: bool,
pub bounds: Option<WindowBounds>,
pub display: Option<Uuid>,
+ pub docks: DockStructure,
+}
+
+#[derive(Debug, PartialEq, Clone, Default)]
+pub struct DockStructure {
+ pub(crate) left: DockData,
+ pub(crate) right: DockData,
+ pub(crate) bottom: DockData,
+}
+
+impl Column for DockStructure {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (left, next_index) = DockData::column(statement, start_index)?;
+ let (right, next_index) = DockData::column(statement, next_index)?;
+ let (bottom, next_index) = DockData::column(statement, next_index)?;
+ Ok((
+ DockStructure {
+ left,
+ right,
+ bottom,
+ },
+ next_index,
+ ))
+ }
+}
+
+impl Bind for DockStructure {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = statement.bind(&self.left, start_index)?;
+ let next_index = statement.bind(&self.right, next_index)?;
+ statement.bind(&self.bottom, next_index)
+ }
+}
+
+#[derive(Debug, PartialEq, Clone, Default)]
+pub struct DockData {
+ pub(crate) visible: bool,
+ pub(crate) active_panel: Option<String>,
+}
+
+impl Column for DockData {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (visible, next_index) = Option::<bool>::column(statement, start_index)?;
+ let (active_panel, next_index) = Option::<String>::column(statement, next_index)?;
+ Ok((
+ DockData {
+ visible: visible.unwrap_or(false),
+ active_panel,
+ },
+ next_index,
+ ))
+ }
+}
+
+impl Bind for DockData {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = statement.bind(&self.visible, start_index)?;
+ statement.bind(&self.active_panel, next_index)
+ }
}
#[derive(Debug, PartialEq, Eq, Clone)]
@@ -266,9 +319,9 @@ impl StaticColumnCount for SerializedItem {
}
impl Bind for &SerializedItem {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
- let next_index = statement.bind(self.kind.clone(), start_index)?;
- let next_index = statement.bind(self.item_id, next_index)?;
- statement.bind(self.active, next_index)
+ let next_index = statement.bind(&self.kind, start_index)?;
+ let next_index = statement.bind(&self.item_id, next_index)?;
+ statement.bind(&self.active, next_index)
}
}
@@ -287,64 +340,3 @@ impl Column for SerializedItem {
))
}
}
-
-impl StaticColumnCount for DockPosition {
- fn column_count() -> usize {
- 2
- }
-}
-impl Bind for DockPosition {
- fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
- let next_index = statement.bind(self.is_visible(), start_index)?;
- statement.bind(self.anchor(), next_index)
- }
-}
-
-impl Column for DockPosition {
- fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
- let (visible, next_index) = bool::column(statement, start_index)?;
- let (dock_anchor, next_index) = DockAnchor::column(statement, next_index)?;
- let position = if visible {
- DockPosition::Shown(dock_anchor)
- } else {
- DockPosition::Hidden(dock_anchor)
- };
- Ok((position, next_index))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::WorkspaceLocation;
- use crate::DockAnchor;
- use db::sqlez::connection::Connection;
-
- #[test]
- fn test_workspace_round_trips() {
- let db = Connection::open_memory(Some("workspace_id_round_trips"));
-
- db.exec(indoc::indoc! {"
- CREATE TABLE workspace_id_test(
- workspace_id INTEGER,
- dock_anchor TEXT
- );"})
- .unwrap()()
- .unwrap();
-
- let workspace_id: WorkspaceLocation = WorkspaceLocation::from(&["\test2", "\test1"]);
-
- db.exec_bound("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)")
- .unwrap()((&workspace_id, DockAnchor::Bottom))
- .unwrap();
-
- assert_eq!(
- db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1")
- .unwrap()()
- .unwrap(),
- Some((
- WorkspaceLocation::from(&["\test1", "\test2"]),
- DockAnchor::Bottom
- ))
- );
- }
-}
@@ -1,321 +0,0 @@
-use crate::{StatusItemView, Workspace};
-use gpui::{
- elements::*, impl_actions, platform::CursorStyle, platform::MouseButton, AnyViewHandle,
- AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
-};
-use serde::Deserialize;
-use std::rc::Rc;
-
-pub trait SidebarItem: View {
- fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
- false
- }
- fn should_show_badge(&self, _: &AppContext) -> bool {
- false
- }
- fn contains_focused_view(&self, _: &AppContext) -> bool {
- false
- }
-}
-
-pub trait SidebarItemHandle {
- fn id(&self) -> usize;
- fn should_show_badge(&self, cx: &WindowContext) -> bool;
- fn is_focused(&self, cx: &WindowContext) -> bool;
- fn as_any(&self) -> &AnyViewHandle;
-}
-
-impl<T> SidebarItemHandle for ViewHandle<T>
-where
- T: SidebarItem,
-{
- fn id(&self) -> usize {
- self.id()
- }
-
- fn should_show_badge(&self, cx: &WindowContext) -> bool {
- self.read(cx).should_show_badge(cx)
- }
-
- fn is_focused(&self, cx: &WindowContext) -> bool {
- ViewHandle::is_focused(self, cx) || self.read(cx).contains_focused_view(cx)
- }
-
- fn as_any(&self) -> &AnyViewHandle {
- self
- }
-}
-
-impl From<&dyn SidebarItemHandle> for AnyViewHandle {
- fn from(val: &dyn SidebarItemHandle) -> Self {
- val.as_any().clone()
- }
-}
-
-pub struct Sidebar {
- sidebar_side: SidebarSide,
- items: Vec<Item>,
- is_open: bool,
- active_item_ix: usize,
-}
-
-#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
-pub enum SidebarSide {
- Left,
- Right,
-}
-
-impl SidebarSide {
- fn to_resizable_side(self) -> Side {
- match self {
- Self::Left => Side::Right,
- Self::Right => Side::Left,
- }
- }
-}
-
-struct Item {
- icon_path: &'static str,
- tooltip: String,
- view: Rc<dyn SidebarItemHandle>,
- _subscriptions: [Subscription; 2],
-}
-
-pub struct SidebarButtons {
- sidebar: ViewHandle<Sidebar>,
- workspace: WeakViewHandle<Workspace>,
-}
-
-#[derive(Clone, Debug, Deserialize, PartialEq)]
-pub struct ToggleSidebarItem {
- pub sidebar_side: SidebarSide,
- pub item_index: usize,
-}
-
-impl_actions!(workspace, [ToggleSidebarItem]);
-
-impl Sidebar {
- pub fn new(sidebar_side: SidebarSide) -> Self {
- Self {
- sidebar_side,
- items: Default::default(),
- active_item_ix: 0,
- is_open: false,
- }
- }
-
- pub fn is_open(&self) -> bool {
- self.is_open
- }
-
- pub fn active_item_ix(&self) -> usize {
- self.active_item_ix
- }
-
- pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
- if open != self.is_open {
- self.is_open = open;
- cx.notify();
- }
- }
-
- pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
- if self.is_open {}
- self.is_open = !self.is_open;
- cx.notify();
- }
-
- pub fn add_item<T: SidebarItem>(
- &mut self,
- icon_path: &'static str,
- tooltip: String,
- view: ViewHandle<T>,
- cx: &mut ViewContext<Self>,
- ) {
- let subscriptions = [
- cx.observe(&view, |_, _, cx| cx.notify()),
- cx.subscribe(&view, |this, view, event, cx| {
- if view.read(cx).should_activate_item_on_event(event, cx) {
- if let Some(ix) = this
- .items
- .iter()
- .position(|item| item.view.id() == view.id())
- {
- this.activate_item(ix, cx);
- }
- }
- }),
- ];
-
- self.items.push(Item {
- icon_path,
- tooltip,
- view: Rc::new(view),
- _subscriptions: subscriptions,
- });
- cx.notify()
- }
-
- pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
- self.active_item_ix = item_ix;
- cx.notify();
- }
-
- pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
- if self.active_item_ix == item_ix {
- self.is_open = false;
- } else {
- self.active_item_ix = item_ix;
- }
- cx.notify();
- }
-
- pub fn active_item(&self) -> Option<&Rc<dyn SidebarItemHandle>> {
- if self.is_open {
- self.items.get(self.active_item_ix).map(|item| &item.view)
- } else {
- None
- }
- }
-}
-
-impl Entity for Sidebar {
- type Event = ();
-}
-
-impl View for Sidebar {
- fn ui_name() -> &'static str {
- "Sidebar"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- if let Some(active_item) = self.active_item() {
- enum ResizeHandleTag {}
- let style = &theme::current(cx).workspace.sidebar;
- ChildView::new(active_item.as_any(), cx)
- .contained()
- .with_style(style.container)
- .with_resize_handle::<ResizeHandleTag>(
- self.sidebar_side as usize,
- self.sidebar_side.to_resizable_side(),
- 4.,
- style.initial_size,
- cx,
- )
- .into_any()
- } else {
- Empty::new().into_any()
- }
- }
-}
-
-impl SidebarButtons {
- pub fn new(
- sidebar: ViewHandle<Sidebar>,
- workspace: WeakViewHandle<Workspace>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- cx.observe(&sidebar, |_, _, cx| cx.notify()).detach();
- Self { sidebar, workspace }
- }
-}
-
-impl Entity for SidebarButtons {
- type Event = ();
-}
-
-impl View for SidebarButtons {
- fn ui_name() -> &'static str {
- "SidebarToggleButton"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &theme::current(cx);
- let tooltip_style = theme.tooltip.clone();
- let theme = &theme.workspace.status_bar.sidebar_buttons;
- let sidebar = self.sidebar.read(cx);
- let item_style = theme.item.clone();
- let badge_style = theme.badge;
- let active_ix = sidebar.active_item_ix;
- let is_open = sidebar.is_open;
- let sidebar_side = sidebar.sidebar_side;
- let group_style = match sidebar_side {
- SidebarSide::Left => theme.group_left,
- SidebarSide::Right => theme.group_right,
- };
-
- #[allow(clippy::needless_collect)]
- let items = sidebar
- .items
- .iter()
- .map(|item| (item.icon_path, item.tooltip.clone(), item.view.clone()))
- .collect::<Vec<_>>();
-
- Flex::row()
- .with_children(items.into_iter().enumerate().map(
- |(ix, (icon_path, tooltip, item_view))| {
- let action = ToggleSidebarItem {
- sidebar_side,
- item_index: ix,
- };
- MouseEventHandler::<Self, _>::new(ix, cx, |state, cx| {
- let is_active = is_open && ix == active_ix;
- let style = item_style.style_for(state, is_active);
- Stack::new()
- .with_child(Svg::new(icon_path).with_color(style.icon_color))
- .with_children(if !is_active && item_view.should_show_badge(cx) {
- Some(
- Empty::new()
- .collapsed()
- .contained()
- .with_style(badge_style)
- .aligned()
- .bottom()
- .right(),
- )
- } else {
- None
- })
- .constrained()
- .with_width(style.icon_size)
- .with_height(style.icon_size)
- .contained()
- .with_style(style.container)
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, {
- let action = action.clone();
- move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- let action = action.clone();
- cx.window_context().defer(move |cx| {
- workspace.update(cx, |workspace, cx| {
- workspace.toggle_sidebar_item(&action, cx)
- });
- });
- }
- }
- })
- .with_tooltip::<Self>(
- ix,
- tooltip,
- Some(Box::new(action)),
- tooltip_style.clone(),
- cx,
- )
- },
- ))
- .contained()
- .with_style(group_style)
- .into_any()
- }
-}
-
-impl StatusItemView for SidebarButtons {
- fn set_active_pane_item(
- &mut self,
- _: Option<&dyn crate::ItemHandle>,
- _: &mut ViewContext<Self>,
- ) {
- }
-}
@@ -1,8 +1,8 @@
+pub mod dock;
/// NOTE: Focus only 'takes' after an update has flushed_effects.
///
/// This may cause issues when you're trying to write tests that use workspace focus to add items at
/// specific locations.
-pub mod dock;
pub mod item;
pub mod notifications;
pub mod pane;
@@ -10,7 +10,6 @@ pub mod pane_group;
mod persistence;
pub mod searchable;
pub mod shared_screen;
-pub mod sidebar;
mod status_bar;
mod toolbar;
mod workspace_settings;
@@ -23,7 +22,6 @@ use client::{
Client, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
-use dock::{Dock, DockDefaultItemFactory, ToggleDockButton};
use drag_and_drop::DragAndDrop;
use futures::{
channel::{mpsc, oneshot},
@@ -62,10 +60,12 @@ use std::{
use crate::{
notifications::simple_message_notification::MessageNotification,
- persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
+ persistence::model::{
+ DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
+ },
};
+use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, TogglePanel};
use lazy_static::lazy_static;
-use log::warn;
use notifications::{NotificationHandle, NotifyResultExt};
pub use pane::*;
pub use pane_group::*;
@@ -78,13 +78,12 @@ use postage::prelude::Stream;
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use serde::Deserialize;
use shared_screen::SharedScreen;
-use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
use theme::Theme;
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::{async_iife, paths, ResultExt};
-pub use workspace_settings::{AutosaveSetting, DockAnchor, GitGutterSetting, WorkspaceSettings};
+pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
lazy_static! {
static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
@@ -119,12 +118,14 @@ actions!(
ActivatePreviousPane,
ActivateNextPane,
FollowNextCollaborator,
- ToggleLeftSidebar,
+ ToggleLeftDock,
NewTerminal,
+ ToggleTerminalFocus,
NewSearch,
Feedback,
Restart,
- Welcome
+ Welcome,
+ ToggleZoom,
]
);
@@ -192,7 +193,6 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
init_settings(cx);
pane::init(cx);
- dock::init(cx);
notifications::init(cx);
cx.add_global_action({
@@ -240,15 +240,15 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
workspace.save_active_item(true, cx).detach_and_log_err(cx);
},
);
- cx.add_action(Workspace::toggle_sidebar_item);
+ cx.add_action(Workspace::toggle_panel);
cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
workspace.activate_previous_pane(cx)
});
cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
workspace.activate_next_pane(cx)
});
- cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
- workspace.toggle_sidebar(SidebarSide::Left, cx);
+ cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
+ workspace.toggle_dock(DockPosition::Left, cx);
});
cx.add_action(Workspace::activate_pane_at_index);
@@ -366,8 +366,8 @@ pub struct AppState {
pub fs: Arc<dyn fs::Fs>,
pub build_window_options:
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
- pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
- pub dock_default_item_factory: DockDefaultItemFactory,
+ pub initialize_workspace:
+ fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
pub background_actions: BackgroundActions,
}
@@ -395,9 +395,8 @@ impl AppState {
fs,
languages,
user_store,
- initialize_workspace: |_, _, _| {},
+ initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
build_window_options: |_, _, _| Default::default(),
- dock_default_item_factory: |_, _| None,
background_actions: || &[],
})
}
@@ -450,7 +449,6 @@ impl DelayedDebouncedEditAction {
}
pub enum Event {
- DockAnchorChanged,
PaneAdded(ViewHandle<Pane>),
ContactRequestedJoin(u64),
}
@@ -460,15 +458,15 @@ pub struct Workspace {
remote_entity_subscription: Option<client::Subscription>,
modal: Option<AnyViewHandle>,
center: PaneGroup,
- left_sidebar: ViewHandle<Sidebar>,
- right_sidebar: ViewHandle<Sidebar>,
+ left_dock: ViewHandle<Dock>,
+ bottom_dock: ViewHandle<Dock>,
+ right_dock: ViewHandle<Dock>,
panes: Vec<ViewHandle<Pane>>,
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
active_pane: ViewHandle<Pane>,
last_active_center_pane: Option<WeakViewHandle<Pane>>,
status_bar: ViewHandle<StatusBar>,
titlebar_item: Option<AnyViewHandle>,
- dock: Dock,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>,
leader_state: LeaderState,
@@ -479,7 +477,7 @@ pub struct Workspace {
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId,
app_state: Arc<AppState>,
- _window_subscriptions: [Subscription; 3],
+ subscriptions: Vec<Subscription>,
_apply_leader_updates: Task<Result<()>>,
_observe_current_user: Task<Result<()>>,
pane_history_timestamp: Arc<AtomicUsize>,
@@ -557,7 +555,6 @@ impl Workspace {
let center_pane = cx.add_view(|cx| {
Pane::new(
weak_handle.clone(),
- None,
app_state.background_actions,
pane_history_timestamp.clone(),
cx,
@@ -566,13 +563,6 @@ impl Workspace {
cx.subscribe(¢er_pane, Self::handle_pane_event).detach();
cx.focus(¢er_pane);
cx.emit(Event::PaneAdded(center_pane.clone()));
- let dock = Dock::new(
- app_state.dock_default_item_factory,
- app_state.background_actions,
- pane_history_timestamp.clone(),
- cx,
- );
- let dock_pane = dock.pane().clone();
let mut current_user = app_state.user_store.read(cx).watch_current_user();
let mut connection_status = app_state.client.status();
@@ -587,7 +577,6 @@ impl Workspace {
}
anyhow::Ok(())
});
- let handle = cx.handle();
// All leader updates are enqueued and then processed in a single task, so
// that each asynchronous operation can be run in order.
@@ -605,18 +594,20 @@ impl Workspace {
cx.emit_global(WorkspaceCreated(weak_handle.clone()));
- let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
- let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
- let left_sidebar_buttons =
- cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), weak_handle.clone(), cx));
- let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx));
- let right_sidebar_buttons =
- cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), weak_handle.clone(), cx));
+ let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left));
+ let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom));
+ let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right));
+ let left_dock_buttons =
+ cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
+ let bottom_dock_buttons =
+ cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
+ let right_dock_buttons =
+ cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
let status_bar = cx.add_view(|cx| {
let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
- status_bar.add_left_item(left_sidebar_buttons, cx);
- status_bar.add_right_item(right_sidebar_buttons, cx);
- status_bar.add_right_item(toggle_dock, cx);
+ status_bar.add_left_item(left_dock_buttons, cx);
+ status_bar.add_right_item(right_dock_buttons, cx);
+ status_bar.add_right_item(bottom_dock_buttons, cx);
status_bar
});
@@ -632,7 +623,7 @@ impl Workspace {
active_call = Some((call, subscriptions));
}
- let subscriptions = [
+ let subscriptions = vec![
cx.observe_fullscreen(|_, _, cx| cx.notify()),
cx.observe_window_activation(Self::on_window_activation_changed),
cx.observe_window_bounds(move |_, mut bounds, display, cx| {
@@ -652,17 +643,25 @@ impl Workspace {
.spawn(DB.set_window_bounds(workspace_id, bounds, display))
.detach_and_log_err(cx);
}),
+ cx.observe(&left_dock, |this, _, cx| {
+ this.serialize_workspace(cx);
+ cx.notify();
+ }),
+ cx.observe(&bottom_dock, |this, _, cx| {
+ this.serialize_workspace(cx);
+ cx.notify();
+ }),
+ cx.observe(&right_dock, |this, _, cx| {
+ this.serialize_workspace(cx);
+ cx.notify();
+ }),
];
let mut this = Workspace {
- modal: None,
weak_self: weak_handle.clone(),
+ modal: None,
center: PaneGroup::new(center_pane.clone()),
- dock,
- // When removing an item, the last element remaining in this array
- // is used to find where focus should fallback to. As such, the order
- // of these two variables is important.
- panes: vec![dock_pane.clone(), center_pane.clone()],
+ panes: vec![center_pane.clone()],
panes_by_item: Default::default(),
active_pane: center_pane.clone(),
last_active_center_pane: Some(center_pane.downgrade()),
@@ -670,8 +669,9 @@ impl Workspace {
titlebar_item: None,
notifications: Default::default(),
remote_entity_subscription: None,
- left_sidebar,
- right_sidebar,
+ left_dock,
+ bottom_dock,
+ right_dock,
project: project.clone(),
leader_state: Default::default(),
follower_states_by_leader: Default::default(),
@@ -683,12 +683,11 @@ impl Workspace {
_observe_current_user,
_apply_leader_updates,
leader_updates_tx,
- _window_subscriptions: subscriptions,
+ subscriptions,
pane_history_timestamp,
};
this.project_remote_id_changed(project.read(cx).remote_id(), cx);
cx.defer(|this, cx| this.update_window_title(cx));
-
this
}
@@ -750,11 +749,7 @@ impl Workspace {
});
let build_workspace = |cx: &mut ViewContext<Workspace>| {
- let mut workspace =
- Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx);
- (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
-
- workspace
+ Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
};
let workspace = requesting_window_id
@@ -802,6 +797,17 @@ impl Workspace {
.1
});
+ (app_state.initialize_workspace)(
+ workspace.downgrade(),
+ serialized_workspace.is_some(),
+ app_state.clone(),
+ cx.clone(),
+ )
+ .await
+ .log_err();
+
+ cx.update_window(workspace.window_id(), |cx| cx.activate_window());
+
let workspace = workspace.downgrade();
notify_if_database_failed(&workspace, &mut cx);
let opened_items = open_items(
@@ -821,12 +827,66 @@ impl Workspace {
self.weak_self.clone()
}
- pub fn left_sidebar(&self) -> &ViewHandle<Sidebar> {
- &self.left_sidebar
+ pub fn left_dock(&self) -> &ViewHandle<Dock> {
+ &self.left_dock
}
- pub fn right_sidebar(&self) -> &ViewHandle<Sidebar> {
- &self.right_sidebar
+ pub fn bottom_dock(&self) -> &ViewHandle<Dock> {
+ &self.bottom_dock
+ }
+
+ pub fn right_dock(&self) -> &ViewHandle<Dock> {
+ &self.right_dock
+ }
+
+ pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
+ let dock = match panel.position(cx) {
+ DockPosition::Left => &self.left_dock,
+ DockPosition::Bottom => &self.bottom_dock,
+ DockPosition::Right => &self.right_dock,
+ };
+
+ self.subscriptions.push(cx.subscribe(&panel, {
+ let mut dock = dock.clone();
+ let mut prev_position = panel.position(cx);
+ move |this, panel, event, cx| {
+ if T::should_change_position_on_event(event) {
+ let new_position = panel.read(cx).position(cx);
+ let mut was_visible = false;
+ dock.update(cx, |dock, cx| {
+ prev_position = new_position;
+
+ was_visible = dock.is_open()
+ && dock
+ .active_panel()
+ .map_or(false, |active_panel| active_panel.id() == panel.id());
+ dock.remove_panel(&panel, cx);
+ });
+ dock = match panel.read(cx).position(cx) {
+ DockPosition::Left => &this.left_dock,
+ DockPosition::Bottom => &this.bottom_dock,
+ DockPosition::Right => &this.right_dock,
+ }
+ .clone();
+ dock.update(cx, |dock, cx| {
+ dock.add_panel(panel.clone(), cx);
+ if was_visible {
+ dock.set_open(true, cx);
+ dock.activate_panel(dock.panels_len() - 1, cx);
+ }
+ });
+ } else if T::should_zoom_in_on_event(event) {
+ this.zoom_out(cx);
+ dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
+ } else if T::should_zoom_out_on_event(event) {
+ this.zoom_out(cx);
+ } else if T::is_focus_event(event) {
+ cx.notify();
+ }
+ }
+ }));
+
+ dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
}
pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
@@ -1272,6 +1332,44 @@ impl Workspace {
}
}
+ fn zoomed(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
+ self.zoomed_panel_for_dock(DockPosition::Left, cx)
+ .or_else(|| self.zoomed_panel_for_dock(DockPosition::Bottom, cx))
+ .or_else(|| self.zoomed_panel_for_dock(DockPosition::Right, cx))
+ .or_else(|| self.zoomed_pane(cx))
+ }
+
+ fn zoomed_panel_for_dock(
+ &self,
+ position: DockPosition,
+ cx: &WindowContext,
+ ) -> Option<AnyViewHandle> {
+ let (dock, other_docks) = match position {
+ DockPosition::Left => (&self.left_dock, [&self.bottom_dock, &self.right_dock]),
+ DockPosition::Bottom => (&self.bottom_dock, [&self.left_dock, &self.right_dock]),
+ DockPosition::Right => (&self.right_dock, [&self.left_dock, &self.bottom_dock]),
+ };
+
+ let zoomed_panel = dock.read(&cx).zoomed_panel(cx)?;
+ if other_docks.iter().all(|dock| !dock.read(cx).has_focus(cx))
+ && !self.active_pane.read(cx).has_focus()
+ {
+ Some(zoomed_panel.as_any().clone())
+ } else {
+ None
+ }
+ }
+
+ fn zoomed_pane(&self, cx: &WindowContext) -> Option<AnyViewHandle> {
+ let active_pane = self.active_pane.read(cx);
+ let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
+ if active_pane.is_zoomed() && docks.iter().all(|dock| !dock.read(cx).has_focus(cx)) {
+ Some(self.active_pane.clone().into_any())
+ } else {
+ None
+ }
+ }
+
pub fn items<'a>(
&'a self,
cx: &'a AppContext,
@@ -1349,47 +1447,42 @@ impl Workspace {
}
}
- pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
- let sidebar = match sidebar_side {
- SidebarSide::Left => &mut self.left_sidebar,
- SidebarSide::Right => &mut self.right_sidebar,
+ pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
+ let dock = match dock_side {
+ DockPosition::Left => &mut self.left_dock,
+ DockPosition::Bottom => &mut self.bottom_dock,
+ DockPosition::Right => &mut self.right_dock,
};
- let open = sidebar.update(cx, |sidebar, cx| {
- let open = !sidebar.is_open();
- sidebar.set_open(open, cx);
- open
+ dock.update(cx, |dock, cx| {
+ let open = !dock.is_open();
+ dock.set_open(open, cx);
});
- if open {
- Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
- }
-
self.serialize_workspace(cx);
cx.focus_self();
cx.notify();
}
- pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
- let sidebar = match action.sidebar_side {
- SidebarSide::Left => &mut self.left_sidebar,
- SidebarSide::Right => &mut self.right_sidebar,
+ pub fn toggle_panel(&mut self, action: &TogglePanel, cx: &mut ViewContext<Self>) {
+ let dock = match action.dock_position {
+ DockPosition::Left => &mut self.left_dock,
+ DockPosition::Bottom => &mut self.bottom_dock,
+ DockPosition::Right => &mut self.right_dock,
};
- let active_item = sidebar.update(cx, move |sidebar, cx| {
- if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
- sidebar.set_open(false, cx);
+ let active_item = dock.update(cx, move |dock, cx| {
+ if dock.is_open() && dock.active_panel_index() == action.panel_index {
+ dock.set_open(false, cx);
None
} else {
- sidebar.set_open(true, cx);
- sidebar.activate_item(action.item_index, cx);
- sidebar.active_item().cloned()
+ dock.set_open(true, cx);
+ dock.activate_panel(action.panel_index, cx);
+ dock.active_panel().cloned()
}
});
if let Some(active_item) = active_item {
- Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
-
- if active_item.is_focused(cx) {
+ if active_item.has_focus(cx) {
cx.focus_self();
} else {
cx.focus(active_item.as_any());
@@ -1403,32 +1496,37 @@ impl Workspace {
cx.notify();
}
- pub fn toggle_sidebar_item_focus(
- &mut self,
- sidebar_side: SidebarSide,
- item_index: usize,
- cx: &mut ViewContext<Self>,
- ) {
- let sidebar = match sidebar_side {
- SidebarSide::Left => &mut self.left_sidebar,
- SidebarSide::Right => &mut self.right_sidebar,
- };
- let active_item = sidebar.update(cx, |sidebar, cx| {
- sidebar.set_open(true, cx);
- sidebar.activate_item(item_index, cx);
- sidebar.active_item().cloned()
- });
- if let Some(active_item) = active_item {
- Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
+ pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
+ for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+ if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
+ let active_item = dock.update(cx, |dock, cx| {
+ dock.set_open(true, cx);
+ dock.activate_panel(panel_index, cx);
+ dock.active_panel().cloned()
+ });
+ if let Some(active_item) = active_item {
+ if active_item.has_focus(cx) {
+ cx.focus_self();
+ } else {
+ cx.focus(active_item.as_any());
+ }
+ }
- if active_item.is_focused(cx) {
- cx.focus_self();
- } else {
- cx.focus(active_item.as_any());
+ self.serialize_workspace(cx);
+ cx.notify();
+ break;
}
}
+ }
- self.serialize_workspace(cx);
+ fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
+ for pane in &self.panes {
+ pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
+ }
+
+ self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
+ self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
+ self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
cx.notify();
}
@@ -1437,7 +1535,6 @@ impl Workspace {
let pane = cx.add_view(|cx| {
Pane::new(
self.weak_handle(),
- None,
self.app_state.background_actions,
self.pane_history_timestamp.clone(),
cx,
@@ -1480,16 +1577,12 @@ impl Workspace {
cx: &mut ViewContext<Self>,
) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
let pane = pane.unwrap_or_else(|| {
- if !self.dock_active() {
- self.active_pane().downgrade()
- } else {
- self.last_active_center_pane.clone().unwrap_or_else(|| {
- self.panes
- .first()
- .expect("There must be an active pane")
- .downgrade()
- })
- }
+ self.last_active_center_pane.clone().unwrap_or_else(|| {
+ self.panes
+ .first()
+ .expect("There must be an active pane")
+ .downgrade()
+ })
});
let task = self.load_path(path.into(), cx);
@@ -1568,9 +1661,6 @@ impl Workspace {
.map(|ix| (pane.clone(), ix))
});
if let Some((pane, ix)) = result {
- if &pane == self.dock_pane() {
- Dock::show(self, false, cx);
- }
pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
true
} else {
@@ -1616,16 +1706,7 @@ impl Workspace {
status_bar.set_active_pane(&self.active_pane, cx);
});
self.active_item_path_changed(cx);
-
- if &pane == self.dock_pane() {
- Dock::show(self, false, cx);
- } else {
- self.last_active_center_pane = Some(pane.downgrade());
- if self.dock.is_anchored_at(DockAnchor::Expanded) {
- Dock::hide(self, cx);
- }
- }
- cx.notify();
+ self.last_active_center_pane = Some(pane.downgrade());
}
self.update_followers(
@@ -1639,6 +1720,8 @@ impl Workspace {
}),
cx,
);
+
+ cx.notify();
}
fn handle_pane_event(
@@ -1647,13 +1730,11 @@ impl Workspace {
event: &pane::Event,
cx: &mut ViewContext<Self>,
) {
- let is_dock = &pane == self.dock.pane();
match event {
- pane::Event::Split(direction) if !is_dock => {
+ pane::Event::Split(direction) => {
self.split_pane(pane, *direction, cx);
}
- pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
- pane::Event::Remove if is_dock => Dock::hide(self, cx),
+ pane::Event::Remove => self.remove_pane(pane, cx),
pane::Event::ActivateItem { local } => {
if *local {
self.unfollow(&pane, cx);
@@ -1679,7 +1760,14 @@ impl Workspace {
pane::Event::Focus => {
self.handle_pane_focused(pane.clone(), cx);
}
- _ => {}
+ pane::Event::ZoomIn => {
+ if pane == self.active_pane {
+ self.zoom_out(cx);
+ pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
+ cx.notify();
+ }
+ }
+ pane::Event::ZoomOut => self.zoom_out(cx),
}
self.serialize_workspace(cx);
@@ -1691,11 +1779,6 @@ impl Workspace {
direction: SplitDirection,
cx: &mut ViewContext<Self>,
) -> Option<ViewHandle<Pane>> {
- if &pane == self.dock_pane() {
- warn!("Can't split dock pane.");
- return None;
- }
-
let item = pane.read(cx).active_item()?;
let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
let new_pane = self.add_pane(cx);
@@ -1719,10 +1802,6 @@ impl Workspace {
) {
let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; };
let Some(from) = from.upgrade(cx) else { return; };
- if &pane_to_split == self.dock_pane() {
- warn!("Can't split dock pane.");
- return;
- }
let new_pane = self.add_pane(cx);
Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx);
@@ -1740,11 +1819,6 @@ impl Workspace {
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let pane_to_split = pane_to_split.upgrade(cx)?;
- if &pane_to_split == self.dock_pane() {
- warn!("Can't split dock pane.");
- return None;
- }
-
let new_pane = self.add_pane(cx);
self.center
.split(&pane_to_split, &new_pane, split_direction)
@@ -1781,14 +1855,6 @@ impl Workspace {
&self.active_pane
}
- pub fn dock_pane(&self) -> &ViewHandle<Pane> {
- self.dock.pane()
- }
-
- fn dock_active(&self) -> bool {
- &self.active_pane == self.dock.pane()
- }
-
fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
if let Some(remote_id) = remote_id {
self.remote_entity_subscription = Some(
@@ -2530,23 +2596,65 @@ impl Workspace {
}
}
+ fn build_serialized_docks(this: &Workspace, cx: &AppContext) -> DockStructure {
+ let left_dock = this.left_dock.read(cx);
+ let left_visible = left_dock.is_open();
+ let left_active_panel = left_dock.active_panel().and_then(|panel| {
+ Some(
+ cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+ .to_string(),
+ )
+ });
+
+ let right_dock = this.right_dock.read(cx);
+ let right_visible = right_dock.is_open();
+ let right_active_panel = right_dock.active_panel().and_then(|panel| {
+ Some(
+ cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+ .to_string(),
+ )
+ });
+
+ let bottom_dock = this.bottom_dock.read(cx);
+ let bottom_visible = bottom_dock.is_open();
+ let bottom_active_panel = bottom_dock.active_panel().and_then(|panel| {
+ Some(
+ cx.view_ui_name(panel.as_any().window_id(), panel.id())?
+ .to_string(),
+ )
+ });
+
+ DockStructure {
+ left: DockData {
+ visible: left_visible,
+ active_panel: left_active_panel,
+ },
+ right: DockData {
+ visible: right_visible,
+ active_panel: right_active_panel,
+ },
+ bottom: DockData {
+ visible: bottom_visible,
+ active_panel: bottom_active_panel,
+ },
+ }
+ }
+
if let Some(location) = self.location(cx) {
// Load bearing special case:
// - with_local_workspace() relies on this to not have other stuff open
// when you open your log
if !location.paths().is_empty() {
- let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
let center_group = build_serialized_pane_group(&self.center.root, cx);
+ let docks = build_serialized_docks(self, cx);
let serialized_workspace = SerializedWorkspace {
id: self.database_id,
location,
- dock_position: self.dock.position(),
- dock_pane,
center_group,
- left_sidebar_open: self.left_sidebar.read(cx).is_open(),
bounds: Default::default(),
display: Default::default(),
+ docks,
};
cx.background()
@@ -2564,26 +2672,14 @@ impl Workspace {
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
cx.spawn(|mut cx| async move {
let result = async_iife! {{
- let (project, dock_pane_handle, old_center_pane) =
+ let (project, old_center_pane) =
workspace.read_with(&cx, |workspace, _| {
(
workspace.project().clone(),
- workspace.dock_pane().downgrade(),
workspace.last_active_center_pane.clone(),
)
})?;
- let dock_items = serialized_workspace
- .dock_pane
- .deserialize_to(
- &project,
- &dock_pane_handle,
- serialized_workspace.id,
- &workspace,
- &mut cx,
- )
- .await?;
-
let mut center_items = None;
let mut center_group = None;
// Traverse the splits tree and add to things
@@ -2599,7 +2695,6 @@ impl Workspace {
let mut opened_items = center_items
.unwrap_or_default()
.into_iter()
- .chain(dock_items.into_iter())
.filter_map(|item| {
let item = item?;
let project_path = item.project_path(cx)?;
@@ -2645,22 +2740,30 @@ impl Workspace {
}
}
- if workspace.left_sidebar().read(cx).is_open()
- != serialized_workspace.left_sidebar_open
- {
- workspace.toggle_sidebar(SidebarSide::Left, cx);
- }
-
- // Note that without after_window, the focus_self() and
- // the focus the dock generates start generating alternating
- // focus due to the deferred execution each triggering each other
- cx.after_window_update(move |workspace, cx| {
- Dock::set_dock_position(
- workspace,
- serialized_workspace.dock_position,
- false,
- cx,
- );
+ let docks = serialized_workspace.docks;
+ workspace.left_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.left.visible, cx);
+ if let Some(active_panel) = docks.left.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
+ }
+ }
+ });
+ workspace.right_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.right.visible, cx);
+ if let Some(active_panel) = docks.right.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
+ }
+ }
+ });
+ workspace.bottom_dock.update(cx, |dock, cx| {
+ dock.set_open(docks.bottom.visible, cx);
+ if let Some(active_panel) = docks.bottom.active_panel {
+ if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+ dock.activate_panel(ix, cx);
+ }
+ }
});
cx.notify();
@@ -2684,12 +2787,38 @@ impl Workspace {
user_store: project.read(cx).user_store(),
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
- initialize_workspace: |_, _, _| {},
- dock_default_item_factory: |_, _| None,
+ initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
background_actions: || &[],
});
Self::new(0, project, app_state, cx)
}
+
+ fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
+ let dock = match position {
+ DockPosition::Left => &self.left_dock,
+ DockPosition::Right => &self.right_dock,
+ DockPosition::Bottom => &self.bottom_dock,
+ };
+ let active_panel = dock.read(cx).active_panel()?;
+ let element = if Some(active_panel.as_any()) == self.zoomed(cx).as_ref() {
+ dock.read(cx).render_placeholder(cx)
+ } else {
+ ChildView::new(dock, cx).into_any()
+ };
+
+ Some(
+ element
+ .constrained()
+ .dynamically(move |constraint, _, cx| match position {
+ DockPosition::Left | DockPosition::Right => SizeConstraint::new(
+ Vector2F::new(20., constraint.min.y()),
+ Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
+ ),
+ _ => constraint,
+ })
+ .into_any(),
+ )
+ }
}
async fn open_items(
@@ -2835,76 +2964,46 @@ impl View for Workspace {
.with_child({
let project = self.project.clone();
Flex::row()
- .with_children(
- if self.left_sidebar.read(cx).active_item().is_some() {
- Some(
- ChildView::new(&self.left_sidebar, cx)
- .constrained()
- .dynamically(|constraint, _, cx| {
- SizeConstraint::new(
- Vector2F::new(20., constraint.min.y()),
- Vector2F::new(
- cx.window_size().x() * 0.8,
- constraint.max.y(),
- ),
- )
- }),
- )
- } else {
- None
- },
- )
+ .with_children(self.render_dock(DockPosition::Left, cx))
.with_child(
- FlexItem::new(
- Flex::column()
- .with_child(
- FlexItem::new(self.center.render(
- &project,
- &theme,
- &self.follower_states_by_leader,
- self.active_call(),
- self.active_pane(),
- &self.app_state,
- cx,
- ))
- .flex(1., true),
- )
- .with_children(self.dock.render(
+ Flex::column()
+ .with_child(
+ FlexItem::new(self.center.render(
+ &project,
&theme,
- DockAnchor::Bottom,
+ &self.follower_states_by_leader,
+ self.active_call(),
+ self.active_pane(),
+ self.zoomed(cx).as_ref(),
+ &self.app_state,
cx,
- )),
- )
- .flex(1., true),
- )
- .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
- .with_children(
- if self.right_sidebar.read(cx).active_item().is_some() {
- Some(
- ChildView::new(&self.right_sidebar, cx)
- .constrained()
- .dynamically(|constraint, _, cx| {
- SizeConstraint::new(
- Vector2F::new(20., constraint.min.y()),
- Vector2F::new(
- cx.window_size().x() * 0.8,
- constraint.max.y(),
- ),
- )
- }),
+ ))
+ .flex(1., true),
)
- } else {
- None
- },
+ .with_children(
+ self.render_dock(DockPosition::Bottom, cx),
+ )
+ .flex(1., true),
)
+ .with_children(self.render_dock(DockPosition::Right, cx))
})
.with_child(Overlay::new(
Stack::new()
- .with_children(self.dock.render(
- &theme,
- DockAnchor::Expanded,
- cx,
- ))
+ .with_children(self.zoomed(cx).map(|zoomed| {
+ enum ZoomBackground {}
+
+ ChildView::new(&zoomed, cx)
+ .contained()
+ .with_style(theme.workspace.zoomed_foreground)
+ .aligned()
+ .contained()
+ .with_style(theme.workspace.zoomed_background)
+ .mouse::<ZoomBackground>(0)
+ .capture_all()
+ .on_down(MouseButton::Left, |_, this: &mut Self, cx| {
+ this.zoom_out(cx);
+ })
+ }))
.with_children(self.modal.as_ref().map(|modal| {
ChildView::new(modal, cx)
.contained()
@@ -1,8 +1,3 @@
-use anyhow::bail;
-use db::sqlez::{
- bindable::{Bind, Column, StaticColumnCount},
- statement::Statement,
-};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Setting;
@@ -13,7 +8,6 @@ pub struct WorkspaceSettings {
pub confirm_quit: bool,
pub show_call_status_icon: bool,
pub autosave: AutosaveSetting,
- pub default_dock_anchor: DockAnchor,
pub git: GitSettings,
}
@@ -23,7 +17,6 @@ pub struct WorkspaceSettingsContent {
pub confirm_quit: Option<bool>,
pub show_call_status_icon: Option<bool>,
pub autosave: Option<AutosaveSetting>,
- pub default_dock_anchor: Option<DockAnchor>,
pub git: Option<GitSettings>,
}
@@ -36,15 +29,6 @@ pub enum AutosaveSetting {
OnWindowChange,
}
-#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum DockAnchor {
- #[default]
- Bottom,
- Right,
- Expanded,
-}
-
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct GitSettings {
pub git_gutter: Option<GitGutterSetting>,
@@ -59,35 +43,6 @@ pub enum GitGutterSetting {
Hide,
}
-impl StaticColumnCount for DockAnchor {}
-
-impl Bind for DockAnchor {
- fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
- match self {
- DockAnchor::Bottom => "Bottom",
- DockAnchor::Right => "Right",
- DockAnchor::Expanded => "Expanded",
- }
- .bind(statement, start_index)
- }
-}
-
-impl Column for DockAnchor {
- fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
- String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
- Ok((
- match anchor_text.as_ref() {
- "Bottom" => DockAnchor::Bottom,
- "Right" => DockAnchor::Right,
- "Expanded" => DockAnchor::Expanded,
- _ => bail!("Stored dock anchor is incorrect"),
- },
- next_index,
- ))
- })
- }
-}
-
impl Setting for WorkspaceSettings {
const KEY: Option<&'static str> = None;
@@ -56,8 +56,7 @@ use fs::RealFs;
use staff_mode::StaffMode;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
use workspace::{
- dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings,
- Workspace,
+ item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, Workspace,
};
use zed::{
self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
@@ -186,7 +185,6 @@ fn main() {
fs,
build_window_options,
initialize_workspace,
- dock_default_item_factory,
background_actions,
});
cx.set_global(Arc::downgrade(&app_state));
@@ -816,7 +814,6 @@ pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
&[
("Go to file", &file_finder::Toggle),
("Open command palette", &command_palette::Toggle),
- ("Focus the dock", &FocusDock),
("Open recent projects", &recent_projects::OpenRecent),
("Change your settings", &OpenSettings),
]
@@ -89,7 +89,7 @@ pub fn menus() -> Vec<Menu<'static>> {
MenuItem::action("Zoom Out", super::DecreaseBufferFontSize),
MenuItem::action("Reset Zoom", super::ResetBufferFontSize),
MenuItem::separator(),
- MenuItem::action("Toggle Left Sidebar", workspace::ToggleLeftSidebar),
+ MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock),
MenuItem::submenu(Menu {
name: "Editor Layout",
items: vec![
@@ -18,10 +18,11 @@ use feedback::{
use futures::{channel::mpsc, StreamExt};
use gpui::{
actions,
+ anyhow::{self, Result},
geometry::vector::vec2f,
impl_actions,
platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
- AppContext, ViewContext,
+ AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
};
pub use lsp;
pub use project;
@@ -31,13 +32,13 @@ use serde::Deserialize;
use serde_json::to_string_pretty;
use settings::{KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH};
use std::{borrow::Cow, str, sync::Arc};
-use terminal_view::terminal_button::TerminalButton;
+use terminal_view::terminal_panel::{self, TerminalPanel};
use util::{channel::ReleaseChannel, paths, ResultExt};
use uuid::Uuid;
use welcome::BaseKeymap;
pub use workspace;
use workspace::{
- create_and_open_local_file, open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow,
+ create_and_open_local_file, dock::PanelHandle, open_new, AppState, NewFile, NewWindow,
Workspace, WorkspaceSettings,
};
@@ -223,7 +224,14 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|workspace: &mut Workspace,
_: &project_panel::ToggleFocus,
cx: &mut ViewContext<Workspace>| {
- workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
+ workspace.toggle_panel_focus::<ProjectPanel>(cx);
+ },
+ );
+ cx.add_action(
+ |workspace: &mut Workspace,
+ _: &terminal_panel::ToggleFocus,
+ cx: &mut ViewContext<Workspace>| {
+ workspace.toggle_panel_focus::<TerminalPanel>(cx);
},
);
cx.add_global_action({
@@ -252,85 +260,107 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
}
pub fn initialize_workspace(
- workspace: &mut Workspace,
- app_state: &Arc<AppState>,
- cx: &mut ViewContext<Workspace>,
-) {
- let workspace_handle = cx.handle();
- cx.subscribe(&workspace_handle, {
- move |workspace, _, event, cx| {
- if let workspace::Event::PaneAdded(pane) = event {
- pane.update(cx, |pane, cx| {
- pane.toolbar().update(cx, |toolbar, cx| {
- let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
- toolbar.add_item(breadcrumbs, cx);
- let buffer_search_bar = cx.add_view(BufferSearchBar::new);
- toolbar.add_item(buffer_search_bar, cx);
- let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
- toolbar.add_item(project_search_bar, cx);
- let submit_feedback_button = cx.add_view(|_| SubmitFeedbackButton::new());
- toolbar.add_item(submit_feedback_button, cx);
- let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
- toolbar.add_item(feedback_info_text, cx);
- let lsp_log_item = cx.add_view(|_| {
- lsp_log::LspLogToolbarItemView::new(workspace.project().clone())
+ workspace_handle: WeakViewHandle<Workspace>,
+ was_deserialized: bool,
+ app_state: Arc<AppState>,
+ cx: AsyncAppContext,
+) -> Task<Result<()>> {
+ cx.spawn(|mut cx| async move {
+ workspace_handle.update(&mut cx, |workspace, cx| {
+ let workspace_handle = cx.handle();
+ cx.subscribe(&workspace_handle, {
+ move |workspace, _, event, cx| {
+ if let workspace::Event::PaneAdded(pane) = event {
+ pane.update(cx, |pane, cx| {
+ pane.toolbar().update(cx, |toolbar, cx| {
+ let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
+ toolbar.add_item(breadcrumbs, cx);
+ let buffer_search_bar = cx.add_view(BufferSearchBar::new);
+ toolbar.add_item(buffer_search_bar, cx);
+ let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
+ toolbar.add_item(project_search_bar, cx);
+ let submit_feedback_button =
+ cx.add_view(|_| SubmitFeedbackButton::new());
+ toolbar.add_item(submit_feedback_button, cx);
+ let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
+ toolbar.add_item(feedback_info_text, cx);
+ let lsp_log_item = cx.add_view(|_| {
+ lsp_log::LspLogToolbarItemView::new(workspace.project().clone())
+ });
+ toolbar.add_item(lsp_log_item, cx);
+ })
});
- toolbar.add_item(lsp_log_item, cx);
- })
- });
- }
- }
- })
- .detach();
+ }
+ }
+ })
+ .detach();
- cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
- cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
+ cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
- let collab_titlebar_item =
- cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
- workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
+ let collab_titlebar_item =
+ cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
+ workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
- let project_panel = ProjectPanel::new(workspace, cx);
- workspace.left_sidebar().update(cx, |sidebar, cx| {
- sidebar.add_item(
- "icons/folder_tree_16.svg",
- "Project Panel".to_string(),
- project_panel,
- cx,
- )
- });
+ let copilot =
+ cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
+ let diagnostic_summary =
+ cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
+ let activity_indicator = activity_indicator::ActivityIndicator::new(
+ workspace,
+ app_state.languages.clone(),
+ cx,
+ );
+ let active_buffer_language =
+ cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
+ let feedback_button = cx.add_view(|_| {
+ feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
+ });
+ let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
+ workspace.status_bar().update(cx, |status_bar, cx| {
+ status_bar.add_left_item(diagnostic_summary, cx);
+ status_bar.add_left_item(activity_indicator, cx);
+ status_bar.add_right_item(feedback_button, cx);
+ status_bar.add_right_item(copilot, cx);
+ status_bar.add_right_item(active_buffer_language, cx);
+ status_bar.add_right_item(cursor_position, cx);
+ });
- let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
- let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
- let diagnostic_summary =
- cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
- let activity_indicator =
- activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
- let active_buffer_language =
- cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
- let feedback_button =
- cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace));
- let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
- workspace.status_bar().update(cx, |status_bar, cx| {
- status_bar.add_left_item(diagnostic_summary, cx);
- status_bar.add_left_item(activity_indicator, cx);
- status_bar.add_right_item(toggle_terminal, cx);
- status_bar.add_right_item(feedback_button, cx);
- status_bar.add_right_item(copilot, cx);
- status_bar.add_right_item(active_buffer_language, cx);
- status_bar.add_right_item(cursor_position, cx);
- });
+ auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
- auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
+ vim::observe_keystrokes(cx);
- vim::observe_keystrokes(cx);
+ cx.on_window_should_close(|workspace, cx| {
+ if let Some(task) = workspace.close(&Default::default(), cx) {
+ task.detach_and_log_err(cx);
+ }
+ false
+ });
+ })?;
+
+ 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);
+ workspace.add_panel(project_panel, cx);
+ if !was_deserialized
+ && workspace
+ .project()
+ .read(cx)
+ .visible_worktrees(cx)
+ .any(|tree| {
+ tree.read(cx)
+ .root_entry()
+ .map_or(false, |entry| entry.is_dir())
+ })
+ {
+ workspace.toggle_dock(project_panel_position, cx);
+ }
- cx.on_window_should_close(|workspace, cx| {
- if let Some(task) = workspace.close(&Default::default(), cx) {
- task.detach_and_log_err(cx);
- }
- false
- });
+ workspace.add_panel(terminal_panel, cx)
+ })?;
+ Ok(())
+ })
}
pub fn build_window_options(
@@ -348,7 +378,8 @@ pub fn build_window_options(
traffic_light_position: Some(vec2f(8., 8.)),
}),
center: false,
- focus: true,
+ focus: false,
+ show: false,
kind: WindowKind::Normal,
is_movable: true,
bounds,
@@ -687,7 +718,7 @@ mod tests {
.unwrap();
workspace_1.update(cx, |workspace, cx| {
assert_eq!(workspace.worktrees(cx).count(), 2);
- assert!(workspace.left_sidebar().read(cx).is_open());
+ assert!(workspace.left_dock().read(cx).is_open());
assert!(workspace.active_pane().is_focused(cx));
});
@@ -730,7 +761,7 @@ mod tests {
.collect::<Vec<_>>(),
&[Path::new("/root/c").into(), Path::new("/root/d").into()]
);
- assert!(workspace.left_sidebar().read(cx).is_open());
+ assert!(workspace.left_dock().read(cx).is_open());
assert!(workspace.active_pane().is_focused(cx));
});
}
@@ -755,6 +786,7 @@ mod tests {
.unwrap()
.downcast::<Workspace>()
.unwrap();
+ let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let editor = workspace.read_with(cx, |workspace, cx| {
workspace
.active_item(cx)
@@ -777,9 +809,9 @@ mod tests {
assert!(cx.is_window_edited(workspace.window_id()));
// Closing the item restores the window's edited state.
- let close = workspace.update(cx, |workspace, cx| {
+ let close = pane.update(cx, |pane, cx| {
drop(editor);
- Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
+ pane.close_active_item(&Default::default(), cx).unwrap()
});
executor.run_until_parked();
cx.simulate_prompt_answer(workspace.window_id(), 1);
@@ -1364,7 +1396,7 @@ mod tests {
cx.foreground().run_until_parked();
workspace.read_with(cx, |workspace, _| {
- assert_eq!(workspace.panes().len(), 2); //Center pane + Dock pane
+ assert_eq!(workspace.panes().len(), 1);
assert_eq!(workspace.active_pane(), &pane_1);
});
@@ -1374,7 +1406,7 @@ mod tests {
cx.foreground().run_until_parked();
workspace.read_with(cx, |workspace, cx| {
- assert_eq!(workspace.panes().len(), 2);
+ assert_eq!(workspace.panes().len(), 1);
assert!(workspace.active_item(cx).is_none());
});
@@ -1403,6 +1435,7 @@ mod tests {
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let entries = cx.read(|cx| workspace.file_project_paths(cx));
let file1 = entries[0].clone();
@@ -1520,14 +1553,13 @@ mod tests {
// Go forward to an item that has been closed, ensuring it gets re-opened at the same
// location.
- workspace
- .update(cx, |workspace, cx| {
- let editor3_id = editor3.id();
- drop(editor3);
- Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor3_id, cx)
- })
- .await
- .unwrap();
+ pane.update(cx, |pane, cx| {
+ let editor3_id = editor3.id();
+ drop(editor3);
+ pane.close_item_by_id(editor3_id, cx)
+ })
+ .await
+ .unwrap();
workspace
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
.await
@@ -1556,14 +1588,13 @@ mod tests {
);
// Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
- workspace
- .update(cx, |workspace, cx| {
- let editor2_id = editor2.id();
- drop(editor2);
- Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor2_id, cx)
- })
- .await
- .unwrap();
+ pane.update(cx, |pane, cx| {
+ let editor2_id = editor2.id();
+ drop(editor2);
+ pane.close_item_by_id(editor2_id, cx)
+ })
+ .await
+ .unwrap();
app_state
.fs
.remove_file(Path::new("/root/a/file2"), Default::default())
@@ -1712,34 +1743,22 @@ mod tests {
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
// Close all the pane items in some arbitrary order.
- workspace
- .update(cx, |workspace, cx| {
- Pane::close_item_by_id(workspace, pane.clone(), file1_item_id, cx)
- })
+ pane.update(cx, |pane, cx| pane.close_item_by_id(file1_item_id, cx))
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
- workspace
- .update(cx, |workspace, cx| {
- Pane::close_item_by_id(workspace, pane.clone(), file4_item_id, cx)
- })
+ pane.update(cx, |pane, cx| pane.close_item_by_id(file4_item_id, cx))
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
- workspace
- .update(cx, |workspace, cx| {
- Pane::close_item_by_id(workspace, pane.clone(), file2_item_id, cx)
- })
+ pane.update(cx, |pane, cx| pane.close_item_by_id(file2_item_id, cx))
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
- workspace
- .update(cx, |workspace, cx| {
- Pane::close_item_by_id(workspace, pane.clone(), file3_item_id, cx)
- })
+ pane.update(cx, |pane, cx| pane.close_item_by_id(file3_item_id, cx))
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), None);
@@ -2070,6 +2089,8 @@ mod tests {
editor::init(cx);
project_panel::init_settings(cx);
pane::init(cx);
+ project_panel::init(cx);
+ terminal_view::init(cx);
app_state
})
}
@@ -93,10 +93,11 @@ export default function statusBar(colorScheme: ColorScheme) {
},
},
},
- sidebarButtons: {
+ panelButtons: {
groupLeft: {},
+ groupBottom: {},
groupRight: {},
- item: {
+ button: {
...statusContainer,
iconSize: 16,
iconColor: foreground(layer, "variant"),
@@ -118,9 +118,25 @@ export default function workspace(colorScheme: ColorScheme) {
},
cursor: "Arrow",
},
- sidebar: {
- initialSize: 240,
- border: border(layer, { left: true, right: true }),
+ zoomedBackground: {
+ padding: 10,
+ cursor: "Arrow",
+ background: withOpacity(background(colorScheme.lowest), 0.5)
+ },
+ zoomedForeground: {
+ shadow: colorScheme.modalShadow,
+ border: border(colorScheme.highest, { overlay: true }),
+ },
+ dock: {
+ left: {
+ border: border(layer, { right: true }),
+ },
+ bottom: {
+ border: border(layer, { top: true }),
+ },
+ right: {
+ border: border(layer, { left: true }),
+ }
},
paneDivider: {
color: borderColor(layer),
@@ -310,19 +326,6 @@ export default function workspace(colorScheme: ColorScheme) {
width: 400,
margin: { right: 10, bottom: 10 },
},
- dock: {
- initialSizeRight: 640,
- initialSizeBottom: 304,
- wash_color: withOpacity(background(colorScheme.highest), 0.5),
- panel: {
- border: border(colorScheme.middle),
- },
- maximized: {
- margin: 32,
- border: border(colorScheme.highest, { overlay: true }),
- shadow: colorScheme.modalShadow,
- },
- },
dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5),
}
}