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

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

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

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

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

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