Move workspace module into its own crate

This commit is contained in:
Antonio Scandurra 2021-10-05 13:45:19 +02:00
parent 2087c4731f
commit 499616d769
24 changed files with 1696 additions and 1584 deletions

View file

@ -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(&params, 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(&params, 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(&params, 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(&params, 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,
)

View file

@ -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| {

View file

@ -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 }));
}

View file

@ -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 {

View file

@ -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);

View file

@ -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),

View file

@ -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,
)?))
}

View file

@ -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

View file

@ -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()
}
}

View file

@ -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);
});
}
}

View file

@ -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(())
// }
}

View file

@ -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,
}
}
}