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
153
crates/workspace/src/items.rs
Normal file
153
crates/workspace/src/items.rs
Normal file
|
@ -0,0 +1,153 @@
|
|||
use super::{Item, ItemView};
|
||||
use crate::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::{ProjectPath, 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()
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
373
crates/workspace/src/pane.rs
Normal file
373
crates/workspace/src/pane.rs
Normal file
|
@ -0,0 +1,373 @@
|
|||
use super::{ItemViewHandle, SplitDirection};
|
||||
use crate::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 project::ProjectPath;
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
384
crates/workspace/src/pane_group.rs
Normal file
384
crates/workspace/src/pane_group.rs
Normal file
|
@ -0,0 +1,384 @@
|
|||
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(())
|
||||
// }
|
||||
}
|
51
crates/workspace/src/settings.rs
Normal file
51
crates/workspace/src/settings.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
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,
|
||||
)?))
|
||||
}
|
200
crates/workspace/src/sidebar.rs
Normal file
200
crates/workspace/src/sidebar.rs
Normal file
|
@ -0,0 +1,200 @@
|
|||
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