diff --git a/assets/settings/default.json b/assets/settings/default.json index 5d85e751d4..d1a6499655 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -108,7 +108,8 @@ "auto_update": true, // Git gutter behavior configuration. "project_panel": { - "dock": "left" + "dock": "left", + "default_width": 240 }, "git": { // Control whether the git gutter is shown. May take 2 values: diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index e2c4af143c..3caa91a3b8 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -187,25 +187,21 @@ pub trait Element: 'static { Tooltip::new::(id, text, action, style, self.into_any(), cx) } - fn with_resize_handle( + fn resizable( self, - element_id: usize, - side: Side, - handle_size: f32, - initial_size: f32, - cx: &mut ViewContext, + side: HandleSide, + size: f32, + on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext), ) -> Resizable where Self: 'static + Sized, { - Resizable::new::( + Resizable::new( self.into_any(), - element_id, side, - handle_size, - initial_size, - cx, - ) + size, + on_resize + ) } } diff --git a/crates/gpui/src/elements/resizable.rs b/crates/gpui/src/elements/resizable.rs index 0e78cc07fb..a3d95893f4 100644 --- a/crates/gpui/src/elements/resizable.rs +++ b/crates/gpui/src/elements/resizable.rs @@ -1,4 +1,4 @@ -use std::{cell::Cell, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; @@ -7,25 +7,23 @@ use crate::{ geometry::rect::RectF, platform::{CursorStyle, MouseButton}, scene::MouseDrag, - AnyElement, Axis, Element, ElementStateHandle, LayoutContext, MouseRegion, SceneBuilder, View, - ViewContext, + AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, View, + ViewContext, SizeConstraint, }; -use super::{ConstrainedBox, Hook}; - #[derive(Copy, Clone, Debug)] -pub enum Side { +pub enum HandleSide { Top, Bottom, Left, Right, } -impl Side { +impl HandleSide { fn axis(&self) -> Axis { match self { - Side::Left | Side::Right => Axis::Horizontal, - Side::Top | Side::Bottom => Axis::Vertical, + HandleSide::Left | HandleSide::Right => Axis::Horizontal, + HandleSide::Top | HandleSide::Bottom => Axis::Vertical, } } @@ -33,8 +31,8 @@ impl Side { /// then top-to-bottom fn before_content(self) -> bool { match self { - Side::Left | Side::Top => true, - Side::Right | Side::Bottom => false, + HandleSide::Left | HandleSide::Top => true, + HandleSide::Right | HandleSide::Bottom => false, } } @@ -55,14 +53,14 @@ impl Side { fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF { match self { - Side::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)), - Side::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())), - Side::Bottom => { + HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)), + HandleSide::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())), + HandleSide::Bottom => { let mut origin = bounds.lower_left(); origin.set_y(origin.y() - handle_size); RectF::new(origin, vec2f(bounds.width(), handle_size)) } - Side::Right => { + HandleSide::Right => { let mut origin = bounds.upper_right(); origin.set_x(origin.x() - handle_size); RectF::new(origin, vec2f(handle_size, bounds.height())) @@ -71,69 +69,44 @@ impl Side { } } -struct ResizeHandleState { - actual_dimension: Cell, - custom_dimension: Cell, +pub struct Resizable { + child: AnyElement, + handle_side: HandleSide, + handle_size: f32, + on_resize: Rc)>> } -pub struct Resizable { - side: Side, - handle_size: f32, - child: AnyElement, - state: Rc, - _state_handle: ElementStateHandle>, -} +const DEFAULT_HANDLE_SIZE: f32 = 4.0; impl Resizable { - pub fn new( + pub fn new( child: AnyElement, - element_id: usize, - side: Side, - handle_size: f32, - initial_size: f32, - cx: &mut ViewContext, + handle_side: HandleSide, + size: f32, + on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext) ) -> Self { - let state_handle = cx.element_state::>( - element_id, - Rc::new(ResizeHandleState { - actual_dimension: Cell::new(initial_size), - custom_dimension: Cell::new(initial_size), - }), - ); - - let state = state_handle.read(cx).clone(); - - let child = Hook::new({ - let constrained = ConstrainedBox::new(child); - match side.axis() { - Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()), - Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()), - } - }) - .on_after_layout({ - let state = state.clone(); - move |size, _| { - state.actual_dimension.set(side.relevant_component(size)); - } - }) + let child = match handle_side.axis() { + Axis::Horizontal => child.constrained().with_max_width(size), + Axis::Vertical => child.constrained().with_max_height(size), + } .into_any(); Self { - side, child, - handle_size, - state, - _state_handle: state_handle, + handle_side, + handle_size: DEFAULT_HANDLE_SIZE, + on_resize: Rc::new(RefCell::new(on_resize)), } } - pub fn current_size(&self) -> f32 { - self.state.actual_dimension.get() + pub fn with_handle_size(mut self, handle_size: f32) -> Self { + self.handle_size = handle_size; + self } } impl Element for Resizable { - type LayoutState = (); + type LayoutState = SizeConstraint; type PaintState = (); fn layout( @@ -142,7 +115,7 @@ impl Element for Resizable { view: &mut V, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - (self.child.layout(constraint, view, cx), ()) + (self.child.layout(constraint, view, cx), constraint) } fn paint( @@ -150,34 +123,37 @@ impl Element for Resizable { scene: &mut SceneBuilder, bounds: pathfinder_geometry::rect::RectF, visible_bounds: pathfinder_geometry::rect::RectF, - _child_size: &mut Self::LayoutState, + constraint: &mut SizeConstraint, view: &mut V, cx: &mut ViewContext, ) -> Self::PaintState { scene.push_stacking_context(None, None); - let handle_region = self.side.of_rect(bounds, self.handle_size); + let handle_region = self.handle_side.of_rect(bounds, self.handle_size); enum ResizeHandle {} scene.push_mouse_region( - MouseRegion::new::(cx.view_id(), self.side as usize, handle_region) + MouseRegion::new::(cx.view_id(), self.handle_side as usize, handle_region) .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere .on_drag(MouseButton::Left, { - let state = self.state.clone(); - let side = self.side; - move |e, _: &mut V, cx| { - let prev_width = state.actual_dimension.get(); - state - .custom_dimension - .set(0f32.max(prev_width + side.compute_delta(e)).round()); - cx.notify(); + let bounds = bounds.clone(); + let side = self.handle_side; + let prev_size = side.relevant_component(bounds.size()); + let min_size = side.relevant_component(constraint.min); + let max_size = side.relevant_component(constraint.max); + let on_resize = self.on_resize.clone(); + move |event, view: &mut V, cx| { + let new_size = min_size.max(prev_size + side.compute_delta(event)).min(max_size).round(); + if new_size != prev_size { + on_resize.borrow_mut()(view, new_size, cx); + } } }), ); scene.push_cursor_region(crate::CursorRegion { bounds: handle_region, - style: match self.side.axis() { + style: match self.handle_side.axis() { Axis::Horizontal => CursorStyle::ResizeLeftRight, Axis::Vertical => CursorStyle::ResizeUpDown, }, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 14779f9b6f..ac0df343ae 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1347,12 +1347,9 @@ impl Entity for ProjectPanel { impl workspace::dock::Panel for ProjectPanel { fn position(&self, cx: &gpui::WindowContext) -> DockPosition { let settings = cx.global::(); - let dock = settings - .project_panel_overrides - .dock - .or(settings.project_panel_defaults.dock) - .unwrap(); - match dock { + match settings + .project_panel + .dock { settings::ProjectPanelDockPosition::Left => DockPosition::Left, settings::ProjectPanelDockPosition::Right => DockPosition::Right, } @@ -1374,6 +1371,10 @@ impl workspace::dock::Panel for ProjectPanel { }) } + fn default_size(&self, cx: &gpui::WindowContext) -> f32 { + cx.global::().project_panel.default_width + } + fn icon_path(&self) -> &'static str { "icons/folder_tree_16.svg" } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 67acd12939..df03d4c1f6 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -44,8 +44,7 @@ pub struct Settings { pub show_call_status_icon: bool, pub vim_mode: bool, pub autosave: Autosave, - pub project_panel_defaults: ProjectPanelSettings, - pub project_panel_overrides: ProjectPanelSettings, + pub project_panel: ProjectPanelSettings, pub editor_defaults: EditorSettings, pub editor_overrides: EditorSettings, pub git: GitSettings, @@ -158,9 +157,15 @@ pub enum GitGutter { pub struct GitGutterConfig {} -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct ProjectPanelSettings { + pub dock: ProjectPanelDockPosition, + pub default_width: f32, +} +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct ProjectPanelSettingsContent { pub dock: Option, + pub default_width: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] @@ -253,6 +258,8 @@ impl Default for HourFormat { #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct TerminalSettings { + pub default_width: Option, + pub default_height: Option, pub shell: Option, pub working_directory: Option, pub font_size: Option, @@ -387,7 +394,7 @@ pub struct SettingsFileContent { #[serde(flatten)] pub editor: EditorSettings, #[serde(default)] - pub project_panel: ProjectPanelSettings, + pub project_panel: ProjectPanelSettingsContent, #[serde(default)] pub journal: JournalSettings, #[serde(default)] @@ -423,7 +430,6 @@ pub struct Features { } #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] pub struct FeaturesContent { pub copilot: Option, } @@ -482,8 +488,10 @@ impl Settings { show_call_status_icon: defaults.show_call_status_icon.unwrap(), vim_mode: defaults.vim_mode.unwrap(), autosave: defaults.autosave.unwrap(), - project_panel_defaults: defaults.project_panel, - project_panel_overrides: Default::default(), + project_panel: ProjectPanelSettings { + dock: defaults.project_panel.dock.unwrap(), + default_width: defaults.project_panel.default_width.unwrap(), + }, editor_defaults: EditorSettings { tab_size: required(defaults.editor.tab_size), hard_tabs: required(defaults.editor.hard_tabs), @@ -590,7 +598,8 @@ impl Settings { } } self.editor_overrides = data.editor; - self.project_panel_overrides = data.project_panel; + merge(&mut self.project_panel.dock, data.project_panel.dock); + merge(&mut self.project_panel.default_width, data.project_panel.default_width); self.git_overrides = data.git.unwrap_or_default(); self.journal_overrides = data.journal; self.terminal_defaults.font_size = data.terminal.font_size; @@ -778,8 +787,10 @@ impl Settings { show_call_status_icon: true, vim_mode: false, autosave: Autosave::Off, - project_panel_defaults: Default::default(), - project_panel_overrides: Default::default(), + project_panel: ProjectPanelSettings { + dock: ProjectPanelDockPosition::Left, + default_width: 240., + }, editor_defaults: EditorSettings { tab_size: Some(4.try_into().unwrap()), hard_tabs: Some(false), diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 5d4099c446..995c8be50b 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -179,6 +179,14 @@ impl Panel for TerminalPanel { }); } + fn default_size(&self, cx: &gpui::WindowContext) -> f32 { + let settings = &cx.global::().terminal_overrides; + match self.position(cx) { + DockPosition::Left | DockPosition::Right => settings.default_width.unwrap_or(640.), + DockPosition::Bottom => settings.default_height.unwrap_or(320.), + } + } + fn icon_path(&self) -> &'static str { "icons/terminal_12.svg" } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 5320a4c652..6ce1e4f691 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -12,6 +12,7 @@ pub trait Panel: View { fn position(&self, cx: &WindowContext) -> DockPosition; fn position_is_valid(&self, position: DockPosition) -> bool; fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext); + fn default_size(&self, cx: &WindowContext) -> f32; fn icon_path(&self) -> &'static str; fn icon_tooltip(&self) -> String; fn icon_label(&self, _: &AppContext) -> Option { @@ -27,6 +28,7 @@ pub trait PanelHandle { fn position(&self, cx: &WindowContext) -> DockPosition; fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool; fn set_position(&self, position: DockPosition, cx: &mut WindowContext); + fn default_size(&self, cx: &WindowContext) -> f32; fn icon_path(&self, cx: &WindowContext) -> &'static str; fn icon_tooltip(&self, cx: &WindowContext) -> String; fn icon_label(&self, cx: &WindowContext) -> Option; @@ -54,6 +56,10 @@ where self.update(cx, |this, cx| this.set_position(position, cx)) } + fn default_size(&self, cx: &WindowContext) -> f32 { + self.read(cx).default_size(cx) + } + fn icon_path(&self, cx: &WindowContext) -> &'static str { self.read(cx).icon_path() } @@ -104,17 +110,18 @@ impl DockPosition { } } - fn to_resizable_side(self) -> Side { + fn to_resize_handle_side(self) -> HandleSide { match self { - Self::Left => Side::Right, - Self::Bottom => Side::Top, - Self::Right => Side::Left, + Self::Left => HandleSide::Right, + Self::Bottom => HandleSide::Top, + Self::Right => HandleSide::Left, } } } struct PanelEntry { panel: Rc, + size: f32, context_menu: ViewHandle, _subscriptions: [Subscription; 2], } @@ -181,8 +188,10 @@ impl Dock { ]; let dock_view_id = cx.view_id(); + let size = panel.default_size(cx); self.panel_entries.push(PanelEntry { panel: Rc::new(panel), + size, context_menu: cx.add_view(|cx| { let mut menu = ContextMenu::new(dock_view_id, cx); menu.set_position_mode(OverlayPositionMode::Local); @@ -237,6 +246,23 @@ impl Dock { None } } + + pub fn active_panel_size(&self) -> Option { + if self.is_open { + self.panel_entries + .get(self.active_panel_index) + .map(|entry| entry.size) + } else { + None + } + } + + pub fn resize_active_panel(&mut self, size: f32, cx: &mut ViewContext) { + if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) { + entry.size = size; + cx.notify(); + } + } } impl Entity for Dock { @@ -250,18 +276,14 @@ impl View for Dock { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { if let Some(active_panel) = self.active_panel() { - enum ResizeHandleTag {} + let size = self.active_panel_size().unwrap(); let style = &cx.global::().theme.workspace.dock; ChildView::new(active_panel.as_any(), cx) .contained() .with_style(style.container) - .with_resize_handle::( - self.position as usize, - self.position.to_resizable_side(), - 4., - style.initial_size, - cx, - ) + .resizable(self.position.to_resize_handle_side(), size, |dock: &mut Self, size, cx| { + dock.resize_active_panel(size, cx); + }) .into_any() } else { Empty::new().into_any() @@ -464,6 +486,10 @@ pub(crate) mod test { cx.emit(TestPanelEvent::PositionChanged); } + fn default_size(&self, _: &WindowContext) -> f32 { + 300. + } + fn icon_path(&self) -> &'static str { "icons/test_panel.svg" } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index dfad53c3a3..2adbff51fe 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -766,7 +766,7 @@ impl FollowableItemHandle for ViewHandle { #[cfg(test)] pub(crate) mod test { use super::{Item, ItemEvent}; - use crate::{dock::Panel, ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; + use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; use gpui::{ elements::Empty, AnyElement, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -1059,42 +1059,4 @@ pub(crate) mod test { Task::Ready(Some(anyhow::Ok(view))) } } - - impl Panel for TestItem { - fn position(&self, _cx: &gpui::WindowContext) -> crate::dock::DockPosition { - unimplemented!() - } - - fn position_is_valid(&self, _position: crate::dock::DockPosition) -> bool { - unimplemented!() - } - - fn set_position( - &mut self, - _position: crate::dock::DockPosition, - _cx: &mut ViewContext, - ) { - unimplemented!() - } - - fn icon_path(&self) -> &'static str { - unimplemented!() - } - - fn icon_tooltip(&self) -> String { - unimplemented!() - } - - fn should_change_position_on_event(_: &Self::Event) -> bool { - unimplemented!() - } - - fn should_activate_on_event(&self, _: &Self::Event, _: &AppContext) -> bool { - unimplemented!() - } - - fn should_close_on_event(&self, _: &Self::Event, _: &AppContext) -> bool { - unimplemented!() - } - } }