Move workspace
module into its own crate
This commit is contained in:
parent
2087c4731f
commit
499616d769
24 changed files with 1696 additions and 1584 deletions
|
@ -1,4 +1,3 @@
|
|||
use crate::{settings::Settings, workspace::Workspace};
|
||||
use editor::{Editor, EditorSettings};
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{
|
||||
|
@ -23,6 +22,7 @@ use std::{
|
|||
},
|
||||
};
|
||||
use util::post_inc;
|
||||
use workspace::{Settings, Workspace};
|
||||
|
||||
pub struct FileFinder {
|
||||
handle: WeakViewHandle<Self>,
|
||||
|
@ -422,16 +422,15 @@ impl FileFinder {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{test::test_app_state, workspace::Workspace};
|
||||
use editor::Insert;
|
||||
use project::fs::FakeFs;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use workspace::{Workspace, WorkspaceParams};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_paths(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
|
@ -449,7 +448,7 @@ mod tests {
|
|||
editor::init(cx);
|
||||
});
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root"), cx)
|
||||
|
@ -493,7 +492,8 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_matching_cancellation(mut cx: gpui::TestAppContext) {
|
||||
let fs = Arc::new(FakeFs::new());
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
let fs = params.fs.as_fake();
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
@ -508,10 +508,7 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
|
||||
let mut app_state = cx.update(test_app_state);
|
||||
Arc::get_mut(&mut app_state).unwrap().fs = fs;
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree("/dir".as_ref(), cx)
|
||||
|
@ -522,7 +519,7 @@ mod tests {
|
|||
.await;
|
||||
let (_, finder) = cx.add_window(|cx| {
|
||||
FileFinder::new(
|
||||
app_state.settings.clone(),
|
||||
params.settings.clone(),
|
||||
workspace.read(cx).project().clone(),
|
||||
cx,
|
||||
)
|
||||
|
@ -569,14 +566,14 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_single_file_worktrees(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
|
||||
.await;
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root/the-parent-dir/the-file"), cx)
|
||||
|
@ -587,7 +584,7 @@ mod tests {
|
|||
.await;
|
||||
let (_, finder) = cx.add_window(|cx| {
|
||||
FileFinder::new(
|
||||
app_state.settings.clone(),
|
||||
params.settings.clone(),
|
||||
workspace.read(cx).project().clone(),
|
||||
cx,
|
||||
)
|
||||
|
@ -622,8 +619,8 @@ mod tests {
|
|||
|
||||
#[gpui::test(retries = 5)]
|
||||
async fn test_multiple_matches_with_same_relative_path(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
|
@ -635,7 +632,7 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
|
@ -650,7 +647,7 @@ mod tests {
|
|||
|
||||
let (_, finder) = cx.add_window(|cx| {
|
||||
FileFinder::new(
|
||||
app_state.settings.clone(),
|
||||
params.settings.clone(),
|
||||
workspace.read(cx).project().clone(),
|
||||
cx,
|
||||
)
|
||||
|
|
|
@ -5,11 +5,9 @@ pub mod language;
|
|||
pub mod menus;
|
||||
pub mod people_panel;
|
||||
pub mod project_panel;
|
||||
pub mod settings;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
pub mod theme_selector;
|
||||
pub mod workspace;
|
||||
|
||||
pub use buffer;
|
||||
use buffer::LanguageRegistry;
|
||||
|
@ -28,18 +26,15 @@ use people_panel::PeoplePanel;
|
|||
use postage::watch;
|
||||
pub use project::{self, fs};
|
||||
use project_panel::ProjectPanel;
|
||||
pub use settings::Settings;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use theme::ThemeRegistry;
|
||||
use util::TryFutureExt;
|
||||
|
||||
use crate::workspace::Workspace;
|
||||
pub use workspace;
|
||||
use workspace::{Settings, Workspace, WorkspaceParams};
|
||||
|
||||
action!(About);
|
||||
action!(Open, Arc<AppState>);
|
||||
action!(OpenPaths, OpenParams);
|
||||
action!(Quit);
|
||||
action!(Authenticate);
|
||||
action!(AdjustBufferFontSize, f32);
|
||||
|
||||
const MIN_FONT_SIZE: f32 = 6.0;
|
||||
|
@ -69,15 +64,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
|||
cx.add_global_action(open_new);
|
||||
cx.add_global_action(quit);
|
||||
|
||||
cx.add_global_action({
|
||||
let rpc = app_state.client.clone();
|
||||
move |_: &Authenticate, cx| {
|
||||
let rpc = rpc.clone();
|
||||
cx.spawn(|cx| async move { rpc.authenticate_and_connect(&cx).log_err().await })
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
|
||||
cx.add_global_action({
|
||||
let settings_tx = app_state.settings_tx.clone();
|
||||
|
||||
|
@ -135,8 +121,9 @@ fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> {
|
|||
log::info!("open new workspace");
|
||||
|
||||
// Add a new workspace if necessary
|
||||
let app_state = &action.0.app_state;
|
||||
let (_, workspace) = cx.add_window(window_options(), |cx| {
|
||||
build_workspace(&action.0.app_state, cx)
|
||||
build_workspace(&WorkspaceParams::from(app_state.as_ref()), cx)
|
||||
});
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_paths(&action.0.paths, cx)
|
||||
|
@ -145,33 +132,31 @@ fn open_paths(action: &OpenPaths, cx: &mut MutableAppContext) -> Task<()> {
|
|||
|
||||
fn open_new(action: &workspace::OpenNew, cx: &mut MutableAppContext) {
|
||||
cx.add_window(window_options(), |cx| {
|
||||
let mut workspace = build_workspace(action.0.as_ref(), cx);
|
||||
let mut workspace = build_workspace(&action.0, cx);
|
||||
workspace.open_new_file(&action, cx);
|
||||
workspace
|
||||
});
|
||||
}
|
||||
|
||||
fn build_workspace(app_state: &AppState, cx: &mut ViewContext<Workspace>) -> Workspace {
|
||||
let mut workspace = Workspace::new(app_state, cx);
|
||||
fn build_workspace(params: &WorkspaceParams, cx: &mut ViewContext<Workspace>) -> Workspace {
|
||||
let mut workspace = Workspace::new(params, cx);
|
||||
let project = workspace.project().clone();
|
||||
workspace.left_sidebar_mut().add_item(
|
||||
"icons/folder-tree-16.svg",
|
||||
ProjectPanel::new(project, app_state.settings.clone(), cx).into(),
|
||||
ProjectPanel::new(project, params.settings.clone(), cx).into(),
|
||||
);
|
||||
workspace.right_sidebar_mut().add_item(
|
||||
"icons/user-16.svg",
|
||||
cx.add_view(|cx| {
|
||||
PeoplePanel::new(app_state.user_store.clone(), app_state.settings.clone(), cx)
|
||||
})
|
||||
.into(),
|
||||
cx.add_view(|cx| PeoplePanel::new(params.user_store.clone(), params.settings.clone(), cx))
|
||||
.into(),
|
||||
);
|
||||
workspace.right_sidebar_mut().add_item(
|
||||
"icons/comment-16.svg",
|
||||
cx.add_view(|cx| {
|
||||
ChatPanel::new(
|
||||
app_state.client.clone(),
|
||||
app_state.channel_list.clone(),
|
||||
app_state.settings.clone(),
|
||||
params.client.clone(),
|
||||
params.channel_list.clone(),
|
||||
params.settings.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
@ -193,13 +178,27 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
|
|||
cx.platform().quit();
|
||||
}
|
||||
|
||||
impl<'a> From<&'a AppState> for WorkspaceParams {
|
||||
fn from(state: &'a AppState) -> Self {
|
||||
Self {
|
||||
client: state.client.clone(),
|
||||
fs: state.fs.clone(),
|
||||
languages: state.languages.clone(),
|
||||
settings: state.settings.clone(),
|
||||
user_store: state.user_store.clone(),
|
||||
channel_list: state.channel_list.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{test::test_app_state, workspace::ItemView};
|
||||
use serde_json::json;
|
||||
use test::test_app_state;
|
||||
use theme::DEFAULT_THEME_NAME;
|
||||
use util::test::temp_tree;
|
||||
use workspace::ItemView;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_paths_action(mut cx: gpui::TestAppContext) {
|
||||
|
@ -270,7 +269,7 @@ mod tests {
|
|||
async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
cx.update(|cx| init(&app_state, cx));
|
||||
cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
|
||||
cx.dispatch_global_action(workspace::OpenNew(app_state.as_ref().into()));
|
||||
let window_id = *cx.window_ids().first().unwrap();
|
||||
let workspace = cx.root_view::<Workspace>(window_id).unwrap();
|
||||
let editor = workspace.update(&mut cx, |workspace, cx| {
|
||||
|
|
|
@ -8,6 +8,7 @@ use parking_lot::Mutex;
|
|||
use simplelog::SimpleLogger;
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
use theme::ThemeRegistry;
|
||||
use workspace::{self, settings, OpenNew};
|
||||
use zed::{
|
||||
self,
|
||||
assets::Assets,
|
||||
|
@ -15,9 +16,7 @@ use zed::{
|
|||
client::{http, ChannelList, UserStore},
|
||||
editor, file_finder,
|
||||
fs::RealFs,
|
||||
language, menus, people_panel, project_panel, settings, theme_selector,
|
||||
workspace::{self, OpenNew},
|
||||
AppState, OpenParams, OpenPaths,
|
||||
language, menus, people_panel, project_panel, theme_selector, AppState, OpenParams, OpenPaths,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
|
@ -54,6 +53,7 @@ fn main() {
|
|||
});
|
||||
|
||||
zed::init(&app_state, cx);
|
||||
client::init(app_state.client.clone(), cx);
|
||||
workspace::init(cx);
|
||||
editor::init(cx);
|
||||
file_finder::init(cx);
|
||||
|
@ -70,7 +70,7 @@ fn main() {
|
|||
|
||||
let paths = collect_path_args();
|
||||
if paths.is_empty() {
|
||||
cx.dispatch_global_action(OpenNew(app_state));
|
||||
cx.dispatch_global_action(OpenNew(app_state.as_ref().into()));
|
||||
} else {
|
||||
cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
|
||||
}
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
use crate::{workspace, AppState};
|
||||
use crate::{AppState, WorkspaceParams};
|
||||
use gpui::{Menu, MenuItem};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
|
||||
let workspace_params = WorkspaceParams {
|
||||
client: state.client.clone(),
|
||||
fs: state.fs.clone(),
|
||||
languages: state.languages.clone(),
|
||||
settings: state.settings.clone(),
|
||||
user_store: state.user_store.clone(),
|
||||
channel_list: state.channel_list.clone(),
|
||||
};
|
||||
|
||||
vec![
|
||||
Menu {
|
||||
name: "Zed",
|
||||
|
@ -27,7 +36,7 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
|
|||
MenuItem::Action {
|
||||
name: "New",
|
||||
keystroke: Some("cmd-n"),
|
||||
action: Box::new(workspace::OpenNew(state.clone())),
|
||||
action: Box::new(workspace::OpenNew(workspace_params)),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use crate::{workspace::Workspace, Settings};
|
||||
use client::{Collaborator, UserStore};
|
||||
use gpui::{
|
||||
action,
|
||||
|
@ -10,6 +9,7 @@ use gpui::{
|
|||
};
|
||||
use postage::watch;
|
||||
use theme::Theme;
|
||||
use workspace::{Settings, Workspace};
|
||||
|
||||
action!(JoinWorktree, u64);
|
||||
action!(LeaveWorktree, u64);
|
||||
|
|
|
@ -648,7 +648,7 @@ mod tests {
|
|||
.read_with(&cx, |t, _| t.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state.as_ref().into(), cx));
|
||||
let panel = workspace.update(&mut cx, |_, cx| ProjectPanel::new(project, settings, cx));
|
||||
assert_eq!(
|
||||
visible_entry_details(&panel, 0..50, &mut cx),
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use gpui::font_cache::{FamilyId, FontCache};
|
||||
use postage::watch;
|
||||
use std::sync::Arc;
|
||||
use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Settings {
|
||||
pub buffer_font_family: FamilyId,
|
||||
pub buffer_font_size: f32,
|
||||
pub tab_size: usize,
|
||||
pub theme: Arc<Theme>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn new(
|
||||
buffer_font_family: &str,
|
||||
font_cache: &FontCache,
|
||||
theme: Arc<Theme>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
|
||||
buffer_font_size: 16.,
|
||||
tab_size: 4,
|
||||
theme,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_tab_size(mut self, tab_size: usize) -> Self {
|
||||
self.tab_size = tab_size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel(
|
||||
buffer_font_family: &str,
|
||||
font_cache: &FontCache,
|
||||
themes: &ThemeRegistry,
|
||||
) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
|
||||
let theme = match themes.get(DEFAULT_THEME_NAME) {
|
||||
Ok(theme) => theme,
|
||||
Err(err) => {
|
||||
panic!("failed to deserialize default theme: {:?}", err)
|
||||
}
|
||||
};
|
||||
Ok(watch::channel_with(Settings::new(
|
||||
buffer_font_family,
|
||||
font_cache,
|
||||
theme,
|
||||
)?))
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{assets::Assets, language, settings::Settings, AppState};
|
||||
use crate::{assets::Assets, language, AppState};
|
||||
use buffer::LanguageRegistry;
|
||||
use client::{http::ServerResponse, test::FakeHttpClient, ChannelList, Client, UserStore};
|
||||
use gpui::{AssetSource, MutableAppContext};
|
||||
|
@ -7,6 +7,7 @@ use postage::watch;
|
|||
use project::fs::FakeFs;
|
||||
use std::sync::Arc;
|
||||
use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
|
||||
use workspace::Settings;
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,153 +0,0 @@
|
|||
use super::{Item, ItemView};
|
||||
use crate::{project::ProjectPath, Settings};
|
||||
use anyhow::Result;
|
||||
use buffer::{Buffer, File as _};
|
||||
use editor::{Editor, EditorSettings, Event};
|
||||
use gpui::{fonts::TextStyle, AppContext, ModelHandle, Task, ViewContext};
|
||||
use postage::watch;
|
||||
use project::Worktree;
|
||||
use std::path::Path;
|
||||
|
||||
impl Item for Buffer {
|
||||
type View = Editor;
|
||||
|
||||
fn build_view(
|
||||
handle: ModelHandle<Self>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut ViewContext<Self::View>,
|
||||
) -> Self::View {
|
||||
Editor::for_buffer(
|
||||
handle,
|
||||
move |cx| {
|
||||
let settings = settings.borrow();
|
||||
let font_cache = cx.font_cache();
|
||||
let font_family_id = settings.buffer_font_family;
|
||||
let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
|
||||
let font_properties = Default::default();
|
||||
let font_id = font_cache
|
||||
.select_font(font_family_id, &font_properties)
|
||||
.unwrap();
|
||||
let font_size = settings.buffer_font_size;
|
||||
|
||||
let mut theme = settings.theme.editor.clone();
|
||||
theme.text = TextStyle {
|
||||
color: theme.text.color,
|
||||
font_family_name,
|
||||
font_family_id,
|
||||
font_id,
|
||||
font_size,
|
||||
font_properties,
|
||||
underline: false,
|
||||
};
|
||||
EditorSettings {
|
||||
tab_size: settings.tab_size,
|
||||
style: theme,
|
||||
}
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn project_path(&self) -> Option<ProjectPath> {
|
||||
self.file().map(|f| ProjectPath {
|
||||
worktree_id: f.worktree_id(),
|
||||
path: f.path().clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemView for Editor {
|
||||
fn should_activate_item_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::Activate)
|
||||
}
|
||||
|
||||
fn should_close_item_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::Closed)
|
||||
}
|
||||
|
||||
fn should_update_tab_on_event(event: &Event) -> bool {
|
||||
matches!(
|
||||
event,
|
||||
Event::Saved | Event::Dirtied | Event::FileHandleChanged
|
||||
)
|
||||
}
|
||||
|
||||
fn title(&self, cx: &AppContext) -> String {
|
||||
let filename = self
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| file.file_name(cx));
|
||||
if let Some(name) = filename {
|
||||
name.to_string_lossy().into()
|
||||
} else {
|
||||
"untitled".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
self.buffer().read(cx).file().map(|file| ProjectPath {
|
||||
worktree_id: file.worktree_id(),
|
||||
path: file.path().clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(self.clone(cx))
|
||||
}
|
||||
|
||||
fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
|
||||
let save = self.buffer().update(cx, |b, cx| b.save(cx))?;
|
||||
Ok(cx.spawn(|_, _| async move {
|
||||
save.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
path: &Path,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.buffer().update(cx, |buffer, cx| {
|
||||
let handle = cx.handle();
|
||||
let text = buffer.as_rope().clone();
|
||||
let version = buffer.version();
|
||||
|
||||
let save_as = worktree.update(cx, |worktree, cx| {
|
||||
worktree
|
||||
.as_local_mut()
|
||||
.unwrap()
|
||||
.save_buffer_as(handle, path, text, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|buffer, mut cx| async move {
|
||||
save_as.await.map(|new_file| {
|
||||
let language = worktree.read_with(&cx, |worktree, cx| {
|
||||
worktree
|
||||
.languages()
|
||||
.select_language(new_file.full_path(cx))
|
||||
.cloned()
|
||||
});
|
||||
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx);
|
||||
buffer.set_language(language, cx);
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
self.buffer().read(cx).is_dirty()
|
||||
}
|
||||
|
||||
fn has_conflict(&self, cx: &AppContext) -> bool {
|
||||
self.buffer().read(cx).has_conflict()
|
||||
}
|
||||
}
|
|
@ -1,372 +0,0 @@
|
|||
use super::{ItemViewHandle, SplitDirection};
|
||||
use crate::{project::ProjectPath, settings::Settings};
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
keymap::Binding,
|
||||
platform::CursorStyle,
|
||||
Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use std::cmp;
|
||||
|
||||
action!(Split, SplitDirection);
|
||||
action!(ActivateItem, usize);
|
||||
action!(ActivatePrevItem);
|
||||
action!(ActivateNextItem);
|
||||
action!(CloseActiveItem);
|
||||
action!(CloseItem, usize);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
||||
pane.activate_item(action.0, cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
|
||||
pane.activate_prev_item(cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
|
||||
pane.activate_next_item(cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
|
||||
pane.close_active_item(cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
|
||||
pane.close_item(action.0, cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, action: &Split, cx| {
|
||||
pane.split(action.0, cx);
|
||||
});
|
||||
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
|
||||
Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
|
||||
Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
|
||||
Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
|
||||
Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
|
||||
Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
|
||||
Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
|
||||
]);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Activate,
|
||||
Remove,
|
||||
Split(SplitDirection),
|
||||
}
|
||||
|
||||
const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct State {
|
||||
pub tabs: Vec<TabState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct TabState {
|
||||
pub title: String,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
pub struct Pane {
|
||||
items: Vec<Box<dyn ItemViewHandle>>,
|
||||
active_item: usize,
|
||||
settings: watch::Receiver<Settings>,
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
pub fn new(settings: watch::Receiver<Settings>) -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
active_item: 0,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate(&self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Activate);
|
||||
}
|
||||
|
||||
pub fn add_item(&mut self, item: Box<dyn ItemViewHandle>, cx: &mut ViewContext<Self>) -> usize {
|
||||
let item_idx = cmp::min(self.active_item + 1, self.items.len());
|
||||
self.items.insert(item_idx, item);
|
||||
cx.notify();
|
||||
item_idx
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn items(&self) -> &[Box<dyn ItemViewHandle>] {
|
||||
&self.items
|
||||
}
|
||||
|
||||
pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
|
||||
self.items.get(self.active_item).cloned()
|
||||
}
|
||||
|
||||
pub fn activate_entry(
|
||||
&mut self,
|
||||
project_path: ProjectPath,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
if let Some(index) = self.items.iter().position(|item| {
|
||||
item.project_path(cx.as_ref())
|
||||
.map_or(false, |item_path| item_path == project_path)
|
||||
}) {
|
||||
self.activate_item(index, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
|
||||
self.items.iter().position(|i| i.id() == item.id())
|
||||
}
|
||||
|
||||
pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
|
||||
if index < self.items.len() {
|
||||
self.active_item = index;
|
||||
self.focus_active_item(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.active_item > 0 {
|
||||
self.active_item -= 1;
|
||||
} else if self.items.len() > 0 {
|
||||
self.active_item = self.items.len() - 1;
|
||||
}
|
||||
self.focus_active_item(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.active_item + 1 < self.items.len() {
|
||||
self.active_item += 1;
|
||||
} else {
|
||||
self.active_item = 0;
|
||||
}
|
||||
self.focus_active_item(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.items.is_empty() {
|
||||
self.close_item(self.items[self.active_item].id(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext<Self>) {
|
||||
self.items.retain(|item| item.id() != item_id);
|
||||
self.active_item = cmp::min(self.active_item, self.items.len().saturating_sub(1));
|
||||
if self.items.is_empty() {
|
||||
cx.emit(Event::Remove);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
cx.focus(active_item.to_any());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Split(direction));
|
||||
}
|
||||
|
||||
fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
let theme = &settings.theme;
|
||||
|
||||
enum Tabs {}
|
||||
let tabs = MouseEventHandler::new::<Tabs, _, _, _>(0, cx, |mouse_state, cx| {
|
||||
let mut row = Flex::row();
|
||||
for (ix, item) in self.items.iter().enumerate() {
|
||||
let is_active = ix == self.active_item;
|
||||
|
||||
row.add_child({
|
||||
let mut title = item.title(cx);
|
||||
if title.len() > MAX_TAB_TITLE_LEN {
|
||||
let mut truncated_len = MAX_TAB_TITLE_LEN;
|
||||
while !title.is_char_boundary(truncated_len) {
|
||||
truncated_len -= 1;
|
||||
}
|
||||
title.truncate(truncated_len);
|
||||
title.push('…');
|
||||
}
|
||||
|
||||
let mut style = if is_active {
|
||||
theme.workspace.active_tab.clone()
|
||||
} else {
|
||||
theme.workspace.tab.clone()
|
||||
};
|
||||
if ix == 0 {
|
||||
style.container.border.left = false;
|
||||
}
|
||||
|
||||
EventHandler::new(
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Align::new({
|
||||
let diameter = 7.0;
|
||||
let icon_color = if item.has_conflict(cx) {
|
||||
Some(style.icon_conflict)
|
||||
} else if item.is_dirty(cx) {
|
||||
Some(style.icon_dirty)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ConstrainedBox::new(
|
||||
Canvas::new(move |bounds, _, cx| {
|
||||
if let Some(color) = icon_color {
|
||||
let square = RectF::new(
|
||||
bounds.origin(),
|
||||
vec2f(diameter, diameter),
|
||||
);
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: square,
|
||||
background: Some(color),
|
||||
border: Default::default(),
|
||||
corner_radius: diameter / 2.,
|
||||
});
|
||||
}
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_width(diameter)
|
||||
.with_height(diameter)
|
||||
.boxed()
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
Align::new(
|
||||
Label::new(
|
||||
title,
|
||||
if is_active {
|
||||
theme.workspace.active_tab.label.clone()
|
||||
} else {
|
||||
theme.workspace.tab.label.clone()
|
||||
},
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(ContainerStyle {
|
||||
margin: Margin {
|
||||
left: style.spacing,
|
||||
right: style.spacing,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Align::new(
|
||||
ConstrainedBox::new(if mouse_state.hovered {
|
||||
let item_id = item.id();
|
||||
enum TabCloseButton {}
|
||||
let icon = Svg::new("icons/x.svg");
|
||||
MouseEventHandler::new::<TabCloseButton, _, _, _>(
|
||||
item_id,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
if mouse_state.hovered {
|
||||
icon.with_color(style.icon_close_active)
|
||||
.boxed()
|
||||
} else {
|
||||
icon.with_color(style.icon_close).boxed()
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_padding(Padding::uniform(4.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |cx| {
|
||||
cx.dispatch_action(CloseItem(item_id))
|
||||
})
|
||||
.named("close-tab-icon")
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
})
|
||||
.with_width(style.icon_width)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(style.container)
|
||||
.boxed(),
|
||||
)
|
||||
.on_mouse_down(move |cx| {
|
||||
cx.dispatch_action(ActivateItem(ix));
|
||||
true
|
||||
})
|
||||
.boxed()
|
||||
})
|
||||
}
|
||||
|
||||
row.add_child(
|
||||
Expanded::new(
|
||||
0.0,
|
||||
Container::new(Empty::new().boxed())
|
||||
.with_border(theme.workspace.tab.container.border)
|
||||
.boxed(),
|
||||
)
|
||||
.named("filler"),
|
||||
);
|
||||
|
||||
row.boxed()
|
||||
});
|
||||
|
||||
ConstrainedBox::new(tabs.boxed())
|
||||
.with_height(theme.workspace.tab.height)
|
||||
.named("tabs")
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Pane {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for Pane {
|
||||
fn ui_name() -> &'static str {
|
||||
"Pane"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
Flex::column()
|
||||
.with_child(self.render_tabs(cx))
|
||||
.with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed())
|
||||
.named("pane")
|
||||
} else {
|
||||
Empty::new().named("pane")
|
||||
}
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.focus_active_item(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PaneHandle {
|
||||
fn add_item_view(&self, item: Box<dyn ItemViewHandle>, cx: &mut MutableAppContext);
|
||||
}
|
||||
|
||||
impl PaneHandle for ViewHandle<Pane> {
|
||||
fn add_item_view(&self, item: Box<dyn ItemViewHandle>, cx: &mut MutableAppContext) {
|
||||
item.set_parent_pane(self, cx);
|
||||
self.update(cx, |pane, cx| {
|
||||
let item_idx = pane.add_item(item, cx);
|
||||
pane.activate_item(item_idx, cx);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,384 +0,0 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use gpui::{elements::*, Axis};
|
||||
use theme::Theme;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PaneGroup {
|
||||
root: Member,
|
||||
}
|
||||
|
||||
impl PaneGroup {
|
||||
pub fn new(pane_id: usize) -> Self {
|
||||
Self {
|
||||
root: Member::Pane(pane_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split(
|
||||
&mut self,
|
||||
old_pane_id: usize,
|
||||
new_pane_id: usize,
|
||||
direction: SplitDirection,
|
||||
) -> Result<()> {
|
||||
match &mut self.root {
|
||||
Member::Pane(pane_id) => {
|
||||
if *pane_id == old_pane_id {
|
||||
self.root = Member::new_axis(old_pane_id, new_pane_id, direction);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Pane not found"))
|
||||
}
|
||||
}
|
||||
Member::Axis(axis) => axis.split(old_pane_id, new_pane_id, direction),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, pane_id: usize) -> Result<bool> {
|
||||
match &mut self.root {
|
||||
Member::Pane(_) => Ok(false),
|
||||
Member::Axis(axis) => {
|
||||
if let Some(last_pane) = axis.remove(pane_id)? {
|
||||
self.root = last_pane;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render<'a>(&self, theme: &Theme) -> ElementBox {
|
||||
self.root.render(theme)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum Member {
|
||||
Axis(PaneAxis),
|
||||
Pane(usize),
|
||||
}
|
||||
|
||||
impl Member {
|
||||
fn new_axis(old_pane_id: usize, new_pane_id: usize, direction: SplitDirection) -> Self {
|
||||
use Axis::*;
|
||||
use SplitDirection::*;
|
||||
|
||||
let axis = match direction {
|
||||
Up | Down => Vertical,
|
||||
Left | Right => Horizontal,
|
||||
};
|
||||
|
||||
let members = match direction {
|
||||
Up | Left => vec![Member::Pane(new_pane_id), Member::Pane(old_pane_id)],
|
||||
Down | Right => vec![Member::Pane(old_pane_id), Member::Pane(new_pane_id)],
|
||||
};
|
||||
|
||||
Member::Axis(PaneAxis { axis, members })
|
||||
}
|
||||
|
||||
pub fn render<'a>(&self, theme: &Theme) -> ElementBox {
|
||||
match self {
|
||||
Member::Pane(view_id) => ChildView::new(*view_id).boxed(),
|
||||
Member::Axis(axis) => axis.render(theme),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct PaneAxis {
|
||||
axis: Axis,
|
||||
members: Vec<Member>,
|
||||
}
|
||||
|
||||
impl PaneAxis {
|
||||
fn split(
|
||||
&mut self,
|
||||
old_pane_id: usize,
|
||||
new_pane_id: usize,
|
||||
direction: SplitDirection,
|
||||
) -> Result<()> {
|
||||
use SplitDirection::*;
|
||||
|
||||
for (idx, member) in self.members.iter_mut().enumerate() {
|
||||
match member {
|
||||
Member::Axis(axis) => {
|
||||
if axis.split(old_pane_id, new_pane_id, direction).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Member::Pane(pane_id) => {
|
||||
if *pane_id == old_pane_id {
|
||||
if direction.matches_axis(self.axis) {
|
||||
match direction {
|
||||
Up | Left => {
|
||||
self.members.insert(idx, Member::Pane(new_pane_id));
|
||||
}
|
||||
Down | Right => {
|
||||
self.members.insert(idx + 1, Member::Pane(new_pane_id));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
*member = Member::new_axis(old_pane_id, new_pane_id, direction);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(anyhow!("Pane not found"))
|
||||
}
|
||||
|
||||
fn remove(&mut self, pane_id_to_remove: usize) -> Result<Option<Member>> {
|
||||
let mut found_pane = false;
|
||||
let mut remove_member = None;
|
||||
for (idx, member) in self.members.iter_mut().enumerate() {
|
||||
match member {
|
||||
Member::Axis(axis) => {
|
||||
if let Ok(last_pane) = axis.remove(pane_id_to_remove) {
|
||||
if let Some(last_pane) = last_pane {
|
||||
*member = last_pane;
|
||||
}
|
||||
found_pane = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Member::Pane(pane_id) => {
|
||||
if *pane_id == pane_id_to_remove {
|
||||
found_pane = true;
|
||||
remove_member = Some(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found_pane {
|
||||
if let Some(idx) = remove_member {
|
||||
self.members.remove(idx);
|
||||
}
|
||||
|
||||
if self.members.len() == 1 {
|
||||
Ok(self.members.pop())
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!("Pane not found"))
|
||||
}
|
||||
}
|
||||
|
||||
fn render<'a>(&self, theme: &Theme) -> ElementBox {
|
||||
let last_member_ix = self.members.len() - 1;
|
||||
Flex::new(self.axis)
|
||||
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
|
||||
let mut member = member.render(theme);
|
||||
if ix < last_member_ix {
|
||||
let mut border = theme.workspace.pane_divider;
|
||||
border.left = false;
|
||||
border.right = false;
|
||||
border.top = false;
|
||||
border.bottom = false;
|
||||
match self.axis {
|
||||
Axis::Vertical => border.bottom = true,
|
||||
Axis::Horizontal => border.right = true,
|
||||
}
|
||||
member = Container::new(member).with_border(border).boxed();
|
||||
}
|
||||
|
||||
Expanded::new(1.0, member).boxed()
|
||||
}))
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum SplitDirection {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl SplitDirection {
|
||||
fn matches_axis(self, orientation: Axis) -> bool {
|
||||
use Axis::*;
|
||||
use SplitDirection::*;
|
||||
|
||||
match self {
|
||||
Up | Down => match orientation {
|
||||
Vertical => true,
|
||||
Horizontal => false,
|
||||
},
|
||||
Left | Right => match orientation {
|
||||
Vertical => false,
|
||||
Horizontal => true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// use super::*;
|
||||
// use serde_json::json;
|
||||
|
||||
// #[test]
|
||||
// fn test_split_and_remove() -> Result<()> {
|
||||
// let mut group = PaneGroup::new(1);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "pane",
|
||||
// "paneId": 1,
|
||||
// })
|
||||
// );
|
||||
|
||||
// group.split(1, 2, SplitDirection::Right)?;
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// group.split(2, 3, SplitDirection::Up)?;
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {
|
||||
// "type": "axis",
|
||||
// "orientation": "vertical",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 3},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// group.split(1, 4, SplitDirection::Right)?;
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {"type": "pane", "paneId": 4},
|
||||
// {
|
||||
// "type": "axis",
|
||||
// "orientation": "vertical",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 3},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// group.split(2, 5, SplitDirection::Up)?;
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {"type": "pane", "paneId": 4},
|
||||
// {
|
||||
// "type": "axis",
|
||||
// "orientation": "vertical",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 3},
|
||||
// {"type": "pane", "paneId": 5},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// assert_eq!(true, group.remove(5)?);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {"type": "pane", "paneId": 4},
|
||||
// {
|
||||
// "type": "axis",
|
||||
// "orientation": "vertical",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 3},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// assert_eq!(true, group.remove(4)?);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {
|
||||
// "type": "axis",
|
||||
// "orientation": "vertical",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 3},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// assert_eq!(true, group.remove(3)?);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// assert_eq!(true, group.remove(2)?);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "pane",
|
||||
// "paneId": 1,
|
||||
// })
|
||||
// );
|
||||
|
||||
// assert_eq!(false, group.remove(1)?);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "pane",
|
||||
// "paneId": 1,
|
||||
// })
|
||||
// );
|
||||
|
||||
// Ok(())
|
||||
// }
|
||||
}
|
|
@ -1,200 +0,0 @@
|
|||
use super::Workspace;
|
||||
use crate::Settings;
|
||||
use gpui::{
|
||||
action, elements::*, platform::CursorStyle, AnyViewHandle, MutableAppContext, RenderContext,
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
pub struct Sidebar {
|
||||
side: Side,
|
||||
items: Vec<Item>,
|
||||
active_item_ix: Option<usize>,
|
||||
width: Rc<RefCell<f32>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
struct Item {
|
||||
icon_path: &'static str,
|
||||
view: AnyViewHandle,
|
||||
}
|
||||
|
||||
action!(ToggleSidebarItem, SidebarItemId);
|
||||
action!(ToggleSidebarItemFocus, SidebarItemId);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SidebarItemId {
|
||||
pub side: Side,
|
||||
pub item_index: usize,
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
pub fn new(side: Side) -> Self {
|
||||
Self {
|
||||
side,
|
||||
items: Default::default(),
|
||||
active_item_ix: None,
|
||||
width: Rc::new(RefCell::new(260.)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_item(&mut self, icon_path: &'static str, view: AnyViewHandle) {
|
||||
self.items.push(Item { icon_path, view });
|
||||
}
|
||||
|
||||
pub fn activate_item(&mut self, item_ix: usize) {
|
||||
self.active_item_ix = Some(item_ix);
|
||||
}
|
||||
|
||||
pub fn toggle_item(&mut self, item_ix: usize) {
|
||||
if self.active_item_ix == Some(item_ix) {
|
||||
self.active_item_ix = None;
|
||||
} else {
|
||||
self.active_item_ix = Some(item_ix);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_item(&self) -> Option<&AnyViewHandle> {
|
||||
self.active_item_ix
|
||||
.and_then(|ix| self.items.get(ix))
|
||||
.map(|item| &item.view)
|
||||
}
|
||||
|
||||
fn theme<'a>(&self, settings: &'a Settings) -> &'a theme::Sidebar {
|
||||
match self.side {
|
||||
Side::Left => &settings.theme.workspace.left_sidebar,
|
||||
Side::Right => &settings.theme.workspace.right_sidebar,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, settings: &Settings, cx: &mut RenderContext<Workspace>) -> ElementBox {
|
||||
let side = self.side;
|
||||
let theme = self.theme(settings);
|
||||
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_children(self.items.iter().enumerate().map(|(item_index, item)| {
|
||||
let theme = if Some(item_index) == self.active_item_ix {
|
||||
&theme.active_item
|
||||
} else {
|
||||
&theme.item
|
||||
};
|
||||
enum SidebarButton {}
|
||||
MouseEventHandler::new::<SidebarButton, _, _, _>(
|
||||
item.view.id(),
|
||||
cx,
|
||||
|_, _| {
|
||||
ConstrainedBox::new(
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Svg::new(item.icon_path)
|
||||
.with_color(theme.icon_color)
|
||||
.boxed(),
|
||||
)
|
||||
.with_height(theme.icon_size)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_height(theme.height)
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_mouse_down(move |cx| {
|
||||
cx.dispatch_action(ToggleSidebarItem(SidebarItemId {
|
||||
side,
|
||||
item_index,
|
||||
}))
|
||||
})
|
||||
.boxed()
|
||||
}))
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_width(theme.width)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn render_active_item(
|
||||
&self,
|
||||
settings: &Settings,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<ElementBox> {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
let mut container = Flex::row();
|
||||
if matches!(self.side, Side::Right) {
|
||||
container.add_child(self.render_resize_handle(settings, cx));
|
||||
}
|
||||
|
||||
container.add_child(
|
||||
Flexible::new(
|
||||
1.,
|
||||
Hook::new(
|
||||
ConstrainedBox::new(ChildView::new(active_item.id()).boxed())
|
||||
.with_max_width(*self.width.borrow())
|
||||
.boxed(),
|
||||
)
|
||||
.on_after_layout({
|
||||
let width = self.width.clone();
|
||||
move |size, _| *width.borrow_mut() = size.x()
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
);
|
||||
if matches!(self.side, Side::Left) {
|
||||
container.add_child(self.render_resize_handle(settings, cx));
|
||||
}
|
||||
Some(container.boxed())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_resize_handle(
|
||||
&self,
|
||||
settings: &Settings,
|
||||
mut cx: &mut MutableAppContext,
|
||||
) -> ElementBox {
|
||||
let width = self.width.clone();
|
||||
let side = self.side;
|
||||
MouseEventHandler::new::<Self, _, _, _>(self.side.id(), &mut cx, |_, _| {
|
||||
Container::new(Empty::new().boxed())
|
||||
.with_style(self.theme(settings).resize_handle)
|
||||
.boxed()
|
||||
})
|
||||
.with_padding(Padding {
|
||||
left: 4.,
|
||||
right: 4.,
|
||||
..Default::default()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::ResizeLeftRight)
|
||||
.on_drag(move |delta, cx| {
|
||||
let prev_width = *width.borrow();
|
||||
match side {
|
||||
Side::Left => *width.borrow_mut() = 0f32.max(prev_width + delta.x()),
|
||||
Side::Right => *width.borrow_mut() = 0f32.max(prev_width - delta.x()),
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Side {
|
||||
fn id(self) -> usize {
|
||||
match self {
|
||||
Side::Left => 0,
|
||||
Side::Right => 1,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue