Detailed changes
@@ -310,7 +310,7 @@
"cmd-shift-m": "diagnostics::Deploy",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
- "shift-escape": "terminal::DeployModal"
+ "shift-escape": "workspace::ActivateOrHideDock"
}
},
// Bindings from Sublime Text
@@ -426,12 +426,5 @@
"cmd-v": "terminal::Paste",
"cmd-k": "terminal::Clear"
}
- },
- {
- "context": "ModalTerminal",
- "bindings": {
- "ctrl-cmd-space": "terminal::ShowCharacterPalette",
- "shift-escape": "terminal::DeployModal"
- }
}
]
@@ -32,6 +32,16 @@
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
+ // Where to place the dock by default. This setting can take three
+ // values:
+ //
+ // 1. Position the dock attached to the bottom of the workspace
+ // "default_dock_anchor": "bottom"
+ // 2. Position the dock to the right of the workspace like a side panel
+ // "default_dock_anchor": "right"
+ // 3. Position the dock full screen over the entire workspace"
+ // "default_dock_anchor": "expanded"
+ "default_dock_anchor": "right",
// How to auto-format modified buffers when saving them. This
// setting can take three values:
//
@@ -122,6 +132,14 @@
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
+ // Set whether the option key behaves as the meta key.
+ // May take 2 values:
+ // 1. Rely on default platform handling of option key, on macOS
+ // this means generating certain unicode characters
+ // "option_to_meta": false,
+ // 2. Make the option keys behave as a 'meta' key, e.g. for emacs
+ // "option_to_meta": true,
+ "option_as_meta": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
@@ -278,7 +278,7 @@ impl View for ActivityIndicator {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let (icon, message, action) = self.content_to_render(cx);
- let mut element = MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
+ let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
let theme = &cx
.global::<Settings>()
.theme
@@ -29,7 +29,7 @@ impl View for UpdateNotification {
let theme = cx.global::<Settings>().theme.clone();
let theme = &theme.update_notification;
- MouseEventHandler::new::<ViewReleaseNotes, _, _>(0, cx, |state, cx| {
+ MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
Flex::column()
.with_child(
Flex::row()
@@ -47,7 +47,7 @@ impl View for UpdateNotification {
.boxed(),
)
.with_child(
- MouseEventHandler::new::<Cancel, _, _>(0, cx, |state, _| {
+ MouseEventHandler::<Cancel>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_thin_8.svg")
.with_color(style.color)
@@ -308,7 +308,7 @@ impl ChatPanel {
enum SignInPromptLabel {}
Align::new(
- MouseEventHandler::new::<SignInPromptLabel, _, _>(0, cx, |mouse_state, _| {
+ MouseEventHandler::<SignInPromptLabel>::new(0, cx, |mouse_state, _| {
Label::new(
"Sign in to use chat".to_string(),
if mouse_state.hovered {
@@ -298,7 +298,8 @@ async fn test_host_disconnect(
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
- let (_, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+ let (_, workspace_b) =
+ cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "b.txt"), true, cx)
@@ -2786,7 +2787,8 @@ async fn test_collaborating_with_code_actions(
// Join the project as client B.
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
- let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+ let (_window_b, workspace_b) =
+ cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), true, cx)
@@ -3001,7 +3003,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
- let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+ let (_window_b, workspace_b) =
+ cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "one.rs"), true, cx)
@@ -5224,6 +5227,7 @@ impl TestServer {
fs: fs.clone(),
build_window_options: Default::default,
initialize_workspace: |_, _, _| unimplemented!(),
+ default_item_factory: |_, _| unimplemented!(),
});
Channel::init(&client);
@@ -5459,7 +5463,9 @@ impl TestClient {
cx: &mut TestAppContext,
) -> ViewHandle<Workspace> {
let (_, root_view) = cx.add_window(|_| EmptyView);
- cx.add_view(&root_view, |cx| Workspace::new(project.clone(), cx))
+ cx.add_view(&root_view, |cx| {
+ Workspace::new(project.clone(), |_, _| unimplemented!(), cx)
+ })
}
async fn simulate_host(
@@ -350,7 +350,8 @@ mod tests {
});
let project = Project::test(app_state.fs.clone(), [], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let editor = cx.add_view(&workspace, |cx| {
let mut editor = Editor::single_line(None, cx);
editor.set_text("abc", cx);
@@ -276,7 +276,7 @@ impl ContactsPanel {
Section::Offline => "Offline",
};
let icon_size = theme.section_icon_size;
- MouseEventHandler::new::<Header, _, _>(section as usize, cx, |_, _| {
+ MouseEventHandler::<Header>::new(section as usize, cx, |_, _| {
Flex::row()
.with_child(
Svg::new(if is_collapsed {
@@ -375,7 +375,7 @@ impl ContactsPanel {
let baseline_offset =
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
- MouseEventHandler::new::<JoinProject, _, _>(project_id as usize, cx, |mouse_state, cx| {
+ MouseEventHandler::<JoinProject>::new(project_id as usize, cx, |mouse_state, cx| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
@@ -424,7 +424,7 @@ impl ContactsPanel {
return None;
}
- let button = MouseEventHandler::new::<ToggleProjectOnline, _, _>(
+ let button = MouseEventHandler::<ToggleProjectOnline>::new(
project_id as usize,
cx,
|state, _| {
@@ -529,7 +529,7 @@ impl ContactsPanel {
enum ToggleOnline {}
let project_id = project_handle.id();
- MouseEventHandler::new::<LocalProject, _, _>(project_id, cx, |state, cx| {
+ MouseEventHandler::<LocalProject>::new(project_id, cx, |state, cx| {
let row = theme.project_row.style_for(state, is_selected);
let mut worktree_root_names = String::new();
let project = if let Some(project) = project_handle.upgrade(cx.deref_mut()) {
@@ -548,7 +548,7 @@ impl ContactsPanel {
Flex::row()
.with_child({
let button =
- MouseEventHandler::new::<ToggleOnline, _, _>(project_id, cx, |state, _| {
+ MouseEventHandler::<ToggleOnline>::new(project_id, cx, |state, _| {
let mut style = *theme.private_button.style_for(state, false);
if is_going_online {
style.color = theme.disabled_button.color;
@@ -636,7 +636,7 @@ impl ContactsPanel {
if is_incoming {
row.add_children([
- MouseEventHandler::new::<Decline, _, _>(user.id as usize, cx, |mouse_state, _| {
+ MouseEventHandler::<Decline>::new(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
@@ -658,7 +658,7 @@ impl ContactsPanel {
.contained()
.with_margin_right(button_spacing)
.boxed(),
- MouseEventHandler::new::<Accept, _, _>(user.id as usize, cx, |mouse_state, _| {
+ MouseEventHandler::<Accept>::new(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
@@ -680,7 +680,7 @@ impl ContactsPanel {
]);
} else {
row.add_child(
- MouseEventHandler::new::<Cancel, _, _>(user.id as usize, cx, |mouse_state, _| {
+ MouseEventHandler::<Cancel>::new(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
@@ -1071,7 +1071,7 @@ impl View for ContactsPanel {
.boxed(),
)
.with_child(
- MouseEventHandler::new::<AddContact, _, _>(0, cx, |_, _| {
+ MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
Svg::new("icons/user_plus_16.svg")
.with_color(theme.add_contact_button.color)
.constrained()
@@ -1102,35 +1102,31 @@ impl View for ContactsPanel {
if info.count > 0 {
Some(
- MouseEventHandler::new::<InviteLink, _, _>(
- 0,
- cx,
- |state, cx| {
- let style =
- theme.invite_row.style_for(state, false).clone();
-
- let copied =
- cx.read_from_clipboard().map_or(false, |item| {
- item.text().as_str() == info.url.as_ref()
- });
-
- Label::new(
- format!(
- "{} invite link ({} left)",
- if copied { "Copied" } else { "Copy" },
- info.count
- ),
- style.label.clone(),
- )
- .aligned()
- .left()
- .constrained()
- .with_height(theme.row_height)
- .contained()
- .with_style(style.container)
- .boxed()
- },
- )
+ MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
+ let style =
+ theme.invite_row.style_for(state, false).clone();
+
+ let copied =
+ cx.read_from_clipboard().map_or(false, |item| {
+ item.text().as_str() == info.url.as_ref()
+ });
+
+ Label::new(
+ format!(
+ "{} invite link ({} left)",
+ if copied { "Copied" } else { "Copy" },
+ info.count
+ ),
+ style.label.clone(),
+ )
+ .aligned()
+ .left()
+ .constrained()
+ .with_height(theme.row_height)
+ .contained()
+ .with_style(style.container)
+ .boxed()
+ })
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new(
@@ -1247,7 +1243,8 @@ mod tests {
.0
.read_with(cx, |worktree, _| worktree.id().to_proto());
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
let panel = cx.add_view(&workspace, |cx| {
ContactsPanel::new(
user_store.clone(),
@@ -52,7 +52,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
.boxed(),
)
.with_child(
- MouseEventHandler::new::<Dismiss, _, _>(user.id as usize, cx, |state, _| {
+ MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
render_icon_button(
theme.dismiss_button.style_for(state, false),
"icons/x_mark_thin_8.svg",
@@ -90,7 +90,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
Flex::row()
.with_children(buttons.into_iter().enumerate().map(
|(ix, (message, action))| {
- MouseEventHandler::new::<Button, _, _>(ix, cx, |state, _| {
+ MouseEventHandler::<Button>::new(ix, cx, |state, _| {
let button = theme.button.style_for(state, false);
Label::new(message.to_string(), button.text.clone())
.contained()
@@ -34,7 +34,7 @@ impl View for ContactsStatusItem {
Appearance::Light | Appearance::VibrantLight => Color::black(),
Appearance::Dark | Appearance::VibrantDark => Color::white(),
};
- MouseEventHandler::new::<Self, _, _>(0, cx, |_, _| {
+ MouseEventHandler::<Self>::new(0, cx, |_, _| {
Svg::new("icons/zed_22.svg")
.with_color(color)
.aligned()
@@ -22,7 +22,6 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContextMenu::cancel);
}
-//
pub enum ContextMenuItem {
Item {
label: String,
@@ -57,7 +56,8 @@ impl ContextMenuItem {
pub struct ContextMenu {
show_count: usize,
- position: Vector2F,
+ anchor_position: Vector2F,
+ anchor_corner: AnchorCorner,
items: Vec<ContextMenuItem>,
selected_index: Option<usize>,
visible: bool,
@@ -100,9 +100,10 @@ impl View for ContextMenu {
.boxed();
Overlay::new(expanded_menu)
- .hoverable(true)
- .fit_mode(OverlayFitMode::SnapToWindow)
- .with_abs_position(self.position)
+ .with_hoverable(true)
+ .with_fit_mode(OverlayFitMode::SnapToWindow)
+ .with_anchor_position(self.anchor_position)
+ .with_anchor_corner(self.anchor_corner)
.boxed()
}
@@ -115,7 +116,8 @@ impl ContextMenu {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
show_count: 0,
- position: Default::default(),
+ anchor_position: Default::default(),
+ anchor_corner: AnchorCorner::TopLeft,
items: Default::default(),
selected_index: Default::default(),
visible: Default::default(),
@@ -226,14 +228,16 @@ impl ContextMenu {
pub fn show(
&mut self,
- position: Vector2F,
+ anchor_position: Vector2F,
+ anchor_corner: AnchorCorner,
items: impl IntoIterator<Item = ContextMenuItem>,
cx: &mut ViewContext<Self>,
) {
let mut items = items.into_iter().peekable();
if items.peek().is_some() {
self.items = items.collect();
- self.position = position;
+ self.anchor_position = anchor_position;
+ self.anchor_corner = anchor_corner;
self.visible = true;
self.show_count += 1;
if !cx.is_self_focused() {
@@ -310,13 +314,13 @@ impl ContextMenu {
enum Menu {}
enum MenuItem {}
let style = cx.global::<Settings>().theme.context_menu.clone();
- MouseEventHandler::new::<Menu, _, _>(0, cx, |_, cx| {
+ MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
Flex::column()
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, action } => {
let action = action.boxed_clone();
- MouseEventHandler::new::<MenuItem, _, _>(ix, cx, |state, _| {
+ MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
let style =
style.item.style_for(state, Some(ix) == self.selected_index);
@@ -776,7 +776,8 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
// Create some diagnostics
project.update(cx, |project, cx| {
@@ -89,7 +89,7 @@ impl View for DiagnosticIndicator {
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
let in_progress = !self.in_progress_checks.is_empty();
let mut element = Flex::row().with_child(
- MouseEventHandler::new::<Summary, _, _>(0, cx, |state, cx| {
+ MouseEventHandler::<Summary>::new(0, cx, |state, cx| {
let style = cx
.global::<Settings>()
.theme
@@ -190,7 +190,7 @@ impl View for DiagnosticIndicator {
} else if let Some(diagnostic) = &self.current_diagnostic {
let message_style = style.diagnostic_message.clone();
element.add_child(
- MouseEventHandler::new::<Message, _, _>(1, cx, |state, _| {
+ MouseEventHandler::<Message>::new(1, cx, |state, _| {
Label::new(
diagnostic.message.split('\n').next().unwrap().to_string(),
message_style.style_for(state, false).text.clone(),
@@ -2,7 +2,7 @@ use std::{any::Any, rc::Rc};
use collections::HashSet;
use gpui::{
- elements::{Container, MouseEventHandler},
+ elements::{MouseEventHandler, Overlay},
geometry::vector::Vector2F,
scene::DragRegionEvent,
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
@@ -114,30 +114,29 @@ impl<V: View> DragAndDrop<V> {
let position = position + region_offset;
+ enum DraggedElementHandler {}
Some(
- MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
- Container::new(render(payload, cx))
- .with_margin_left(position.x())
- .with_margin_top(position.y())
- .aligned()
- .top()
- .left()
- .boxed()
- })
- .with_cursor_style(CursorStyle::Arrow)
- .on_up(MouseButton::Left, |_, cx| {
- cx.defer(|cx| {
- cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
- });
- cx.propogate_event();
- })
- .on_up_out(MouseButton::Left, |_, cx| {
- cx.defer(|cx| {
- cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
- });
- })
- // Don't block hover events or invalidations
- .with_hoverable(false)
+ Overlay::new(
+ MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
+ render(payload, cx)
+ })
+ .with_cursor_style(CursorStyle::Arrow)
+ .on_up(MouseButton::Left, |_, cx| {
+ cx.defer(|cx| {
+ cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
+ });
+ cx.propogate_event();
+ })
+ .on_up_out(MouseButton::Left, |_, cx| {
+ cx.defer(|cx| {
+ cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
+ });
+ })
+ // Don't block hover events or invalidations
+ .with_hoverable(false)
+ .boxed(),
+ )
+ .with_anchor_position(position)
.boxed(),
)
},
@@ -174,7 +173,7 @@ pub trait Draggable {
Self: Sized;
}
-impl Draggable for MouseEventHandler {
+impl<Tag> Draggable for MouseEventHandler<Tag> {
fn as_draggable<V: View, P: Any>(
self,
payload: P,
@@ -682,7 +682,7 @@ impl CompletionsMenu {
let completion = &completions[mat.candidate_id];
let item_ix = start_ix + ix;
items.push(
- MouseEventHandler::new::<CompletionTag, _, _>(
+ MouseEventHandler::<CompletionTag>::new(
mat.candidate_id,
cx,
|state, _| {
@@ -830,7 +830,7 @@ impl CodeActionsMenu {
for (ix, action) in actions[range].iter().enumerate() {
let item_ix = start_ix + ix;
items.push(
- MouseEventHandler::new::<ActionTag, _, _>(item_ix, cx, |state, _| {
+ MouseEventHandler::<ActionTag>::new(item_ix, cx, |state, _| {
let item_style = if item_ix == selected_item {
style.autocomplete.selected_item
} else if state.hovered {
@@ -2735,7 +2735,7 @@ impl Editor {
if self.available_code_actions.is_some() {
enum Tag {}
Some(
- MouseEventHandler::new::<Tag, _, _>(0, cx, |_, _| {
+ MouseEventHandler::<Tag>::new(0, cx, |_, _| {
Svg::new("icons/bolt_8.svg")
.with_color(style.code_actions.indicator)
.boxed()
@@ -7100,7 +7100,7 @@ mod tests {
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
use workspace::Item;
- let (_, pane) = cx.add_window(Default::default(), Pane::new);
+ let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
cx.add_view(&pane, |cx| {
@@ -7826,7 +7826,7 @@ mod tests {
#[gpui::test]
async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx).await;
+ let mut cx = EditorTestContext::new(cx);
cx.set_state("one ยซtwo threeหยป four");
cx.update_editor(|editor, cx| {
editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
@@ -7974,7 +7974,7 @@ mod tests {
#[gpui::test]
async fn test_newline_below(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx).await;
+ let mut cx = EditorTestContext::new(cx);
cx.update(|cx| {
cx.update_global::<Settings, _, _>(|settings, _| {
settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
@@ -8050,7 +8050,7 @@ mod tests {
#[gpui::test]
async fn test_tab(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx).await;
+ let mut cx = EditorTestContext::new(cx);
cx.update(|cx| {
cx.update_global::<Settings, _, _>(|settings, _| {
settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap());
@@ -8081,7 +8081,7 @@ mod tests {
#[gpui::test]
async fn test_tab_on_blank_line_auto_indents(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx).await;
+ let mut cx = EditorTestContext::new(cx);
let language = Arc::new(
Language::new(
LanguageConfig::default(),
@@ -8139,7 +8139,7 @@ mod tests {
#[gpui::test]
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx).await;
+ let mut cx = EditorTestContext::new(cx);
cx.set_state(indoc! {"
ยซoneหยป ยซtwoหยป
@@ -8208,7 +8208,7 @@ mod tests {
#[gpui::test]
async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx).await;
+ let mut cx = EditorTestContext::new(cx);
cx.update(|cx| {
cx.update_global::<Settings, _, _>(|settings, _| {
settings.editor_overrides.hard_tabs = Some(true);
@@ -8416,7 +8416,7 @@ mod tests {
#[gpui::test]
async fn test_backspace(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx).await;
+ let mut cx = EditorTestContext::new(cx);
// Basic backspace
cx.set_state(indoc! {"
@@ -8463,7 +8463,7 @@ mod tests {
#[gpui::test]
async fn test_delete(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx).await;
+ let mut cx = EditorTestContext::new(cx);
cx.set_state(indoc! {"
onหe two three
@@ -8800,7 +8800,7 @@ mod tests {
#[gpui::test]
async fn test_clipboard(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx).await;
+ let mut cx = EditorTestContext::new(cx);
cx.set_state("ยซoneโ
หยปtwo ยซthree หยปfour ยซfive หยปsix ");
cx.update_editor(|e, cx| e.cut(&Cut, cx));
@@ -8876,7 +8876,7 @@ mod tests {
#[gpui::test]
async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx).await;
+ let mut cx = EditorTestContext::new(cx);
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::language()),
@@ -9305,7 +9305,7 @@ mod tests {
#[gpui::test]
async fn test_select_next(cx: &mut gpui::TestAppContext) {
- let mut cx = EditorTestContext::new(cx).await;
+ let mut cx = EditorTestContext::new(cx);
cx.set_state("abc\nหabc abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
@@ -5,7 +5,9 @@ use super::{
};
use crate::{
display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
- hover_popover::HoverAt,
+ hover_popover::{
+ HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
+ },
link_go_to_definition::{
CmdShiftChanged, GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
},
@@ -28,7 +30,7 @@ use gpui::{
text_layout::{self, Line, RunStyle, TextLayoutCache},
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext,
LayoutContext, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent,
- MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext,
+ MouseRegion, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext,
WeakViewHandle,
};
use json::json;
@@ -41,12 +43,9 @@ use std::{
fmt::Write,
iter,
ops::Range,
+ sync::Arc,
};
-const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
-const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
-const HOVER_POPOVER_GAP: f32 = 10.;
-
struct SelectionLayout {
head: DisplayPoint,
range: Range<DisplayPoint>,
@@ -76,9 +75,10 @@ impl SelectionLayout {
}
}
+#[derive(Clone)]
pub struct EditorElement {
view: WeakViewHandle<Editor>,
- style: EditorStyle,
+ style: Arc<EditorStyle>,
cursor_shape: CursorShape,
}
@@ -90,7 +90,7 @@ impl EditorElement {
) -> Self {
Self {
view,
- style,
+ style: Arc::new(style),
cursor_shape,
}
}
@@ -110,8 +110,98 @@ impl EditorElement {
self.update_view(cx, |view, cx| view.snapshot(cx))
}
+ fn attach_mouse_handlers(
+ view: &WeakViewHandle<Editor>,
+ position_map: &Arc<PositionMap>,
+ visible_bounds: RectF,
+ text_bounds: RectF,
+ gutter_bounds: RectF,
+ bounds: RectF,
+ cx: &mut PaintContext,
+ ) {
+ enum EditorElementMouseHandlers {}
+ cx.scene.push_mouse_region(
+ MouseRegion::new::<EditorElementMouseHandlers>(view.id(), view.id(), visible_bounds)
+ .on_down(MouseButton::Left, {
+ let position_map = position_map.clone();
+ move |e, cx| {
+ if !Self::mouse_down(
+ e.platform_event,
+ position_map.as_ref(),
+ text_bounds,
+ gutter_bounds,
+ cx,
+ ) {
+ cx.propogate_event();
+ }
+ }
+ })
+ .on_down(MouseButton::Right, {
+ let position_map = position_map.clone();
+ move |e, cx| {
+ if !Self::mouse_right_down(
+ e.position,
+ position_map.as_ref(),
+ text_bounds,
+ cx,
+ ) {
+ cx.propogate_event();
+ }
+ }
+ })
+ .on_up(MouseButton::Left, {
+ let view = view.clone();
+ let position_map = position_map.clone();
+ move |e, cx| {
+ if !Self::mouse_up(
+ view.clone(),
+ e.position,
+ e.cmd,
+ e.shift,
+ position_map.as_ref(),
+ text_bounds,
+ cx,
+ ) {
+ cx.propogate_event()
+ }
+ }
+ })
+ .on_drag(MouseButton::Left, {
+ let view = view.clone();
+ let position_map = position_map.clone();
+ move |e, cx| {
+ if !Self::mouse_dragged(
+ view.clone(),
+ e.platform_event,
+ position_map.as_ref(),
+ text_bounds,
+ cx,
+ ) {
+ cx.propogate_event()
+ }
+ }
+ })
+ .on_move({
+ let position_map = position_map.clone();
+ move |e, cx| {
+ if !Self::mouse_moved(e.platform_event, &position_map, text_bounds, cx) {
+ cx.propogate_event()
+ }
+ }
+ })
+ .on_scroll({
+ let position_map = position_map.clone();
+ move |e, cx| {
+ if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
+ {
+ cx.propogate_event()
+ }
+ }
+ }),
+ );
+ }
+
fn mouse_down(
- &self,
MouseButtonEvent {
position,
ctrl,
@@ -121,18 +211,18 @@ impl EditorElement {
mut click_count,
..
}: MouseButtonEvent,
- layout: &mut LayoutState,
- paint: &mut PaintState,
+ position_map: &PositionMap,
+ text_bounds: RectF,
+ gutter_bounds: RectF,
cx: &mut EventContext,
) -> bool {
- if paint.gutter_bounds.contains_point(position) {
+ if gutter_bounds.contains_point(position) {
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
- } else if !paint.text_bounds.contains_point(position) {
+ } else if !text_bounds.contains_point(position) {
return false;
}
- let snapshot = self.snapshot(cx.app);
- let (position, target_position) = paint.point_for_position(&snapshot, layout, position);
+ let (position, target_position) = position_map.point_for_position(text_bounds, position);
if shift && alt {
cx.dispatch_action(Select(SelectPhase::BeginColumnar {
@@ -156,33 +246,31 @@ impl EditorElement {
}
fn mouse_right_down(
- &self,
position: Vector2F,
- layout: &mut LayoutState,
- paint: &mut PaintState,
+ position_map: &PositionMap,
+ text_bounds: RectF,
cx: &mut EventContext,
) -> bool {
- if !paint.text_bounds.contains_point(position) {
+ if !text_bounds.contains_point(position) {
return false;
}
- let snapshot = self.snapshot(cx.app);
- let (point, _) = paint.point_for_position(&snapshot, layout, position);
+ let (point, _) = position_map.point_for_position(text_bounds, position);
cx.dispatch_action(DeployMouseContextMenu { position, point });
true
}
fn mouse_up(
- &self,
+ view: WeakViewHandle<Editor>,
position: Vector2F,
cmd: bool,
shift: bool,
- layout: &mut LayoutState,
- paint: &mut PaintState,
+ position_map: &PositionMap,
+ text_bounds: RectF,
cx: &mut EventContext,
) -> bool {
- let view = self.view(cx.app.as_ref());
+ let view = view.upgrade(cx.app).unwrap().read(cx.app);
let end_selection = view.has_pending_selection();
let pending_nonempty_selections = view.has_pending_nonempty_selection();
@@ -190,9 +278,8 @@ impl EditorElement {
cx.dispatch_action(Select(SelectPhase::End));
}
- if !pending_nonempty_selections && cmd && paint.text_bounds.contains_point(position) {
- let (point, target_point) =
- paint.point_for_position(&self.snapshot(cx), layout, position);
+ if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
+ let (point, target_point) = position_map.point_for_position(text_bounds, position);
if point == target_point {
if shift {
@@ -209,22 +296,21 @@ impl EditorElement {
}
fn mouse_dragged(
- &self,
+ view: WeakViewHandle<Editor>,
MouseMovedEvent {
cmd,
shift,
position,
..
}: MouseMovedEvent,
- layout: &mut LayoutState,
- paint: &mut PaintState,
+ position_map: &PositionMap,
+ text_bounds: RectF,
cx: &mut EventContext,
) -> bool {
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
// Don't trigger hover popover if mouse is hovering over context menu
- let point = if paint.text_bounds.contains_point(position) {
- let (point, target_point) =
- paint.point_for_position(&self.snapshot(cx), layout, position);
+ let point = if text_bounds.contains_point(position) {
+ let (point, target_point) = position_map.point_for_position(text_bounds, position);
if point == target_point {
Some(point)
} else {
@@ -240,14 +326,13 @@ impl EditorElement {
shift_held: shift,
});
- let view = self.view(cx.app);
+ let view = view.upgrade(cx.app).unwrap().read(cx.app);
if view.has_pending_selection() {
- let rect = paint.text_bounds;
let mut scroll_delta = Vector2F::zero();
- let vertical_margin = layout.line_height.min(rect.height() / 3.0);
- let top = rect.origin_y() + vertical_margin;
- let bottom = rect.lower_left().y() - vertical_margin;
+ let vertical_margin = position_map.line_height.min(text_bounds.height() / 3.0);
+ let top = text_bounds.origin_y() + vertical_margin;
+ let bottom = text_bounds.lower_left().y() - vertical_margin;
if position.y() < top {
scroll_delta.set_y(-scale_vertical_mouse_autoscroll_delta(top - position.y()))
}
@@ -255,9 +340,9 @@ impl EditorElement {
scroll_delta.set_y(scale_vertical_mouse_autoscroll_delta(position.y() - bottom))
}
- let horizontal_margin = layout.line_height.min(rect.width() / 3.0);
- let left = rect.origin_x() + horizontal_margin;
- let right = rect.upper_right().x() - horizontal_margin;
+ let horizontal_margin = position_map.line_height.min(text_bounds.width() / 3.0);
+ let left = text_bounds.origin_x() + horizontal_margin;
+ let right = text_bounds.upper_right().x() - horizontal_margin;
if position.x() < left {
scroll_delta.set_x(-scale_horizontal_mouse_autoscroll_delta(
left - position.x(),
@@ -269,14 +354,14 @@ impl EditorElement {
))
}
- let snapshot = self.snapshot(cx.app);
- let (position, target_position) = paint.point_for_position(&snapshot, layout, position);
+ let (position, target_position) =
+ position_map.point_for_position(text_bounds, position);
cx.dispatch_action(Select(SelectPhase::Update {
position,
goal_column: target_position.column(),
- scroll_position: (snapshot.scroll_position() + scroll_delta)
- .clamp(Vector2F::zero(), layout.scroll_max),
+ scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
+ .clamp(Vector2F::zero(), position_map.scroll_max),
}));
cx.dispatch_action(HoverAt { point });
@@ -288,22 +373,20 @@ impl EditorElement {
}
fn mouse_moved(
- &self,
MouseMovedEvent {
cmd,
shift,
position,
..
}: MouseMovedEvent,
- layout: &LayoutState,
- paint: &PaintState,
+ position_map: &PositionMap,
+ text_bounds: RectF,
cx: &mut EventContext,
) -> bool {
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
// Don't trigger hover popover if mouse is hovering over context menu
- let point = if paint.text_bounds.contains_point(position) {
- let (point, target_point) =
- paint.point_for_position(&self.snapshot(cx), layout, position);
+ let point = if text_bounds.contains_point(position) {
+ let (point, target_point) = position_map.point_for_position(text_bounds, position);
if point == target_point {
Some(point)
} else {
@@ -319,23 +402,6 @@ impl EditorElement {
shift_held: shift,
});
- if paint
- .context_menu_bounds
- .map_or(false, |context_menu_bounds| {
- context_menu_bounds.contains_point(position)
- })
- {
- return false;
- }
-
- if paint
- .hover_popover_bounds
- .iter()
- .any(|hover_bounds| hover_bounds.contains_point(position))
- {
- return false;
- }
-
cx.dispatch_action(HoverAt { point });
true
}
@@ -349,28 +415,27 @@ impl EditorElement {
}
fn scroll(
- &self,
position: Vector2F,
mut delta: Vector2F,
precise: bool,
- layout: &mut LayoutState,
- paint: &mut PaintState,
+ position_map: &PositionMap,
+ bounds: RectF,
cx: &mut EventContext,
) -> bool {
- if !paint.bounds.contains_point(position) {
+ if !bounds.contains_point(position) {
return false;
}
- let snapshot = self.snapshot(cx.app);
- let max_glyph_width = layout.em_width;
+ let max_glyph_width = position_map.em_width;
if !precise {
- delta *= vec2f(max_glyph_width, layout.line_height);
+ delta *= vec2f(max_glyph_width, position_map.line_height);
}
- let scroll_position = snapshot.scroll_position();
+ let scroll_position = position_map.snapshot.scroll_position();
let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width;
- let y = (scroll_position.y() * layout.line_height - delta.y()) / layout.line_height;
- let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), layout.scroll_max);
+ let y =
+ (scroll_position.y() * position_map.line_height - delta.y()) / position_map.line_height;
+ let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), position_map.scroll_max);
cx.dispatch_action(Scroll(scroll_position));
@@ -385,7 +450,8 @@ impl EditorElement {
cx: &mut PaintContext,
) {
let bounds = gutter_bounds.union_rect(text_bounds);
- let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
+ let scroll_top =
+ layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
let editor = self.view(cx.app);
cx.scene.push_quad(Quad {
bounds: gutter_bounds,
@@ -414,11 +480,12 @@ impl EditorElement {
if !contains_non_empty_selection {
let origin = vec2f(
bounds.origin_x(),
- bounds.origin_y() + (layout.line_height * *start_row as f32) - scroll_top,
+ bounds.origin_y() + (layout.position_map.line_height * *start_row as f32)
+ - scroll_top,
);
let size = vec2f(
bounds.width(),
- layout.line_height * (end_row - start_row + 1) as f32,
+ layout.position_map.line_height * (end_row - start_row + 1) as f32,
);
cx.scene.push_quad(Quad {
bounds: RectF::new(origin, size),
@@ -432,12 +499,13 @@ impl EditorElement {
if let Some(highlighted_rows) = &layout.highlighted_rows {
let origin = vec2f(
bounds.origin_x(),
- bounds.origin_y() + (layout.line_height * highlighted_rows.start as f32)
+ bounds.origin_y()
+ + (layout.position_map.line_height * highlighted_rows.start as f32)
- scroll_top,
);
let size = vec2f(
bounds.width(),
- layout.line_height * highlighted_rows.len() as f32,
+ layout.position_map.line_height * highlighted_rows.len() as f32,
);
cx.scene.push_quad(Quad {
bounds: RectF::new(origin, size),
@@ -456,23 +524,30 @@ impl EditorElement {
layout: &mut LayoutState,
cx: &mut PaintContext,
) {
- let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
+ let scroll_top =
+ layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
if let Some(line) = line {
let line_origin = bounds.origin()
+ vec2f(
bounds.width() - line.width() - layout.gutter_padding,
- ix as f32 * layout.line_height - (scroll_top % layout.line_height),
+ ix as f32 * layout.position_map.line_height
+ - (scroll_top % layout.position_map.line_height),
);
- line.paint(line_origin, visible_bounds, layout.line_height, cx);
+ line.paint(
+ line_origin,
+ visible_bounds,
+ layout.position_map.line_height,
+ cx,
+ );
}
}
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
let mut x = bounds.width() - layout.gutter_padding;
- let mut y = *row as f32 * layout.line_height - scroll_top;
+ let mut y = *row as f32 * layout.position_map.line_height - scroll_top;
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
- y += (layout.line_height - indicator.size().y()) / 2.;
+ y += (layout.position_map.line_height - indicator.size().y()) / 2.;
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
}
}
@@ -482,17 +557,17 @@ impl EditorElement {
bounds: RectF,
visible_bounds: RectF,
layout: &mut LayoutState,
- paint: &mut PaintState,
cx: &mut PaintContext,
) {
let view = self.view(cx.app);
let style = &self.style;
let local_replica_id = view.replica_id(cx);
- let scroll_position = layout.snapshot.scroll_position();
+ let scroll_position = layout.position_map.snapshot.scroll_position();
let start_row = scroll_position.y() as u32;
- let scroll_top = scroll_position.y() * layout.line_height;
- let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
- let max_glyph_width = layout.em_width;
+ let scroll_top = scroll_position.y() * layout.position_map.line_height;
+ let end_row =
+ ((scroll_top + bounds.height()) / layout.position_map.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
+ let max_glyph_width = layout.position_map.em_width;
let scroll_left = scroll_position.x() * max_glyph_width;
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
@@ -514,7 +589,7 @@ impl EditorElement {
end_row,
*color,
0.,
- 0.15 * layout.line_height,
+ 0.15 * layout.position_map.line_height,
layout,
content_origin,
scroll_top,
@@ -527,7 +602,7 @@ impl EditorElement {
let mut cursors = SmallVec::<[Cursor; 32]>::new();
for (replica_id, selections) in &layout.selections {
let selection_style = style.replica_selection_style(*replica_id);
- let corner_radius = 0.15 * layout.line_height;
+ let corner_radius = 0.15 * layout.position_map.line_height;
for selection in selections {
self.paint_highlighted_range(
@@ -548,50 +623,52 @@ impl EditorElement {
if view.show_local_cursors() || *replica_id != local_replica_id {
let cursor_position = selection.head;
if (start_row..end_row).contains(&cursor_position.row()) {
- let cursor_row_layout =
- &layout.line_layouts[(cursor_position.row() - start_row) as usize];
+ let cursor_row_layout = &layout.position_map.line_layouts
+ [(cursor_position.row() - start_row) as usize];
let cursor_column = cursor_position.column() as usize;
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
let mut block_width =
cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
if block_width == 0.0 {
- block_width = layout.em_width;
+ block_width = layout.position_map.em_width;
}
-
- let block_text =
- if let CursorShape::Block = self.cursor_shape {
- layout.snapshot.chars_at(cursor_position).next().and_then(
- |character| {
- let font_id =
- cursor_row_layout.font_for_index(cursor_column)?;
- let text = character.to_string();
-
- Some(cx.text_layout_cache.layout_str(
- &text,
- cursor_row_layout.font_size(),
- &[(
- text.len(),
- RunStyle {
- font_id,
- color: style.background,
- underline: Default::default(),
- },
- )],
- ))
- },
- )
- } else {
- None
- };
+ let block_text = if let CursorShape::Block = self.cursor_shape {
+ layout
+ .position_map
+ .snapshot
+ .chars_at(cursor_position)
+ .next()
+ .and_then(|character| {
+ let font_id =
+ cursor_row_layout.font_for_index(cursor_column)?;
+ let text = character.to_string();
+
+ Some(cx.text_layout_cache.layout_str(
+ &text,
+ cursor_row_layout.font_size(),
+ &[(
+ text.len(),
+ RunStyle {
+ font_id,
+ color: style.background,
+ underline: Default::default(),
+ },
+ )],
+ ))
+ })
+ } else {
+ None
+ };
let x = cursor_character_x - scroll_left;
- let y = cursor_position.row() as f32 * layout.line_height - scroll_top;
+ let y = cursor_position.row() as f32 * layout.position_map.line_height
+ - scroll_top;
cursors.push(Cursor {
color: selection_style.cursor,
block_width,
origin: vec2f(x, y),
- line_height: layout.line_height,
+ line_height: layout.position_map.line_height,
shape: self.cursor_shape,
block_text,
});
@@ -602,13 +679,16 @@ impl EditorElement {
if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) {
// Draw glyphs
- for (ix, line) in layout.line_layouts.iter().enumerate() {
+ for (ix, line) in layout.position_map.line_layouts.iter().enumerate() {
let row = start_row + ix as u32;
line.paint(
content_origin
- + vec2f(-scroll_left, row as f32 * layout.line_height - scroll_top),
+ + vec2f(
+ -scroll_left,
+ row as f32 * layout.position_map.line_height - scroll_top,
+ ),
visible_text_bounds,
- layout.line_height,
+ layout.position_map.line_height,
cx,
);
}
@@ -622,9 +702,10 @@ impl EditorElement {
if let Some((position, context_menu)) = layout.context_menu.as_mut() {
cx.scene.push_stacking_context(None);
- let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
+ let cursor_row_layout =
+ &layout.position_map.line_layouts[(position.row() - start_row) as usize];
let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
- let y = (position.row() + 1) as f32 * layout.line_height - scroll_top;
+ let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top;
let mut list_origin = content_origin + vec2f(x, y);
let list_width = context_menu.size().x();
let list_height = context_menu.size().y();
@@ -636,7 +717,7 @@ impl EditorElement {
}
if list_origin.y() + list_height > bounds.max_y() {
- list_origin.set_y(list_origin.y() - layout.line_height - list_height);
+ list_origin.set_y(list_origin.y() - layout.position_map.line_height - list_height);
}
context_menu.paint(
@@ -645,8 +726,6 @@ impl EditorElement {
cx,
);
- paint.context_menu_bounds = Some(RectF::new(list_origin, context_menu.size()));
-
cx.scene.pop_stacking_context();
}
@@ -654,22 +733,21 @@ impl EditorElement {
cx.scene.push_stacking_context(None);
// This is safe because we check on layout whether the required row is available
- let hovered_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
+ let hovered_row_layout =
+ &layout.position_map.line_layouts[(position.row() - start_row) as usize];
// Minimum required size: Take the first popover, and add 1.5 times the minimum popover
// height. This is the size we will use to decide whether to render popovers above or below
// the hovered line.
let first_size = hover_popovers[0].size();
- let height_to_reserve =
- first_size.y() + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.line_height;
+ let height_to_reserve = first_size.y()
+ + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height;
// Compute Hovered Point
let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
- let y = position.row() as f32 * layout.line_height - scroll_top;
+ let y = position.row() as f32 * layout.position_map.line_height - scroll_top;
let hovered_point = content_origin + vec2f(x, y);
- paint.hover_popover_bounds.clear();
-
if hovered_point.y() - height_to_reserve > 0.0 {
// There is enough space above. Render popovers above the hovered point
let mut current_y = hovered_point.y();
@@ -688,16 +766,11 @@ impl EditorElement {
cx,
);
- paint.hover_popover_bounds.push(
- RectF::new(popover_origin, hover_popover.size())
- .dilate(Vector2F::new(0., 5.)),
- );
-
current_y = popover_origin.y() - HOVER_POPOVER_GAP;
}
} else {
// There is not enough space above. Render popovers below the hovered point
- let mut current_y = hovered_point.y() + layout.line_height;
+ let mut current_y = hovered_point.y() + layout.position_map.line_height;
for hover_popover in hover_popovers {
let size = hover_popover.size();
let mut popover_origin = vec2f(hovered_point.x(), current_y);
@@ -713,11 +786,6 @@ impl EditorElement {
cx,
);
- paint.hover_popover_bounds.push(
- RectF::new(popover_origin, hover_popover.size())
- .dilate(Vector2F::new(0., 5.)),
- );
-
current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP;
}
}
@@ -753,14 +821,16 @@ impl EditorElement {
let highlighted_range = HighlightedRange {
color,
- line_height: layout.line_height,
+ line_height: layout.position_map.line_height,
corner_radius,
- start_y: content_origin.y() + row_range.start as f32 * layout.line_height
+ start_y: content_origin.y()
+ + row_range.start as f32 * layout.position_map.line_height
- scroll_top,
lines: row_range
.into_iter()
.map(|row| {
- let line_layout = &layout.line_layouts[(row - start_row) as usize];
+ let line_layout =
+ &layout.position_map.line_layouts[(row - start_row) as usize];
HighlightedRangeLine {
start_x: if row == range.start.row() {
content_origin.x()
@@ -793,13 +863,16 @@ impl EditorElement {
layout: &mut LayoutState,
cx: &mut PaintContext,
) {
- let scroll_position = layout.snapshot.scroll_position();
- let scroll_left = scroll_position.x() * layout.em_width;
- let scroll_top = scroll_position.y() * layout.line_height;
+ let scroll_position = layout.position_map.snapshot.scroll_position();
+ let scroll_left = scroll_position.x() * layout.position_map.em_width;
+ let scroll_top = scroll_position.y() * layout.position_map.line_height;
for block in &mut layout.blocks {
- let mut origin =
- bounds.origin() + vec2f(0., block.row as f32 * layout.line_height - scroll_top);
+ let mut origin = bounds.origin()
+ + vec2f(
+ 0.,
+ block.row as f32 * layout.position_map.line_height - scroll_top,
+ );
if !matches!(block.style, BlockStyle::Sticky) {
origin += vec2f(-scroll_left, 0.);
}
@@ -1048,7 +1121,7 @@ impl EditorElement {
enum JumpIcon {}
cx.render(&editor, |_, cx| {
- MouseEventHandler::new::<JumpIcon, _, _>(*key, cx, |state, _| {
+ MouseEventHandler::<JumpIcon>::new(*key, cx, |state, _| {
let style = style.jump_icon.style_for(state, false);
Svg::new("icons/arrow_up_right_8.svg")
.with_color(style.color)
@@ -1181,7 +1254,7 @@ impl EditorElement {
impl Element for EditorElement {
type LayoutState = LayoutState;
- type PaintState = PaintState;
+ type PaintState = ();
fn layout(
&mut self,
@@ -1483,22 +1556,24 @@ impl Element for EditorElement {
(
size,
LayoutState {
- size,
- scroll_max,
+ position_map: Arc::new(PositionMap {
+ size,
+ scroll_max,
+ line_layouts,
+ line_height,
+ em_width,
+ em_advance,
+ snapshot,
+ }),
gutter_size,
gutter_padding,
text_size,
gutter_margin,
- snapshot,
active_rows,
highlighted_rows,
highlighted_ranges,
- line_layouts,
line_number_layouts,
blocks,
- line_height,
- em_width,
- em_advance,
selections,
context_menu,
code_actions_indicator,
@@ -1522,19 +1597,21 @@ impl Element for EditorElement {
layout.text_size,
);
- let mut paint_state = PaintState {
- bounds,
- gutter_bounds,
+ Self::attach_mouse_handlers(
+ &self.view,
+ &layout.position_map,
+ visible_bounds,
text_bounds,
- context_menu_bounds: None,
- hover_popover_bounds: Default::default(),
- };
+ gutter_bounds,
+ bounds,
+ cx,
+ );
self.paint_background(gutter_bounds, text_bounds, layout, cx);
if layout.gutter_size.x() > 0. {
self.paint_gutter(gutter_bounds, visible_bounds, layout, cx);
}
- self.paint_text(text_bounds, visible_bounds, layout, &mut paint_state, cx);
+ self.paint_text(text_bounds, visible_bounds, layout, cx);
if !layout.blocks.is_empty() {
cx.scene.push_layer(Some(bounds));
@@ -1543,8 +1620,6 @@ impl Element for EditorElement {
}
cx.scene.pop_layer();
-
- paint_state
}
fn dispatch_event(
@@ -1552,78 +1627,15 @@ impl Element for EditorElement {
event: &Event,
_: RectF,
_: RectF,
- layout: &mut LayoutState,
- paint: &mut PaintState,
+ _: &mut LayoutState,
+ _: &mut (),
cx: &mut EventContext,
) -> bool {
- if let Some((_, context_menu)) = &mut layout.context_menu {
- if context_menu.dispatch_event(event, cx) {
- return true;
- }
- }
-
- if let Some((_, indicator)) = &mut layout.code_actions_indicator {
- if indicator.dispatch_event(event, cx) {
- return true;
- }
- }
-
- if let Some((_, popover_elements)) = &mut layout.hover_popovers {
- for popover_element in popover_elements.iter_mut() {
- if popover_element.dispatch_event(event, cx) {
- return true;
- }
- }
+ if let Event::ModifiersChanged(event) = event {
+ self.modifiers_changed(*event, cx);
}
- for block in &mut layout.blocks {
- if block.element.dispatch_event(event, cx) {
- return true;
- }
- }
-
- match event {
- &Event::MouseDown(
- event @ MouseButtonEvent {
- button: MouseButton::Left,
- ..
- },
- ) => self.mouse_down(event, layout, paint, cx),
-
- &Event::MouseDown(MouseButtonEvent {
- button: MouseButton::Right,
- position,
- ..
- }) => self.mouse_right_down(position, layout, paint, cx),
-
- &Event::MouseUp(MouseButtonEvent {
- button: MouseButton::Left,
- position,
- cmd,
- shift,
- ..
- }) => self.mouse_up(position, cmd, shift, layout, paint, cx),
-
- Event::MouseMoved(
- event @ MouseMovedEvent {
- pressed_button: Some(MouseButton::Left),
- ..
- },
- ) => self.mouse_dragged(*event, layout, paint, cx),
-
- Event::ScrollWheel(ScrollWheelEvent {
- position,
- delta,
- precise,
- ..
- }) => self.scroll(*position, *delta, *precise, layout, paint, cx),
-
- &Event::ModifiersChanged(event) => self.modifiers_changed(event, cx),
-
- &Event::MouseMoved(event) => self.mouse_moved(event, layout, paint, cx),
-
- _ => false,
- }
+ false
}
fn rect_for_text_range(
@@ -1640,26 +1652,34 @@ impl Element for EditorElement {
layout.text_size,
);
let content_origin = text_bounds.origin() + vec2f(layout.gutter_margin, 0.);
- let scroll_position = layout.snapshot.scroll_position();
+ let scroll_position = layout.position_map.snapshot.scroll_position();
let start_row = scroll_position.y() as u32;
- let scroll_top = scroll_position.y() * layout.line_height;
- let scroll_left = scroll_position.x() * layout.em_width;
+ let scroll_top = scroll_position.y() * layout.position_map.line_height;
+ let scroll_left = scroll_position.x() * layout.position_map.em_width;
- let range_start =
- OffsetUtf16(range_utf16.start).to_display_point(&layout.snapshot.display_snapshot);
+ let range_start = OffsetUtf16(range_utf16.start)
+ .to_display_point(&layout.position_map.snapshot.display_snapshot);
if range_start.row() < start_row {
return None;
}
let line = layout
+ .position_map
.line_layouts
.get((range_start.row() - start_row) as usize)?;
let range_start_x = line.x_for_index(range_start.column() as usize);
- let range_start_y = range_start.row() as f32 * layout.line_height;
+ let range_start_y = range_start.row() as f32 * layout.position_map.line_height;
Some(RectF::new(
- content_origin + vec2f(range_start_x, range_start_y + layout.line_height)
+ content_origin
+ + vec2f(
+ range_start_x,
+ range_start_y + layout.position_map.line_height,
+ )
- vec2f(scroll_left, scroll_top),
- vec2f(layout.em_width, layout.line_height),
+ vec2f(
+ layout.position_map.em_width,
+ layout.position_map.line_height,
+ ),
))
}
@@ -1678,21 +1698,15 @@ impl Element for EditorElement {
}
pub struct LayoutState {
- size: Vector2F,
- scroll_max: Vector2F,
+ position_map: Arc<PositionMap>,
gutter_size: Vector2F,
gutter_padding: f32,
gutter_margin: f32,
text_size: Vector2F,
- snapshot: EditorSnapshot,
active_rows: BTreeMap<u32, bool>,
highlighted_rows: Option<Range<u32>>,
- line_layouts: Vec<text_layout::Line>,
line_number_layouts: Vec<Option<text_layout::Line>>,
blocks: Vec<BlockLayout>,
- line_height: f32,
- em_width: f32,
- em_advance: f32,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
context_menu: Option<(DisplayPoint, ElementBox)>,
@@ -1700,6 +1714,52 @@ pub struct LayoutState {
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
}
+pub struct PositionMap {
+ size: Vector2F,
+ line_height: f32,
+ scroll_max: Vector2F,
+ em_width: f32,
+ em_advance: f32,
+ line_layouts: Vec<text_layout::Line>,
+ snapshot: EditorSnapshot,
+}
+
+impl PositionMap {
+ /// Returns two display points:
+ /// 1. The nearest *valid* position in the editor
+ /// 2. An unclipped, potentially *invalid* position that maps directly to
+ /// the given pixel position.
+ fn point_for_position(
+ &self,
+ text_bounds: RectF,
+ position: Vector2F,
+ ) -> (DisplayPoint, DisplayPoint) {
+ let scroll_position = self.snapshot.scroll_position();
+ let position = position - text_bounds.origin();
+ let y = position.y().max(0.0).min(self.size.y());
+ let x = position.x() + (scroll_position.x() * self.em_width);
+ let row = (y / self.line_height + scroll_position.y()) as u32;
+ let (column, x_overshoot) = if let Some(line) = self
+ .line_layouts
+ .get(row as usize - scroll_position.y() as usize)
+ {
+ if let Some(ix) = line.index_for_x(x) {
+ (ix as u32, 0.0)
+ } else {
+ (line.len() as u32, 0f32.max(x - line.width()))
+ }
+ } else {
+ (0, x)
+ };
+
+ let mut target_point = DisplayPoint::new(row, column);
+ let point = self.snapshot.clip_point(target_point, Bias::Left);
+ *target_point.column_mut() += (x_overshoot / self.em_advance) as u32;
+
+ (point, target_point)
+ }
+}
+
struct BlockLayout {
row: u32,
element: ElementBox,
@@ -20,6 +20,10 @@ use crate::{
pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
+pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
+pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
+pub const HOVER_POPOVER_GAP: f32 = 10.;
+
#[derive(Clone, PartialEq)]
pub struct HoverAt {
pub point: Option<DisplayPoint>,
@@ -312,7 +316,7 @@ pub struct InfoPopover {
impl InfoPopover {
pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
- MouseEventHandler::new::<InfoPopover, _, _>(0, cx, |_, cx| {
+ MouseEventHandler::<InfoPopover>::new(0, cx, |_, cx| {
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
flex.extend(self.contents.iter().map(|content| {
let project = self.project.read(cx);
@@ -350,10 +354,11 @@ impl InfoPopover {
.with_style(style.hover_popover.container)
.boxed()
})
+ .on_move(|_, _| {})
.with_cursor_style(CursorStyle::Arrow)
.with_padding(Padding {
- bottom: 5.,
- top: 5.,
+ bottom: HOVER_POPOVER_GAP,
+ top: HOVER_POPOVER_GAP,
..Default::default()
})
.boxed()
@@ -383,13 +388,19 @@ impl DiagnosticPopover {
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
- MouseEventHandler::new::<DiagnosticPopover, _, _>(0, cx, |_, _| {
+ MouseEventHandler::<DiagnosticPopover>::new(0, cx, |_, _| {
Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style)
.with_soft_wrap(true)
.contained()
.with_style(container_style)
.boxed()
})
+ .with_padding(Padding {
+ top: HOVER_POPOVER_GAP,
+ bottom: HOVER_POPOVER_GAP,
+ ..Default::default()
+ })
+ .on_move(|_, _| {})
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(GoToDiagnostic)
})
@@ -1,5 +1,8 @@
use context_menu::ContextMenuItem;
-use gpui::{geometry::vector::Vector2F, impl_internal_actions, MutableAppContext, ViewContext};
+use gpui::{
+ elements::AnchorCorner, geometry::vector::Vector2F, impl_internal_actions, MutableAppContext,
+ ViewContext,
+};
use crate::{
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
@@ -46,6 +49,7 @@ pub fn deploy_context_menu(
editor.mouse_context_menu.update(cx, |menu, cx| {
menu.show(
position,
+ AnchorCorner::TopLeft,
vec![
ContextMenuItem::item("Rename Symbol", Rename),
ContextMenuItem::item("Go To Definition", GoToDefinition),
@@ -88,7 +88,7 @@ pub struct EditorTestContext<'a> {
}
impl<'a> EditorTestContext<'a> {
- pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
+ pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
let (window_id, editor) = cx.update(|cx| {
cx.set_global(Settings::test(cx));
crate::init(cx);
@@ -364,7 +364,8 @@ impl<'a> EditorLspTestContext<'a> {
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
.await;
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
@@ -316,7 +316,8 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
@@ -370,7 +371,8 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
@@ -444,7 +446,8 @@ mod tests {
cx,
)
.await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
finder
@@ -468,7 +471,8 @@ mod tests {
cx,
)
.await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
@@ -520,7 +524,8 @@ mod tests {
cx,
)
.await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
@@ -558,7 +563,8 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
finder
@@ -1981,6 +1981,7 @@ impl MutableAppContext {
{
let mut app = self.upgrade();
let presenter = Rc::downgrade(&presenter);
+
window.on_event(Box::new(move |event| {
app.update(|cx| {
if let Some(presenter) = presenter.upgrade() {
@@ -4094,7 +4095,7 @@ pub struct RenderParams {
pub view_id: usize,
pub titlebar_height: f32,
pub hovered_region_ids: HashSet<MouseRegionId>,
- pub clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
+ pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
pub refreshing: bool,
pub appearance: Appearance,
}
@@ -4104,7 +4105,7 @@ pub struct RenderContext<'a, T: View> {
pub(crate) view_id: usize,
pub(crate) view_type: PhantomData<T>,
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
- pub(crate) clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
+ pub(crate) clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
pub app: &'a mut MutableAppContext,
pub titlebar_height: f32,
pub appearance: Appearance,
@@ -4145,10 +4146,7 @@ impl<'a, V: View> RenderContext<'a, V> {
}
pub fn mouse_state<Tag: 'static>(&self, region_id: usize) -> MouseState {
- let region_id = MouseRegionId {
- view_id: self.view_id,
- discriminant: (TypeId::of::<Tag>(), region_id),
- };
+ let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
MouseState {
hovered: self.hovered_region_ids.contains(®ion_id),
clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| {
@@ -4161,9 +4159,10 @@ impl<'a, V: View> RenderContext<'a, V> {
}
}
- pub fn element_state<Tag: 'static, T: 'static + Default>(
+ pub fn element_state<Tag: 'static, T: 'static>(
&mut self,
element_id: usize,
+ initial: T,
) -> ElementStateHandle<T> {
let id = ElementStateId {
view_id: self.view_id(),
@@ -4173,9 +4172,16 @@ impl<'a, V: View> RenderContext<'a, V> {
self.cx
.element_states
.entry(id)
- .or_insert_with(|| Box::new(T::default()));
+ .or_insert_with(|| Box::new(initial));
ElementStateHandle::new(id, self.frame_count, &self.cx.ref_counts)
}
+
+ pub fn default_element_state<Tag: 'static, T: 'static + Default>(
+ &mut self,
+ element_id: usize,
+ ) -> ElementStateHandle<T> {
+ self.element_state::<Tag, T>(element_id, T::default())
+ }
}
impl AsRef<AppContext> for &AppContext {
@@ -5298,6 +5304,10 @@ impl<T: 'static> ElementStateHandle<T> {
}
}
+ pub fn id(&self) -> ElementStateId {
+ self.id
+ }
+
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
cx.element_states
.get(&self.id)
@@ -6101,12 +6111,12 @@ mod tests {
}
impl super::View for View {
- fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+ fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+ enum Handler {}
let mouse_down_count = self.mouse_down_count.clone();
- EventHandler::new(Empty::new().boxed())
- .on_mouse_down(move |_| {
+ MouseEventHandler::<Handler>::new(0, cx, |_, _| Empty::new().boxed())
+ .on_down(MouseButton::Left, move |_, _| {
mouse_down_count.fetch_add(1, SeqCst);
- true
})
.boxed()
}
@@ -3,7 +3,6 @@ mod canvas;
mod constrained_box;
mod container;
mod empty;
-mod event_handler;
mod expanded;
mod flex;
mod hook;
@@ -13,6 +12,7 @@ mod label;
mod list;
mod mouse_event_handler;
mod overlay;
+mod resizable;
mod stack;
mod svg;
mod text;
@@ -21,8 +21,8 @@ mod uniform_list;
use self::expanded::Expanded;
pub use self::{
- align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
- hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
+ align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*,
+ keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*,
stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
};
pub use crate::presenter::ChildView;
@@ -187,6 +187,27 @@ pub trait Element {
{
Tooltip::new::<Tag, T>(id, text, action, style, self.boxed(), cx)
}
+
+ fn with_resize_handle<Tag: 'static, T: View>(
+ self,
+ element_id: usize,
+ side: Side,
+ handle_size: f32,
+ initial_size: f32,
+ cx: &mut RenderContext<T>,
+ ) -> Resizable
+ where
+ Self: 'static + Sized,
+ {
+ Resizable::new::<Tag, T>(
+ self.boxed(),
+ element_id,
+ side,
+ handle_size,
+ initial_size,
+ cx,
+ )
+ }
}
pub enum Lifecycle<T: Element> {
@@ -373,6 +373,24 @@ pub struct Padding {
pub right: f32,
}
+impl Padding {
+ pub fn horizontal(padding: f32) -> Self {
+ Self {
+ left: padding,
+ right: padding,
+ ..Default::default()
+ }
+ }
+
+ pub fn vertical(padding: f32) -> Self {
+ Self {
+ top: padding,
+ bottom: padding,
+ ..Default::default()
+ }
+ }
+}
+
impl<'de> Deserialize<'de> for Padding {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -1,177 +0,0 @@
-use crate::{
- geometry::vector::Vector2F, presenter::MeasurementContext, CursorRegion, DebugContext, Element,
- ElementBox, Event, EventContext, LayoutContext, MouseButton, MouseButtonEvent, MouseRegion,
- NavigationDirection, PaintContext, SizeConstraint,
-};
-use pathfinder_geometry::rect::RectF;
-use serde_json::json;
-use std::{any::TypeId, ops::Range};
-
-pub struct EventHandler {
- child: ElementBox,
- capture_all: Option<(TypeId, usize)>,
- mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
- right_mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
- navigate_mouse_down: Option<Box<dyn FnMut(NavigationDirection, &mut EventContext) -> bool>>,
-}
-
-impl EventHandler {
- pub fn new(child: ElementBox) -> Self {
- Self {
- child,
- capture_all: None,
- mouse_down: None,
- right_mouse_down: None,
- navigate_mouse_down: None,
- }
- }
-
- pub fn on_mouse_down<F>(mut self, callback: F) -> Self
- where
- F: 'static + FnMut(&mut EventContext) -> bool,
- {
- self.mouse_down = Some(Box::new(callback));
- self
- }
-
- pub fn on_right_mouse_down<F>(mut self, callback: F) -> Self
- where
- F: 'static + FnMut(&mut EventContext) -> bool,
- {
- self.right_mouse_down = Some(Box::new(callback));
- self
- }
-
- pub fn on_navigate_mouse_down<F>(mut self, callback: F) -> Self
- where
- F: 'static + FnMut(NavigationDirection, &mut EventContext) -> bool,
- {
- self.navigate_mouse_down = Some(Box::new(callback));
- self
- }
-
- pub fn capture_all<T: 'static>(mut self, id: usize) -> Self {
- self.capture_all = Some((TypeId::of::<T>(), id));
- self
- }
-}
-
-impl Element for EventHandler {
- type LayoutState = ();
- type PaintState = ();
-
- fn layout(
- &mut self,
- constraint: SizeConstraint,
- cx: &mut LayoutContext,
- ) -> (Vector2F, Self::LayoutState) {
- let size = self.child.layout(constraint, cx);
- (size, ())
- }
-
- fn paint(
- &mut self,
- bounds: RectF,
- visible_bounds: RectF,
- _: &mut Self::LayoutState,
- cx: &mut PaintContext,
- ) -> Self::PaintState {
- if let Some(discriminant) = self.capture_all {
- cx.scene.push_stacking_context(None);
- cx.scene.push_cursor_region(CursorRegion {
- bounds: visible_bounds,
- style: Default::default(),
- });
- cx.scene.push_mouse_region(MouseRegion::handle_all(
- cx.current_view_id(),
- Some(discriminant),
- visible_bounds,
- ));
- cx.scene.pop_stacking_context();
- }
- self.child.paint(bounds.origin(), visible_bounds, cx);
- }
-
- fn dispatch_event(
- &mut self,
- event: &Event,
- _: RectF,
- visible_bounds: RectF,
- _: &mut Self::LayoutState,
- _: &mut Self::PaintState,
- cx: &mut EventContext,
- ) -> bool {
- if self.capture_all.is_some() {
- return true;
- }
-
- if self.child.dispatch_event(event, cx) {
- true
- } else {
- match event {
- Event::MouseDown(MouseButtonEvent {
- button: MouseButton::Left,
- position,
- ..
- }) => {
- if let Some(callback) = self.mouse_down.as_mut() {
- if visible_bounds.contains_point(*position) {
- return callback(cx);
- }
- }
- false
- }
- Event::MouseDown(MouseButtonEvent {
- button: MouseButton::Right,
- position,
- ..
- }) => {
- if let Some(callback) = self.right_mouse_down.as_mut() {
- if visible_bounds.contains_point(*position) {
- return callback(cx);
- }
- }
- false
- }
- Event::MouseDown(MouseButtonEvent {
- button: MouseButton::Navigate(direction),
- position,
- ..
- }) => {
- if let Some(callback) = self.navigate_mouse_down.as_mut() {
- if visible_bounds.contains_point(*position) {
- return callback(*direction, cx);
- }
- }
- false
- }
- _ => false,
- }
- }
- }
-
- fn rect_for_text_range(
- &self,
- range_utf16: Range<usize>,
- _: RectF,
- _: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- cx: &MeasurementContext,
- ) -> Option<RectF> {
- self.child.rect_for_text_range(range_utf16, cx)
- }
-
- fn debug(
- &self,
- _: RectF,
- _: &Self::LayoutState,
- _: &Self::PaintState,
- cx: &DebugContext,
- ) -> serde_json::Value {
- json!({
- "type": "EventHandler",
- "child": self.child.debug(cx),
- })
- }
-}
@@ -52,7 +52,7 @@ impl Flex {
Tag: 'static,
V: View,
{
- let scroll_state = cx.element_state::<Tag, ScrollState>(element_id);
+ let scroll_state = cx.default_element_state::<Tag, ScrollState>(element_id);
scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to);
self.scroll_state = Some(scroll_state);
self
@@ -13,31 +13,32 @@ use crate::{
MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
};
use serde_json::json;
-use std::{any::TypeId, ops::Range};
+use std::{marker::PhantomData, ops::Range};
-pub struct MouseEventHandler {
+pub struct MouseEventHandler<Tag: 'static> {
child: ElementBox,
- discriminant: (TypeId, usize),
+ region_id: usize,
cursor_style: Option<CursorStyle>,
handlers: HandlerSet,
hoverable: bool,
padding: Padding,
+ _tag: PhantomData<Tag>,
}
-impl MouseEventHandler {
- pub fn new<Tag, V, F>(id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
+impl<Tag> MouseEventHandler<Tag> {
+ pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
where
- Tag: 'static,
V: View,
F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
{
Self {
- child: render_child(cx.mouse_state::<Tag>(id), cx),
+ child: render_child(cx.mouse_state::<Tag>(region_id), cx),
+ region_id,
cursor_style: None,
- discriminant: (TypeId::of::<Tag>(), id),
handlers: Default::default(),
hoverable: true,
padding: Default::default(),
+ _tag: PhantomData,
}
}
@@ -140,7 +141,7 @@ impl MouseEventHandler {
}
}
-impl Element for MouseEventHandler {
+impl<Tag> Element for MouseEventHandler<Tag> {
type LayoutState = ();
type PaintState = ();
@@ -168,9 +169,9 @@ impl Element for MouseEventHandler {
}
cx.scene.push_mouse_region(
- MouseRegion::from_handlers(
+ MouseRegion::from_handlers::<Tag>(
cx.current_view_id(),
- Some(self.discriminant),
+ self.region_id,
hit_bounds,
self.handlers.clone(),
)
@@ -4,14 +4,15 @@ use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::ToJson,
presenter::MeasurementContext,
- DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
+ Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
PaintContext, SizeConstraint,
};
use serde_json::json;
pub struct Overlay {
child: ElementBox,
- abs_position: Option<Vector2F>,
+ anchor_position: Option<Vector2F>,
+ anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode,
hoverable: bool,
}
@@ -19,31 +20,79 @@ pub struct Overlay {
#[derive(Copy, Clone)]
pub enum OverlayFitMode {
SnapToWindow,
- FlipAlignment,
+ SwitchAnchor,
None,
}
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum AnchorCorner {
+ TopLeft,
+ TopRight,
+ BottomLeft,
+ BottomRight,
+}
+
+impl AnchorCorner {
+ fn get_bounds(&self, anchor_position: Vector2F, size: Vector2F) -> RectF {
+ match self {
+ Self::TopLeft => RectF::from_points(anchor_position, anchor_position + size),
+ Self::TopRight => RectF::from_points(
+ anchor_position - Vector2F::new(size.x(), 0.),
+ anchor_position + Vector2F::new(0., size.y()),
+ ),
+ Self::BottomLeft => RectF::from_points(
+ anchor_position - Vector2F::new(0., size.y()),
+ anchor_position + Vector2F::new(size.x(), 0.),
+ ),
+ Self::BottomRight => RectF::from_points(anchor_position - size, anchor_position),
+ }
+ }
+
+ fn switch_axis(self, axis: Axis) -> Self {
+ match axis {
+ Axis::Vertical => match self {
+ AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
+ AnchorCorner::TopRight => AnchorCorner::BottomRight,
+ AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
+ AnchorCorner::BottomRight => AnchorCorner::TopRight,
+ },
+ Axis::Horizontal => match self {
+ AnchorCorner::TopLeft => AnchorCorner::TopRight,
+ AnchorCorner::TopRight => AnchorCorner::TopLeft,
+ AnchorCorner::BottomLeft => AnchorCorner::BottomRight,
+ AnchorCorner::BottomRight => AnchorCorner::BottomLeft,
+ },
+ }
+ }
+}
+
impl Overlay {
pub fn new(child: ElementBox) -> Self {
Self {
child,
- abs_position: None,
+ anchor_position: None,
+ anchor_corner: AnchorCorner::TopLeft,
fit_mode: OverlayFitMode::None,
hoverable: false,
}
}
- pub fn with_abs_position(mut self, position: Vector2F) -> Self {
- self.abs_position = Some(position);
+ pub fn with_anchor_position(mut self, position: Vector2F) -> Self {
+ self.anchor_position = Some(position);
self
}
- pub fn fit_mode(mut self, fit_mode: OverlayFitMode) -> Self {
+ pub fn with_anchor_corner(mut self, anchor_corner: AnchorCorner) -> Self {
+ self.anchor_corner = anchor_corner;
+ self
+ }
+
+ pub fn with_fit_mode(mut self, fit_mode: OverlayFitMode) -> Self {
self.fit_mode = fit_mode;
self
}
- pub fn hoverable(mut self, hoverable: bool) -> Self {
+ pub fn with_hoverable(mut self, hoverable: bool) -> Self {
self.hoverable = hoverable;
self
}
@@ -58,7 +107,7 @@ impl Element for Overlay {
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
- let constraint = if self.abs_position.is_some() {
+ let constraint = if self.anchor_position.is_some() {
SizeConstraint::new(Vector2F::zero(), cx.window_size)
} else {
constraint
@@ -74,45 +123,75 @@ impl Element for Overlay {
size: &mut Self::LayoutState,
cx: &mut PaintContext,
) {
- let mut bounds = RectF::new(self.abs_position.unwrap_or_else(|| bounds.origin()), *size);
- cx.scene.push_stacking_context(None);
-
- if self.hoverable {
- cx.scene.push_mouse_region(MouseRegion {
- view_id: cx.current_view_id(),
- bounds,
- ..Default::default()
- });
- }
+ let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
+ let mut bounds = self.anchor_corner.get_bounds(anchor_position, *size);
match self.fit_mode {
OverlayFitMode::SnapToWindow => {
- // Snap the right edge of the overlay to the right edge of the window if
- // its horizontal bounds overflow.
- if bounds.lower_right().x() > cx.window_size.x() {
- bounds.set_origin_x((cx.window_size.x() - bounds.width()).max(0.));
+ // Snap the horizontal edges of the overlay to the horizontal edges of the window if
+ // its horizontal bounds overflow
+ if bounds.max_x() > cx.window_size.x() {
+ let mut lower_right = bounds.lower_right();
+ lower_right.set_x(cx.window_size.x());
+ bounds = RectF::from_points(lower_right - *size, lower_right);
+ } else if bounds.min_x() < 0. {
+ let mut upper_left = bounds.origin();
+ upper_left.set_x(0.);
+ bounds = RectF::from_points(upper_left, upper_left + *size);
}
- // Snap the bottom edge of the overlay to the bottom edge of the window if
+ // Snap the vertical edges of the overlay to the vertical edges of the window if
// its vertical bounds overflow.
- if bounds.lower_right().y() > cx.window_size.y() {
- bounds.set_origin_y((cx.window_size.y() - bounds.height()).max(0.));
+ if bounds.max_y() > cx.window_size.y() {
+ let mut lower_right = bounds.lower_right();
+ lower_right.set_y(cx.window_size.y());
+ bounds = RectF::from_points(lower_right - *size, lower_right);
+ } else if bounds.min_y() < 0. {
+ let mut upper_left = bounds.origin();
+ upper_left.set_y(0.);
+ bounds = RectF::from_points(upper_left, upper_left + *size);
}
}
- OverlayFitMode::FlipAlignment => {
- // Right-align overlay if its horizontal bounds overflow.
- if bounds.lower_right().x() > cx.window_size.x() {
- bounds.set_origin_x(bounds.origin_x() - bounds.width());
+ OverlayFitMode::SwitchAnchor => {
+ let mut anchor_corner = self.anchor_corner;
+
+ if bounds.max_x() > cx.window_size.x() {
+ anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
+ }
+
+ if bounds.max_y() > cx.window_size.y() {
+ anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
+ }
+
+ if bounds.min_x() < 0. {
+ anchor_corner = anchor_corner.switch_axis(Axis::Horizontal)
}
- // Bottom-align overlay if its vertical bounds overflow.
- if bounds.lower_right().y() > cx.window_size.y() {
- bounds.set_origin_y(bounds.origin_y() - bounds.height());
+ if bounds.min_y() < 0. {
+ anchor_corner = anchor_corner.switch_axis(Axis::Vertical)
+ }
+
+ // Update bounds if needed
+ if anchor_corner != self.anchor_corner {
+ bounds = anchor_corner.get_bounds(anchor_position, *size)
}
}
OverlayFitMode::None => {}
}
+ cx.scene.push_stacking_context(None);
+
+ if self.hoverable {
+ enum OverlayHoverCapture {}
+ // Block hovers in lower stacking contexts
+ cx.scene
+ .push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
+ cx.current_view_id(),
+ cx.current_view_id(),
+ bounds,
+ ));
+ }
+
self.child.paint(bounds.origin(), bounds, cx);
cx.scene.pop_stacking_context();
}
@@ -150,7 +229,7 @@ impl Element for Overlay {
) -> serde_json::Value {
json!({
"type": "Overlay",
- "abs_position": self.abs_position.to_json(),
+ "abs_position": self.anchor_position.to_json(),
"child": self.child.debug(cx),
})
}
@@ -0,0 +1,225 @@
+use std::{cell::Cell, rc::Rc};
+
+use pathfinder_geometry::vector::{vec2f, Vector2F};
+use serde_json::json;
+
+use crate::{
+ geometry::rect::RectF, scene::DragRegionEvent, Axis, CursorStyle, Element, ElementBox,
+ ElementStateHandle, MouseButton, MouseRegion, RenderContext, View,
+};
+
+use super::{ConstrainedBox, Hook};
+
+#[derive(Copy, Clone, Debug)]
+pub enum Side {
+ Top,
+ Bottom,
+ Left,
+ Right,
+}
+
+impl Side {
+ fn axis(&self) -> Axis {
+ match self {
+ Side::Left | Side::Right => Axis::Horizontal,
+ Side::Top | Side::Bottom => Axis::Vertical,
+ }
+ }
+
+ /// 'before' is in reference to the standard english document ordering of left-to-right
+ /// then top-to-bottom
+ fn before_content(self) -> bool {
+ match self {
+ Side::Left | Side::Top => true,
+ Side::Right | Side::Bottom => false,
+ }
+ }
+
+ fn relevant_component(&self, vector: Vector2F) -> f32 {
+ match self.axis() {
+ Axis::Horizontal => vector.x(),
+ Axis::Vertical => vector.y(),
+ }
+ }
+
+ fn compute_delta(&self, e: DragRegionEvent) -> f32 {
+ if self.before_content() {
+ self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
+ } else {
+ self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
+ }
+ }
+
+ 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 => {
+ let mut origin = bounds.lower_left();
+ origin.set_y(origin.y() - handle_size);
+ RectF::new(origin, vec2f(bounds.width(), handle_size))
+ }
+ Side::Right => {
+ let mut origin = bounds.upper_right();
+ origin.set_x(origin.x() - handle_size);
+ RectF::new(origin, vec2f(handle_size, bounds.height()))
+ }
+ }
+ }
+}
+
+struct ResizeHandleState {
+ actual_dimension: Cell<f32>,
+ custom_dimension: Cell<f32>,
+}
+
+pub struct Resizable {
+ side: Side,
+ handle_size: f32,
+ child: ElementBox,
+ state: Rc<ResizeHandleState>,
+ _state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
+}
+
+impl Resizable {
+ pub fn new<Tag: 'static, T: View>(
+ child: ElementBox,
+ element_id: usize,
+ side: Side,
+ handle_size: f32,
+ initial_size: f32,
+ cx: &mut RenderContext<T>,
+ ) -> Self {
+ let state_handle = cx.element_state::<Tag, Rc<ResizeHandleState>>(
+ 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()),
+ }
+ .boxed()
+ })
+ .on_after_layout({
+ let state = state.clone();
+ move |size, _| {
+ state.actual_dimension.set(side.relevant_component(size));
+ }
+ })
+ .boxed();
+
+ Self {
+ side,
+ child,
+ handle_size,
+ state,
+ _state_handle: state_handle,
+ }
+ }
+
+ pub fn current_size(&self) -> f32 {
+ self.state.actual_dimension.get()
+ }
+}
+
+impl Element for Resizable {
+ type LayoutState = ();
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: crate::SizeConstraint,
+ cx: &mut crate::LayoutContext,
+ ) -> (Vector2F, Self::LayoutState) {
+ (self.child.layout(constraint, cx), ())
+ }
+
+ fn paint(
+ &mut self,
+ bounds: pathfinder_geometry::rect::RectF,
+ visible_bounds: pathfinder_geometry::rect::RectF,
+ _child_size: &mut Self::LayoutState,
+ cx: &mut crate::PaintContext,
+ ) -> Self::PaintState {
+ cx.scene.push_stacking_context(None);
+
+ let handle_region = self.side.of_rect(bounds, self.handle_size);
+
+ enum ResizeHandle {}
+ cx.scene.push_mouse_region(
+ MouseRegion::new::<ResizeHandle>(
+ cx.current_view_id(),
+ self.side as usize,
+ handle_region,
+ )
+ .on_down(MouseButton::Left, |_, _| {}) // 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, cx| {
+ let prev_width = state.actual_dimension.get();
+ state
+ .custom_dimension
+ .set(0f32.max(prev_width + side.compute_delta(e)).round());
+ cx.notify();
+ }
+ }),
+ );
+
+ cx.scene.push_cursor_region(crate::CursorRegion {
+ bounds: handle_region,
+ style: match self.side.axis() {
+ Axis::Horizontal => CursorStyle::ResizeLeftRight,
+ Axis::Vertical => CursorStyle::ResizeUpDown,
+ },
+ });
+
+ cx.scene.pop_stacking_context();
+
+ self.child.paint(bounds.origin(), visible_bounds, cx);
+ }
+
+ fn dispatch_event(
+ &mut self,
+ event: &crate::Event,
+ _bounds: pathfinder_geometry::rect::RectF,
+ _visible_bounds: pathfinder_geometry::rect::RectF,
+ _layout: &mut Self::LayoutState,
+ _paint: &mut Self::PaintState,
+ cx: &mut crate::EventContext,
+ ) -> bool {
+ self.child.dispatch_event(event, cx)
+ }
+
+ fn rect_for_text_range(
+ &self,
+ range_utf16: std::ops::Range<usize>,
+ _bounds: pathfinder_geometry::rect::RectF,
+ _visible_bounds: pathfinder_geometry::rect::RectF,
+ _layout: &Self::LayoutState,
+ _paint: &Self::PaintState,
+ cx: &crate::MeasurementContext,
+ ) -> Option<pathfinder_geometry::rect::RectF> {
+ self.child.rect_for_text_range(range_utf16, cx)
+ }
+
+ fn debug(
+ &self,
+ _bounds: pathfinder_geometry::rect::RectF,
+ _layout: &Self::LayoutState,
+ _paint: &Self::PaintState,
+ cx: &crate::DebugContext,
+ ) -> serde_json::Value {
+ json!({
+ "child": self.child.debug(cx),
+ })
+ }
+}
@@ -62,7 +62,7 @@ impl Tooltip {
struct ElementState<Tag>(Tag);
struct MouseEventHandlerState<Tag>(Tag);
- let state_handle = cx.element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
+ let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
let state = state_handle.read(cx).clone();
let tooltip = if state.visible.get() {
let mut collapsed_tooltip = Self::render_tooltip(
@@ -84,42 +84,41 @@ impl Tooltip {
})
.boxed(),
)
- .fit_mode(OverlayFitMode::FlipAlignment)
- .with_abs_position(state.position.get())
+ .with_fit_mode(OverlayFitMode::SwitchAnchor)
+ .with_anchor_position(state.position.get())
.boxed(),
)
} else {
None
};
- let child =
- MouseEventHandler::new::<MouseEventHandlerState<Tag>, _, _>(id, cx, |_, _| child)
- .on_hover(move |e, cx| {
- let position = e.position;
- let window_id = cx.window_id();
- if let Some(view_id) = cx.view_id() {
- if e.started {
- if !state.visible.get() {
- state.position.set(position);
+ let child = MouseEventHandler::<MouseEventHandlerState<Tag>>::new(id, cx, |_, _| child)
+ .on_hover(move |e, cx| {
+ let position = e.position;
+ let window_id = cx.window_id();
+ if let Some(view_id) = cx.view_id() {
+ if e.started {
+ if !state.visible.get() {
+ state.position.set(position);
- let mut debounce = state.debounce.borrow_mut();
- if debounce.is_none() {
- *debounce = Some(cx.spawn({
- let state = state.clone();
- |mut cx| async move {
- cx.background().timer(DEBOUNCE_TIMEOUT).await;
- state.visible.set(true);
- cx.update(|cx| cx.notify_view(window_id, view_id));
- }
- }));
- }
+ let mut debounce = state.debounce.borrow_mut();
+ if debounce.is_none() {
+ *debounce = Some(cx.spawn({
+ let state = state.clone();
+ |mut cx| async move {
+ cx.background().timer(DEBOUNCE_TIMEOUT).await;
+ state.visible.set(true);
+ cx.update(|cx| cx.notify_view(window_id, view_id));
+ }
+ }));
}
- } else {
- state.visible.set(false);
- state.debounce.take();
}
+ } else {
+ state.visible.set(false);
+ state.debounce.take();
}
- })
- .boxed();
+ }
+ })
+ .boxed();
Self {
child,
tooltip,
@@ -195,6 +195,7 @@ pub enum PromptLevel {
pub enum CursorStyle {
Arrow,
ResizeLeftRight,
+ ResizeUpDown,
PointingHand,
IBeam,
}
@@ -690,6 +690,7 @@ impl platform::Platform for MacPlatform {
let cursor: id = match style {
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
+ CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
};
@@ -8,7 +8,8 @@ use crate::{
platform::{CursorStyle, Event},
scene::{
ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
- HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+ HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent,
+ UpOutRegionEvent, UpRegionEvent,
},
text_layout::TextLayoutCache,
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, Appearance, AssetCache, ElementBox,
@@ -36,7 +37,7 @@ pub struct Presenter {
asset_cache: Arc<AssetCache>,
last_mouse_moved_event: Option<Event>,
hovered_region_ids: HashSet<MouseRegionId>,
- clicked_regions: Vec<MouseRegion>,
+ clicked_region_ids: HashSet<MouseRegionId>,
clicked_button: Option<MouseButton>,
mouse_position: Vector2F,
titlebar_height: f32,
@@ -63,7 +64,7 @@ impl Presenter {
asset_cache,
last_mouse_moved_event: None,
hovered_region_ids: Default::default(),
- clicked_regions: Vec::new(),
+ clicked_region_ids: Default::default(),
clicked_button: None,
mouse_position: vec2f(0., 0.),
titlebar_height,
@@ -91,15 +92,9 @@ impl Presenter {
view_id: *view_id,
titlebar_height: self.titlebar_height,
hovered_region_ids: self.hovered_region_ids.clone(),
- clicked_region_ids: self.clicked_button.map(|button| {
- (
- self.clicked_regions
- .iter()
- .filter_map(MouseRegion::id)
- .collect(),
- button,
- )
- }),
+ clicked_region_ids: self
+ .clicked_button
+ .map(|button| (self.clicked_region_ids.clone(), button)),
refreshing: false,
appearance,
})
@@ -123,15 +118,9 @@ impl Presenter {
view_id: *view_id,
titlebar_height: self.titlebar_height,
hovered_region_ids: self.hovered_region_ids.clone(),
- clicked_region_ids: self.clicked_button.map(|button| {
- (
- self.clicked_regions
- .iter()
- .filter_map(MouseRegion::id)
- .collect(),
- button,
- )
- }),
+ clicked_region_ids: self
+ .clicked_button
+ .map(|button| (self.clicked_region_ids.clone(), button)),
refreshing: true,
appearance,
})
@@ -196,15 +185,9 @@ impl Presenter {
view_stack: Vec::new(),
refreshing,
hovered_region_ids: self.hovered_region_ids.clone(),
- clicked_region_ids: self.clicked_button.map(|button| {
- (
- self.clicked_regions
- .iter()
- .filter_map(MouseRegion::id)
- .collect(),
- button,
- )
- }),
+ clicked_region_ids: self
+ .clicked_button
+ .map(|button| (self.clicked_region_ids.clone(), button)),
titlebar_height: self.titlebar_height,
appearance: self.appearance,
window_size,
@@ -248,6 +231,7 @@ impl Presenter {
) -> bool {
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
let mut events_to_send = Vec::new();
+ let mut invalidated_views: HashSet<usize> = Default::default();
// 1. Allocate the correct set of GPUI events generated from the platform events
// -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
@@ -261,16 +245,23 @@ impl Presenter {
// If there is already clicked_button stored, don't replace it.
if self.clicked_button.is_none() {
- self.clicked_regions = self
+ self.clicked_region_ids = self
.mouse_regions
.iter()
.filter_map(|(region, _)| {
- region
- .bounds
- .contains_point(e.position)
- .then(|| region.clone())
+ if region.bounds.contains_point(e.position) {
+ Some(region.id())
+ } else {
+ None
+ }
})
.collect();
+
+ // Clicked status is used when rendering views via the RenderContext.
+ // So when it changes, these views need to be rerendered
+ for clicked_region_id in self.clicked_region_ids.iter() {
+ invalidated_views.insert(clicked_region_id.view_id());
+ }
self.clicked_button = Some(e.button);
}
@@ -354,6 +345,12 @@ impl Presenter {
self.last_mouse_moved_event = Some(event.clone());
}
+ Event::ScrollWheel(e) => {
+ events_to_send.push(MouseRegionEvent::ScrollWheel(ScrollWheelRegionEvent {
+ region: Default::default(),
+ platform_event: e.clone(),
+ }))
+ }
_ => {}
}
@@ -362,7 +359,6 @@ impl Presenter {
self.mouse_position = position;
}
- let mut invalidated_views: HashSet<usize> = Default::default();
let mut any_event_handled = false;
// 2. Process the raw mouse events into region events
for mut region_event in events_to_send {
@@ -388,23 +384,21 @@ impl Presenter {
top_most_depth = Some(depth);
}
- if let Some(region_id) = region.id() {
- // This unwrap relies on short circuiting boolean expressions
- // The right side of the && is only executed when contains_mouse
- // is true, and we know above that when contains_mouse is true
- // top_most_depth is set
- if contains_mouse && depth == top_most_depth.unwrap() {
- //Ensure that hover entrance events aren't sent twice
- if self.hovered_region_ids.insert(region_id) {
- valid_regions.push(region.clone());
- invalidated_views.insert(region.view_id);
- }
- } else {
- // Ensure that hover exit events aren't sent twice
- if self.hovered_region_ids.remove(®ion_id) {
- valid_regions.push(region.clone());
- invalidated_views.insert(region.view_id);
- }
+ // This unwrap relies on short circuiting boolean expressions
+ // The right side of the && is only executed when contains_mouse
+ // is true, and we know above that when contains_mouse is true
+ // top_most_depth is set
+ if contains_mouse && depth == top_most_depth.unwrap() {
+ //Ensure that hover entrance events aren't sent twice
+ if self.hovered_region_ids.insert(region.id()) {
+ valid_regions.push(region.clone());
+ invalidated_views.insert(region.id().view_id());
+ }
+ } else {
+ // Ensure that hover exit events aren't sent twice
+ if self.hovered_region_ids.remove(®ion.id()) {
+ valid_regions.push(region.clone());
+ invalidated_views.insert(region.id().view_id());
}
}
}
@@ -417,21 +411,30 @@ impl Presenter {
.unwrap_or(false)
{
// Clear clicked regions and clicked button
- let clicked_regions =
- std::mem::replace(&mut self.clicked_regions, Vec::new());
+ let clicked_region_ids =
+ std::mem::replace(&mut self.clicked_region_ids, Default::default());
+ // Clicked status is used when rendering views via the RenderContext.
+ // So when it changes, these views need to be rerendered
+ for clicked_region_id in clicked_region_ids.iter() {
+ invalidated_views.insert(clicked_region_id.view_id());
+ }
self.clicked_button = None;
// Find regions which still overlap with the mouse since the last MouseDown happened
- for clicked_region in clicked_regions.into_iter().rev() {
- if clicked_region.bounds.contains_point(e.position) {
- valid_regions.push(clicked_region);
+ for (mouse_region, _) in self.mouse_regions.iter().rev() {
+ if clicked_region_ids.contains(&mouse_region.id()) {
+ if mouse_region.bounds.contains_point(self.mouse_position) {
+ valid_regions.push(mouse_region.clone());
+ }
}
}
}
}
MouseRegionEvent::Drag(_) => {
- for clicked_region in self.clicked_regions.iter().rev() {
- valid_regions.push(clicked_region.clone());
+ for (mouse_region, _) in self.mouse_regions.iter().rev() {
+ if self.clicked_region_ids.contains(&mouse_region.id()) {
+ valid_regions.push(mouse_region.clone());
+ }
}
}
@@ -460,18 +463,18 @@ impl Presenter {
region_event.set_region(valid_region.bounds);
if let MouseRegionEvent::Hover(e) = &mut region_event {
- e.started = valid_region
- .id()
- .map(|region_id| hovered_region_ids.contains(®ion_id))
- .unwrap_or(false)
+ e.started = hovered_region_ids.contains(&valid_region.id())
}
- // Handle Down events if the MouseRegion has a Click handler. This makes the api more intuitive as you would
+ // Handle Down events if the MouseRegion has a Click or Drag handler. This makes the api more intuitive as you would
// not expect a MouseRegion to be transparent to Down events if it also has a Click handler.
// This behavior can be overridden by adding a Down handler that calls cx.propogate_event
if let MouseRegionEvent::Down(e) = ®ion_event {
if valid_region
.handlers
.contains_handler(MouseRegionEvent::click_disc(), Some(e.button))
+ || valid_region
+ .handlers
+ .contains_handler(MouseRegionEvent::drag_disc(), Some(e.button))
{
event_cx.handled = true;
}
@@ -479,8 +482,10 @@ impl Presenter {
if let Some(callback) = valid_region.handlers.get(®ion_event.handler_key()) {
event_cx.handled = true;
- event_cx.invalidated_views.insert(valid_region.view_id);
- event_cx.with_current_view(valid_region.view_id, {
+ event_cx
+ .invalidated_views
+ .insert(valid_region.id().view_id());
+ event_cx.with_current_view(valid_region.id().view_id(), {
let region_event = region_event.clone();
|cx| {
callback(region_event, cx);
@@ -560,7 +565,7 @@ pub struct LayoutContext<'a> {
titlebar_height: f32,
appearance: Appearance,
hovered_region_ids: HashSet<MouseRegionId>,
- clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
+ clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
}
impl<'a> LayoutContext<'a> {
@@ -1,6 +1,8 @@
mod mouse_region;
mod mouse_region_event;
+#[cfg(debug_assertions)]
+use collections::HashSet;
use serde::Deserialize;
use serde_json::json;
use std::{borrow::Cow, sync::Arc};
@@ -20,6 +22,8 @@ pub struct Scene {
scale_factor: f32,
stacking_contexts: Vec<StackingContext>,
active_stacking_context_stack: Vec<usize>,
+ #[cfg(debug_assertions)]
+ mouse_region_ids: HashSet<MouseRegionId>,
}
struct StackingContext {
@@ -178,6 +182,8 @@ impl Scene {
scale_factor,
stacking_contexts: vec![stacking_context],
active_stacking_context_stack: vec![0],
+ #[cfg(debug_assertions)]
+ mouse_region_ids: Default::default(),
}
}
@@ -242,7 +248,24 @@ impl Scene {
pub fn push_mouse_region(&mut self, region: MouseRegion) {
if can_draw(region.bounds) {
- self.active_layer().push_mouse_region(region);
+ // Ensure that Regions cannot be added to a scene with the same region id.
+ #[cfg(debug_assertions)]
+ let region_id;
+ #[cfg(debug_assertions)]
+ {
+ region_id = region.id();
+ }
+
+ if self.active_layer().push_mouse_region(region) {
+ #[cfg(debug_assertions)]
+ {
+ if !self.mouse_region_ids.insert(region_id) {
+ let tag_name = region_id.tag_type_name();
+ panic!("Same MouseRegionId: {region_id:?} inserted multiple times to the same scene. \
+ Will cause problems! Look for MouseRegion that uses Tag: {tag_name}");
+ }
+ }
+ }
}
}
@@ -370,15 +393,17 @@ impl Layer {
}
}
- fn push_mouse_region(&mut self, region: MouseRegion) {
+ fn push_mouse_region(&mut self, region: MouseRegion) -> bool {
if let Some(bounds) = region
.bounds
.intersection(self.clip_bounds.unwrap_or(region.bounds))
{
if can_draw(bounds) {
self.mouse_regions.push(region);
+ return true;
}
}
+ false
}
fn push_underline(&mut self, underline: Underline) {
@@ -552,11 +577,8 @@ impl ToJson for Border {
}
impl MouseRegion {
- pub fn id(&self) -> Option<MouseRegionId> {
- self.discriminant.map(|discriminant| MouseRegionId {
- view_id: self.view_id,
- discriminant,
- })
+ pub fn id(&self) -> MouseRegionId {
+ self.id
}
}
@@ -1,4 +1,4 @@
-use std::{any::TypeId, mem::Discriminant, rc::Rc};
+use std::{any::TypeId, fmt::Debug, mem::Discriminant, rc::Rc};
use collections::HashMap;
@@ -6,50 +6,51 @@ use pathfinder_geometry::rect::RectF;
use crate::{EventContext, MouseButton};
-use super::mouse_region_event::{
- ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
- MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+use super::{
+ mouse_region_event::{
+ ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
+ MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+ },
+ ScrollWheelRegionEvent,
};
-#[derive(Clone, Default)]
+#[derive(Clone)]
pub struct MouseRegion {
- pub view_id: usize,
- pub discriminant: Option<(TypeId, usize)>,
+ pub id: MouseRegionId,
pub bounds: RectF,
pub handlers: HandlerSet,
pub hoverable: bool,
}
impl MouseRegion {
- pub fn new(view_id: usize, discriminant: Option<(TypeId, usize)>, bounds: RectF) -> Self {
- Self::from_handlers(view_id, discriminant, bounds, Default::default())
+ /// Region ID is used to track semantically equivalent mouse regions across render passes.
+ /// e.g. if you have mouse handlers attached to a list item type, then each item of the list
+ /// should pass a different (consistent) region_id. If you have one big region that covers your
+ /// whole component, just pass the view_id again.
+ pub fn new<Tag: 'static>(view_id: usize, region_id: usize, bounds: RectF) -> Self {
+ Self::from_handlers::<Tag>(view_id, region_id, bounds, Default::default())
}
- pub fn from_handlers(
- view_id: usize,
- discriminant: Option<(TypeId, usize)>,
- bounds: RectF,
- handlers: HandlerSet,
- ) -> Self {
- Self {
- view_id,
- discriminant,
- bounds,
- handlers,
- hoverable: true,
- }
+ pub fn handle_all<Tag: 'static>(view_id: usize, region_id: usize, bounds: RectF) -> Self {
+ Self::from_handlers::<Tag>(view_id, region_id, bounds, HandlerSet::capture_all())
}
- pub fn handle_all(
+ pub fn from_handlers<Tag: 'static>(
view_id: usize,
- discriminant: Option<(TypeId, usize)>,
+ region_id: usize,
bounds: RectF,
+ handlers: HandlerSet,
) -> Self {
Self {
- view_id,
- discriminant,
+ id: MouseRegionId {
+ view_id,
+ tag: TypeId::of::<Tag>(),
+ region_id,
+ #[cfg(debug_assertions)]
+ tag_type_name: std::any::type_name::<Tag>(),
+ },
bounds,
- handlers: HandlerSet::capture_all(),
+ handlers,
hoverable: true,
}
}
@@ -124,6 +125,14 @@ impl MouseRegion {
self
}
+ pub fn on_scroll(
+ mut self,
+ handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
+ ) -> Self {
+ self.handlers = self.handlers.on_scroll(handler);
+ self
+ }
+
pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
self.hoverable = is_hoverable;
self
@@ -132,8 +141,32 @@ impl MouseRegion {
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct MouseRegionId {
- pub view_id: usize,
- pub discriminant: (TypeId, usize),
+ view_id: usize,
+ tag: TypeId,
+ region_id: usize,
+ #[cfg(debug_assertions)]
+ tag_type_name: &'static str,
+}
+
+impl MouseRegionId {
+ pub(crate) fn new<Tag: 'static>(view_id: usize, region_id: usize) -> Self {
+ MouseRegionId {
+ view_id,
+ region_id,
+ tag: TypeId::of::<Tag>(),
+ #[cfg(debug_assertions)]
+ tag_type_name: std::any::type_name::<Tag>(),
+ }
+ }
+
+ pub fn view_id(&self) -> usize {
+ self.view_id
+ }
+
+ #[cfg(debug_assertions)]
+ pub fn tag_type_name(&self) -> &'static str {
+ self.tag_type_name
+ }
}
#[derive(Clone, Default)]
@@ -345,4 +378,22 @@ impl HandlerSet {
}));
self
}
+
+ pub fn on_scroll(
+ mut self,
+ handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
+ ) -> Self {
+ self.set.insert((MouseRegionEvent::scroll_wheel_disc(), None),
+ Rc::new(move |region_event, cx| {
+ if let MouseRegionEvent::ScrollWheel(e) = region_event {
+ handler(e, cx);
+ } else {
+ panic!(
+ "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ScrollWheel, found {:?}",
+ region_event
+ );
+ }
+ }));
+ self
+ }
}
@@ -168,7 +168,7 @@ impl MouseRegionEvent {
pub fn is_capturable(&self) -> bool {
match self {
MouseRegionEvent::Move(_) => true,
- MouseRegionEvent::Drag(_) => false,
+ MouseRegionEvent::Drag(_) => true,
MouseRegionEvent::Hover(_) => false,
MouseRegionEvent::Down(_) => true,
MouseRegionEvent::Up(_) => true,
@@ -109,7 +109,7 @@ impl View for Select {
Default::default()
};
let mut result = Flex::column().with_child(
- MouseEventHandler::new::<Header, _, _>(self.handle.id(), cx, |mouse_state, cx| {
+ MouseEventHandler::<Header>::new(self.handle.id(), cx, |mouse_state, cx| {
Container::new((self.render_item)(
self.selected_item_ix,
ItemType::Header,
@@ -137,22 +137,18 @@ impl View for Select {
let selected_item_ix = this.selected_item_ix;
range.end = range.end.min(this.item_count);
items.extend(range.map(|ix| {
- MouseEventHandler::new::<Item, _, _>(
- ix,
- cx,
- |mouse_state, cx| {
- (this.render_item)(
- ix,
- if ix == selected_item_ix {
- ItemType::Selected
- } else {
- ItemType::Unselected
- },
- mouse_state.hovered,
- cx,
- )
- },
- )
+ MouseEventHandler::<Item>::new(ix, cx, |mouse_state, cx| {
+ (this.render_item)(
+ ix,
+ if ix == selected_item_ix {
+ ItemType::Selected
+ } else {
+ ItemType::Unselected
+ },
+ mouse_state.hovered,
+ cx,
+ )
+ })
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(SelectItem(ix))
})
@@ -85,7 +85,7 @@ impl<D: PickerDelegate> View for Picker<D> {
let selected_ix = delegate.read(cx).selected_index();
range.end = cmp::min(range.end, delegate.read(cx).match_count());
items.extend(range.map(move |ix| {
- MouseEventHandler::new::<D, _, _>(ix, cx, |state, cx| {
+ MouseEventHandler::<D>::new(ix, cx, |state, cx| {
delegate
.read(cx)
.render_match(ix, state, ix == selected_ix, cx)
@@ -5,8 +5,8 @@ use gpui::{
actions,
anyhow::{anyhow, Result},
elements::{
- ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement,
- ScrollTarget, Stack, Svg, UniformList, UniformListState,
+ AnchorCorner, ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler,
+ ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
},
geometry::vector::Vector2F,
impl_internal_actions, keymap,
@@ -302,7 +302,7 @@ impl ProjectPanel {
}
self.context_menu.update(cx, |menu, cx| {
- menu.show(action.position, menu_entries, cx);
+ menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx);
});
cx.notify();
@@ -1012,7 +1012,7 @@ impl ProjectPanel {
) -> ElementBox {
let kind = details.kind;
let show_editor = details.is_editing && !details.is_processing;
- MouseEventHandler::new::<Self, _, _>(entry_id.to_usize(), cx, |state, _| {
+ MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, _| {
let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
let mut style = theme.entry.style_for(state, details.is_selected).clone();
if details.is_ignored {
@@ -1107,7 +1107,7 @@ impl View for ProjectPanel {
let last_worktree_root_id = self.last_worktree_root_id;
Stack::new()
.with_child(
- MouseEventHandler::new::<Tag, _, _>(0, cx, |_, cx| {
+ MouseEventHandler::<Tag>::new(0, cx, |_, cx| {
UniformList::new(
self.list.clone(),
self.visible_entries
@@ -1243,7 +1243,8 @@ mod tests {
.await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx),
@@ -1335,7 +1336,8 @@ mod tests {
.await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
select_path(&panel, "root1", cx);
@@ -319,7 +319,7 @@ impl BufferSearchBar {
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
let is_active = self.is_search_option_enabled(option);
Some(
- MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
+ MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
let style = &cx
.global::<Settings>()
.theme
@@ -367,7 +367,7 @@ impl BufferSearchBar {
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
enum NavButton {}
- MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
+ MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
let style = &cx
.global::<Settings>()
.theme
@@ -176,7 +176,7 @@ impl View for ProjectSearchView {
} else {
"No results"
};
- MouseEventHandler::new::<Status, _, _>(0, cx, |_, _| {
+ MouseEventHandler::<Status>::new(0, cx, |_, _| {
Label::new(text.to_string(), theme.search.results_status.clone())
.aligned()
.contained()
@@ -723,7 +723,7 @@ impl ProjectSearchBar {
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
enum NavButton {}
- MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
+ MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
let style = &cx
.global::<Settings>()
.theme
@@ -758,7 +758,7 @@ impl ProjectSearchBar {
) -> ElementBox {
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
let is_active = self.is_option_enabled(option, cx);
- MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
+ MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
let style = &cx
.global::<Settings>()
.theme
@@ -29,6 +29,7 @@ pub struct Settings {
pub show_completions_on_input: bool,
pub vim_mode: bool,
pub autosave: Autosave,
+ pub default_dock_anchor: DockAnchor,
pub editor_defaults: EditorSettings,
pub editor_overrides: EditorSettings,
pub terminal_defaults: TerminalSettings,
@@ -98,6 +99,7 @@ pub struct TerminalSettings {
pub env: Option<HashMap<String, String>>,
pub blinking: Option<TerminalBlink>,
pub alternate_scroll: Option<AlternateScroll>,
+ pub option_as_meta: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -150,6 +152,15 @@ pub enum WorkingDirectory {
Always { directory: String },
}
+#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DockAnchor {
+ #[default]
+ Bottom,
+ Right,
+ Expanded,
+}
+
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct SettingsFileContent {
pub experiments: Option<FeatureFlags>,
@@ -167,6 +178,8 @@ pub struct SettingsFileContent {
pub vim_mode: Option<bool>,
#[serde(default)]
pub autosave: Option<Autosave>,
+ #[serde(default)]
+ pub default_dock_anchor: Option<DockAnchor>,
#[serde(flatten)]
pub editor: EditorSettings,
#[serde(default)]
@@ -216,6 +229,7 @@ impl Settings {
projects_online_by_default: defaults.projects_online_by_default.unwrap(),
vim_mode: defaults.vim_mode.unwrap(),
autosave: defaults.autosave.unwrap(),
+ default_dock_anchor: defaults.default_dock_anchor.unwrap(),
editor_defaults: EditorSettings {
tab_size: required(defaults.editor.tab_size),
hard_tabs: required(defaults.editor.hard_tabs),
@@ -268,6 +282,8 @@ impl Settings {
merge(&mut self.autosave, data.autosave);
merge(&mut self.experiments, data.experiments);
merge(&mut self.staff_mode, data.staff_mode);
+ merge(&mut self.default_dock_anchor, data.default_dock_anchor);
+
// Ensure terminal font is loaded, so we can request it in terminal_element layout
if let Some(terminal_font) = &data.terminal.font_family {
font_cache.load_family(&[terminal_font]).log_err();
@@ -336,6 +352,7 @@ impl Settings {
show_completions_on_input: true,
vim_mode: false,
autosave: Autosave::Off,
+ default_dock_anchor: DockAnchor::Bottom,
editor_defaults: EditorSettings {
tab_size: Some(4.try_into().unwrap()),
hard_tabs: Some(false),
@@ -2,7 +2,7 @@
use alacritty_terminal::term::TermMode;
use gpui::keymap::Keystroke;
-#[derive(Debug)]
+#[derive(Debug, PartialEq, Eq)]
pub enum Modifiers {
None,
Alt,
@@ -45,10 +45,10 @@ impl Modifiers {
///that depend on terminal modes also have a mapping that doesn't depend on the terminal mode.
///This is fragile, but as these mappings are locked up in legacy compatibility, it's probably good enough
pub fn might_convert(keystroke: &Keystroke) -> bool {
- to_esc_str(keystroke, &TermMode::NONE).is_some()
+ to_esc_str(keystroke, &TermMode::NONE, false).is_some()
}
-pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option<String> {
+pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode, alt_is_meta: bool) -> Option<String> {
let modifiers = Modifiers::new(keystroke);
// Manual Bindings including modifiers
@@ -244,6 +244,17 @@ pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option<String> {
}
}
+ let alt_meta_binding = if alt_is_meta && modifiers == Modifiers::Alt && keystroke.key.is_ascii()
+ {
+ Some(format!("\x1b{}", keystroke.key))
+ } else {
+ None
+ };
+
+ if alt_meta_binding.is_some() {
+ return alt_meta_binding;
+ }
+
None
}
@@ -286,26 +297,26 @@ mod test {
let shift_end = Keystroke::parse("shift-end").unwrap();
let none = TermMode::NONE;
- assert_eq!(to_esc_str(&shift_pageup, &none), None);
- assert_eq!(to_esc_str(&shift_pagedown, &none), None);
- assert_eq!(to_esc_str(&shift_home, &none), None);
- assert_eq!(to_esc_str(&shift_end, &none), None);
+ assert_eq!(to_esc_str(&shift_pageup, &none, false), None);
+ assert_eq!(to_esc_str(&shift_pagedown, &none, false), None);
+ assert_eq!(to_esc_str(&shift_home, &none, false), None);
+ assert_eq!(to_esc_str(&shift_end, &none, false), None);
let alt_screen = TermMode::ALT_SCREEN;
assert_eq!(
- to_esc_str(&shift_pageup, &alt_screen),
+ to_esc_str(&shift_pageup, &alt_screen, false),
Some("\x1b[5;2~".to_string())
);
assert_eq!(
- to_esc_str(&shift_pagedown, &alt_screen),
+ to_esc_str(&shift_pagedown, &alt_screen, false),
Some("\x1b[6;2~".to_string())
);
assert_eq!(
- to_esc_str(&shift_home, &alt_screen),
+ to_esc_str(&shift_home, &alt_screen, false),
Some("\x1b[1;2H".to_string())
);
assert_eq!(
- to_esc_str(&shift_end, &alt_screen),
+ to_esc_str(&shift_end, &alt_screen, false),
Some("\x1b[1;2F".to_string())
);
@@ -313,8 +324,14 @@ mod test {
let pagedown = Keystroke::parse("pagedown").unwrap();
let any = TermMode::ANY;
- assert_eq!(to_esc_str(&pageup, &any), Some("\x1b[5~".to_string()));
- assert_eq!(to_esc_str(&pagedown, &any), Some("\x1b[6~".to_string()));
+ assert_eq!(
+ to_esc_str(&pageup, &any, false),
+ Some("\x1b[5~".to_string())
+ );
+ assert_eq!(
+ to_esc_str(&pagedown, &any, false),
+ Some("\x1b[6~".to_string())
+ );
}
#[test]
@@ -327,7 +344,7 @@ mod test {
function: false,
key: "๐๐ป".to_string(), //2 char string
};
- assert_eq!(to_esc_str(&ks, &TermMode::NONE), None);
+ assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None);
}
#[test]
@@ -340,15 +357,27 @@ mod test {
let left = Keystroke::parse("left").unwrap();
let right = Keystroke::parse("right").unwrap();
- assert_eq!(to_esc_str(&up, &none), Some("\x1b[A".to_string()));
- assert_eq!(to_esc_str(&down, &none), Some("\x1b[B".to_string()));
- assert_eq!(to_esc_str(&right, &none), Some("\x1b[C".to_string()));
- assert_eq!(to_esc_str(&left, &none), Some("\x1b[D".to_string()));
+ assert_eq!(to_esc_str(&up, &none, false), Some("\x1b[A".to_string()));
+ assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".to_string()));
+ assert_eq!(to_esc_str(&right, &none, false), Some("\x1b[C".to_string()));
+ assert_eq!(to_esc_str(&left, &none, false), Some("\x1b[D".to_string()));
- assert_eq!(to_esc_str(&up, &app_cursor), Some("\x1bOA".to_string()));
- assert_eq!(to_esc_str(&down, &app_cursor), Some("\x1bOB".to_string()));
- assert_eq!(to_esc_str(&right, &app_cursor), Some("\x1bOC".to_string()));
- assert_eq!(to_esc_str(&left, &app_cursor), Some("\x1bOD".to_string()));
+ assert_eq!(
+ to_esc_str(&up, &app_cursor, false),
+ Some("\x1bOA".to_string())
+ );
+ assert_eq!(
+ to_esc_str(&down, &app_cursor, false),
+ Some("\x1bOB".to_string())
+ );
+ assert_eq!(
+ to_esc_str(&right, &app_cursor, false),
+ Some("\x1bOC".to_string())
+ );
+ assert_eq!(
+ to_esc_str(&left, &app_cursor, false),
+ Some("\x1bOD".to_string())
+ );
}
#[test]
@@ -361,11 +390,13 @@ mod test {
assert_eq!(
to_esc_str(
&Keystroke::parse(&format!("ctrl-{}", lower)).unwrap(),
- &mode
+ &mode,
+ false
),
to_esc_str(
&Keystroke::parse(&format!("ctrl-shift-{}", upper)).unwrap(),
- &mode
+ &mode,
+ false
),
"On letter: {}/{}",
lower,
@@ -374,6 +405,40 @@ mod test {
}
}
+ #[test]
+ fn alt_is_meta() {
+ let ascii_printable = ' '..='~';
+ for character in ascii_printable {
+ assert_eq!(
+ to_esc_str(
+ &Keystroke::parse(&format!("alt-{}", character)).unwrap(),
+ &TermMode::NONE,
+ true
+ )
+ .unwrap(),
+ format!("\x1b{}", character)
+ );
+ }
+
+ let gpui_keys = [
+ "up", "down", "right", "left", "f1", "f2", "f3", "f4", "F5", "f6", "f7", "f8", "f9",
+ "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "insert",
+ "pageup", "pagedown", "end", "home",
+ ];
+
+ for key in gpui_keys {
+ assert_ne!(
+ to_esc_str(
+ &Keystroke::parse(&format!("alt-{}", key)).unwrap(),
+ &TermMode::NONE,
+ true
+ )
+ .unwrap(),
+ format!("\x1b{}", key)
+ );
+ }
+ }
+
#[test]
fn test_modifier_code_calc() {
// Code Modifiers
@@ -6,6 +6,7 @@ use alacritty_terminal::grid::Dimensions;
/// with modifications for our circumstances
use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point, Side};
use alacritty_terminal::term::TermMode;
+use gpui::scene::ScrollWheelRegionEvent;
use gpui::{geometry::vector::Vector2F, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
use crate::TerminalSize;
@@ -114,7 +115,7 @@ impl MouseButton {
pub fn scroll_report(
point: Point,
scroll_lines: i32,
- e: &ScrollWheelEvent,
+ e: &ScrollWheelRegionEvent,
mode: TermMode,
) -> Option<impl Iterator<Item = Vec<u8>>> {
if mode.intersects(TermMode::MOUSE_MODE) {
@@ -1,78 +0,0 @@
-use gpui::{ModelHandle, ViewContext};
-use settings::{Settings, WorkingDirectory};
-use workspace::{programs::ProgramManager, Workspace};
-
-use crate::{
- terminal_container_view::{
- get_working_directory, DeployModal, TerminalContainer, TerminalContainerContent,
- },
- Event, Terminal,
-};
-
-pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
- let window = cx.window_id();
-
- // Pull the terminal connection out of the global if it has been stored
- let possible_terminal = ProgramManager::remove::<Terminal, _>(window, cx);
-
- if let Some(terminal_handle) = possible_terminal {
- workspace.toggle_modal(cx, |_, cx| {
- // Create a view from the stored connection if the terminal modal is not already shown
- cx.add_view(|cx| TerminalContainer::from_terminal(terminal_handle.clone(), true, cx))
- });
- // Toggle Modal will dismiss the terminal modal if it is currently shown, so we must
- // store the terminal back in the global
- ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
- } else {
- // No connection was stored, create a new terminal
- if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
- // No terminal modal visible, construct a new one.
- let wd_strategy = cx
- .global::<Settings>()
- .terminal_overrides
- .working_directory
- .clone()
- .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
-
- let working_directory = get_working_directory(workspace, cx, wd_strategy);
-
- let this = cx.add_view(|cx| TerminalContainer::new(working_directory, true, cx));
-
- if let TerminalContainerContent::Connected(connected) = &this.read(cx).content {
- let terminal_handle = connected.read(cx).handle();
- cx.subscribe(&terminal_handle, on_event).detach();
- // Set the global immediately if terminal construction was successful,
- // in case the user opens the command palette
- ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
- }
-
- this
- }) {
- // Terminal modal was dismissed and the terminal view is connected, store the terminal
- if let TerminalContainerContent::Connected(connected) =
- &closed_terminal_handle.read(cx).content
- {
- let terminal_handle = connected.read(cx).handle();
- // Set the global immediately if terminal construction was successful,
- // in case the user opens the command palette
- ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
- }
- }
- }
-}
-
-pub fn on_event(
- workspace: &mut Workspace,
- _: ModelHandle<Terminal>,
- event: &Event,
- cx: &mut ViewContext<Workspace>,
-) {
- // Dismiss the modal if the terminal quit
- if let Event::CloseTerminal = event {
- ProgramManager::remove::<Terminal, _>(cx.window_id(), cx);
-
- if workspace.modal::<TerminalContainer>().is_some() {
- workspace.dismiss_modal(cx)
- }
- }
-}
@@ -1,5 +1,4 @@
pub mod mappings;
-pub mod modal;
pub mod terminal_container_view;
pub mod terminal_element;
pub mod terminal_view;
@@ -32,7 +31,6 @@ use futures::{
use mappings::mouse::{
alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
};
-use modal::deploy_modal;
use procinfo::LocalProcessInfo;
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
@@ -51,9 +49,10 @@ use thiserror::Error;
use gpui::{
geometry::vector::{vec2f, Vector2F},
keymap::Keystroke,
- scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
- ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
- ScrollWheelEvent, Task,
+ scene::{
+ ClickRegionEvent, DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent,
+ },
+ ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task,
};
use crate::mappings::{
@@ -63,8 +62,6 @@ use crate::mappings::{
///Initialize and register all of our action handlers
pub fn init(cx: &mut MutableAppContext) {
- cx.add_action(deploy_modal);
-
terminal_view::init(cx);
terminal_container_view::init(cx);
}
@@ -677,8 +674,8 @@ impl Terminal {
self.write_to_pty(input);
}
- pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
- let esc = to_esc_str(keystroke, &self.last_content.mode);
+ pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool {
+ let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta);
if let Some(esc) = esc {
self.input(esc);
true
@@ -908,10 +905,10 @@ impl Terminal {
}
///Scroll the terminal
- pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Vector2F) {
+ pub fn scroll_wheel(&mut self, e: ScrollWheelRegionEvent, origin: Vector2F) {
let mouse_mode = self.mouse_mode(e.shift);
- if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) {
+ if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
if mouse_mode {
let point = mouse_point(
e.position.sub(origin),
@@ -920,7 +917,7 @@ impl Terminal {
);
if let Some(scrolls) =
- scroll_report(point, scroll_lines as i32, e, self.last_content.mode)
+ scroll_report(point, scroll_lines as i32, &e, self.last_content.mode)
{
for scroll in scrolls {
self.pty_tx.notify(scroll);
@@ -943,7 +940,11 @@ impl Terminal {
}
}
- fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option<i32> {
+ fn determine_scroll_lines(
+ &mut self,
+ e: &ScrollWheelRegionEvent,
+ mouse_mode: bool,
+ ) -> Option<i32> {
let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
match e.phase {
@@ -366,7 +366,7 @@ impl TerminalElement {
) {
let connection = self.terminal;
- let mut region = MouseRegion::new(view_id, None, visible_bounds);
+ let mut region = MouseRegion::new::<Self>(view_id, view_id, visible_bounds);
// Terminal Emulator controlled behavior:
region = region
@@ -427,7 +427,14 @@ impl TerminalElement {
position: e.position,
});
}
- });
+ })
+ .on_scroll(TerminalElement::generic_button_handler(
+ connection,
+ origin,
+ move |terminal, origin, e, _cx| {
+ terminal.scroll_wheel(e, origin);
+ },
+ ));
// Mouse mode handlers:
// All mouse modes need the extra click handlers
@@ -742,24 +749,13 @@ impl Element for TerminalElement {
fn dispatch_event(
&mut self,
event: &gpui::Event,
- bounds: gpui::geometry::rect::RectF,
- visible_bounds: gpui::geometry::rect::RectF,
- layout: &mut Self::LayoutState,
+ _bounds: gpui::geometry::rect::RectF,
+ _visible_bounds: gpui::geometry::rect::RectF,
+ _layout: &mut Self::LayoutState,
_paint: &mut Self::PaintState,
cx: &mut gpui::EventContext,
) -> bool {
match event {
- Event::ScrollWheel(e) => visible_bounds
- .contains_point(e.position)
- .then(|| {
- let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
-
- if let Some(terminal) = self.terminal.upgrade(cx.app) {
- terminal.update(cx.app, |term, _| term.scroll_wheel(e, origin));
- cx.notify();
- }
- })
- .is_some(),
Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
if !cx.is_parent_view_focused() {
return false;
@@ -775,7 +771,15 @@ impl Element for TerminalElement {
self.terminal
.upgrade(cx.app)
.map(|model_handle| {
- model_handle.update(cx.app, |term, _| term.try_keystroke(keystroke))
+ model_handle.update(cx.app, |term, cx| {
+ term.try_keystroke(
+ keystroke,
+ cx.global::<Settings>()
+ .terminal_overrides
+ .option_as_meta
+ .unwrap_or(false),
+ )
+ })
})
.unwrap_or(false)
}
@@ -4,7 +4,7 @@ use alacritty_terminal::{index::Point, term::TermMode};
use context_menu::{ContextMenu, ContextMenuItem};
use gpui::{
actions,
- elements::{ChildView, ParentElement, Stack},
+ elements::{AnchorCorner, ChildView, ParentElement, Stack},
geometry::vector::Vector2F,
impl_internal_actions,
keymap::Keystroke,
@@ -139,8 +139,9 @@ impl TerminalView {
ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
];
- self.context_menu
- .update(cx, |menu, cx| menu.show(action.position, menu_entries, cx));
+ self.context_menu.update(cx, |menu, cx| {
+ menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx)
+ });
cx.notify();
}
@@ -155,8 +156,14 @@ impl TerminalView {
{
cx.show_character_palette();
} else {
- self.terminal.update(cx, |term, _| {
- term.try_keystroke(&Keystroke::parse("ctrl-cmd-space").unwrap())
+ self.terminal.update(cx, |term, cx| {
+ term.try_keystroke(
+ &Keystroke::parse("ctrl-cmd-space").unwrap(),
+ cx.global::<Settings>()
+ .terminal_overrides
+ .option_as_meta
+ .unwrap_or(false),
+ )
});
}
}
@@ -280,7 +287,7 @@ impl TerminalView {
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal.update(cx, |term, _| {
- term.try_keystroke(&Keystroke::parse("up").unwrap())
+ term.try_keystroke(&Keystroke::parse("up").unwrap(), false)
});
}
@@ -288,7 +295,7 @@ impl TerminalView {
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal.update(cx, |term, _| {
- term.try_keystroke(&Keystroke::parse("down").unwrap())
+ term.try_keystroke(&Keystroke::parse("down").unwrap(), false)
});
}
@@ -296,7 +303,7 @@ impl TerminalView {
fn ctrl_c(&mut self, _: &CtrlC, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal.update(cx, |term, _| {
- term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap())
+ term.try_keystroke(&Keystroke::parse("ctrl-c").unwrap(), false)
});
}
@@ -304,7 +311,7 @@ impl TerminalView {
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal.update(cx, |term, _| {
- term.try_keystroke(&Keystroke::parse("escape").unwrap())
+ term.try_keystroke(&Keystroke::parse("escape").unwrap(), false)
});
}
@@ -312,7 +319,7 @@ impl TerminalView {
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.terminal.update(cx, |term, _| {
- term.try_keystroke(&Keystroke::parse("enter").unwrap())
+ term.try_keystroke(&Keystroke::parse("enter").unwrap(), false)
});
}
}
@@ -21,7 +21,9 @@ impl<'a> TerminalTestContext<'a> {
let params = self.cx.update(AppState::test);
let project = Project::test(params.fs.clone(), [], self.cx).await;
- let (_, workspace) = self.cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (_, workspace) = self
+ .cx
+ .add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
(project, workspace)
}
@@ -49,7 +49,7 @@ pub struct Workspace {
pub pane_divider: Border,
pub leader_border_opacity: f32,
pub leader_border_width: f32,
- pub sidebar_resize_handle: ContainerStyle,
+ pub sidebar: Sidebar,
pub status_bar: StatusBar,
pub toolbar: Toolbar,
pub disconnected_overlay: ContainedText,
@@ -58,6 +58,7 @@ pub struct Workspace {
pub notifications: Notifications,
pub joining_project_avatar: ImageStyle,
pub joining_project_message: ContainedText,
+ pub dock: Dock,
}
#[derive(Clone, Deserialize, Default)]
@@ -150,6 +151,16 @@ pub struct Toolbar {
pub nav_button: Interactive<IconButton>,
}
+#[derive(Clone, Deserialize, Default)]
+pub struct Dock {
+ pub initial_size_right: f32,
+ pub initial_size_bottom: f32,
+ pub wash_color: Color,
+ pub flex: f32,
+ pub panel: ContainerStyle,
+ pub maximized: ContainerStyle,
+}
+
#[derive(Clone, Deserialize, Default)]
pub struct Notifications {
#[serde(flatten)]
@@ -232,7 +243,9 @@ pub struct StatusBarLspStatus {
#[derive(Deserialize, Default)]
pub struct Sidebar {
- pub resize_handle: ContainerStyle,
+ pub initial_size: f32,
+ #[serde(flatten)]
+ pub container: ContainerStyle,
}
#[derive(Clone, Copy, Deserialize, Default)]
@@ -564,6 +577,7 @@ pub struct CodeActions {
pub struct Interactive<T> {
pub default: T,
pub hover: Option<T>,
+ pub clicked: Option<T>,
pub active: Option<T>,
pub disabled: Option<T>,
}
@@ -572,6 +586,8 @@ impl<T> Interactive<T> {
pub fn style_for(&self, state: MouseState, active: bool) -> &T {
if active {
self.active.as_ref().unwrap_or(&self.default)
+ } else if state.clicked == Some(gpui::MouseButton::Left) && self.clicked.is_some() {
+ self.clicked.as_ref().unwrap()
} else if state.hovered {
self.hover.as_ref().unwrap_or(&self.default)
} else {
@@ -594,6 +610,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
#[serde(flatten)]
default: Value,
hover: Option<Value>,
+ clicked: Option<Value>,
active: Option<Value>,
disabled: Option<Value>,
}
@@ -620,6 +637,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
};
let hover = deserialize_state(json.hover)?;
+ let clicked = deserialize_state(json.clicked)?;
let active = deserialize_state(json.active)?;
let disabled = deserialize_state(json.disabled)?;
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
@@ -627,6 +645,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
Ok(Interactive {
default,
hover,
+ clicked,
active,
disabled,
})
@@ -39,7 +39,8 @@ impl<'a> VimTestContext<'a> {
.insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
.await;
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
// Setup search toolbars
workspace.update(cx, |workspace, cx| {
@@ -0,0 +1,693 @@
+use collections::HashMap;
+use gpui::{
+ actions,
+ elements::{ChildView, Container, Empty, Margin, MouseEventHandler, Side, Svg},
+ impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton,
+ MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use serde::Deserialize;
+use settings::{DockAnchor, Settings};
+use theme::Theme;
+
+use crate::{sidebar::SidebarSide, ItemHandle, Pane, StatusItemView, Workspace};
+
+#[derive(PartialEq, Clone, Deserialize)]
+pub struct MoveDock(pub DockAnchor);
+
+#[derive(PartialEq, Clone)]
+pub struct AddDefaultItemToDock;
+
+actions!(workspace, [ToggleDock, ActivateOrHideDock]);
+impl_internal_actions!(workspace, [MoveDock, AddDefaultItemToDock]);
+
+pub fn init(cx: &mut MutableAppContext) {
+ cx.add_action(Dock::toggle);
+ cx.add_action(Dock::activate_or_hide_dock);
+ cx.add_action(Dock::move_dock);
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub enum DockPosition {
+ Shown(DockAnchor),
+ Hidden(DockAnchor),
+}
+
+impl Default for DockPosition {
+ fn default() -> Self {
+ DockPosition::Hidden(Default::default())
+ }
+}
+
+pub fn icon_for_dock_anchor(anchor: DockAnchor) -> &'static str {
+ match anchor {
+ DockAnchor::Right => "icons/dock_right_12.svg",
+ DockAnchor::Bottom => "icons/dock_bottom_12.svg",
+ DockAnchor::Expanded => "icons/dock_modal_12.svg",
+ }
+}
+
+impl DockPosition {
+ fn is_visible(&self) -> bool {
+ match self {
+ DockPosition::Shown(_) => true,
+ DockPosition::Hidden(_) => false,
+ }
+ }
+
+ fn anchor(&self) -> DockAnchor {
+ match self {
+ DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor,
+ }
+ }
+
+ fn toggle(self) -> Self {
+ match self {
+ DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
+ DockPosition::Hidden(anchor) => DockPosition::Shown(anchor),
+ }
+ }
+
+ fn hide(self) -> Self {
+ match self {
+ DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
+ DockPosition::Hidden(_) => self,
+ }
+ }
+
+ fn show(self) -> Self {
+ match self {
+ DockPosition::Hidden(anchor) => DockPosition::Shown(anchor),
+ DockPosition::Shown(_) => self,
+ }
+ }
+}
+
+pub type DefaultItemFactory =
+ fn(&mut Workspace, &mut ViewContext<Workspace>) -> Box<dyn ItemHandle>;
+
+pub struct Dock {
+ position: DockPosition,
+ panel_sizes: HashMap<DockAnchor, f32>,
+ pane: ViewHandle<Pane>,
+ default_item_factory: DefaultItemFactory,
+}
+
+impl Dock {
+ pub fn new(cx: &mut ViewContext<Workspace>, default_item_factory: DefaultItemFactory) -> Self {
+ let anchor = cx.global::<Settings>().default_dock_anchor;
+ let pane = cx.add_view(|cx| Pane::new(Some(anchor), cx));
+ pane.update(cx, |pane, cx| {
+ pane.set_active(false, cx);
+ });
+ let pane_id = pane.id();
+ cx.subscribe(&pane, move |workspace, _, event, cx| {
+ workspace.handle_pane_event(pane_id, event, cx);
+ })
+ .detach();
+
+ Self {
+ pane,
+ panel_sizes: Default::default(),
+ position: DockPosition::Hidden(anchor),
+ default_item_factory,
+ }
+ }
+
+ pub fn pane(&self) -> &ViewHandle<Pane> {
+ &self.pane
+ }
+
+ pub fn visible_pane(&self) -> Option<&ViewHandle<Pane>> {
+ self.position.is_visible().then(|| self.pane())
+ }
+
+ pub fn is_anchored_at(&self, anchor: DockAnchor) -> bool {
+ self.position.is_visible() && self.position.anchor() == anchor
+ }
+
+ fn set_dock_position(
+ workspace: &mut Workspace,
+ new_position: DockPosition,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ if workspace.dock.position == new_position {
+ return;
+ }
+
+ workspace.dock.position = new_position;
+ // Tell the pane about the new anchor position
+ workspace.dock.pane.update(cx, |pane, cx| {
+ pane.set_docked(Some(new_position.anchor()), cx)
+ });
+
+ if workspace.dock.position.is_visible() {
+ // Close the right sidebar if the dock is on the right side and the right sidebar is open
+ if workspace.dock.position.anchor() == DockAnchor::Right {
+ if workspace.right_sidebar().read(cx).is_open() {
+ workspace.toggle_sidebar(SidebarSide::Right, cx);
+ }
+ }
+
+ // Ensure that the pane has at least one item or construct a default item to put in it
+ let pane = workspace.dock.pane.clone();
+ if pane.read(cx).items().next().is_none() {
+ let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
+ // Adding the item focuses the pane by default
+ Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
+ } else {
+ cx.focus(pane);
+ }
+ } else if let Some(last_active_center_pane) = workspace.last_active_center_pane.clone() {
+ cx.focus(last_active_center_pane);
+ }
+ cx.emit(crate::Event::DockAnchorChanged);
+ cx.notify();
+ }
+
+ pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+ Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
+ }
+
+ pub fn show(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+ Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
+ }
+
+ pub fn hide_on_sidebar_shown(
+ workspace: &mut Workspace,
+ sidebar_side: SidebarSide,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ if (sidebar_side == SidebarSide::Right && workspace.dock.is_anchored_at(DockAnchor::Right))
+ || workspace.dock.is_anchored_at(DockAnchor::Expanded)
+ {
+ Self::hide(workspace, cx);
+ }
+ }
+
+ fn toggle(workspace: &mut Workspace, _: &ToggleDock, cx: &mut ViewContext<Workspace>) {
+ Self::set_dock_position(workspace, workspace.dock.position.toggle(), cx);
+ }
+
+ fn activate_or_hide_dock(
+ workspace: &mut Workspace,
+ _: &ActivateOrHideDock,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let dock_pane = workspace.dock_pane().clone();
+ if dock_pane.read(cx).is_active() {
+ Self::hide(workspace, cx);
+ } else {
+ Self::show(workspace, cx);
+ cx.focus(dock_pane);
+ }
+ }
+
+ fn move_dock(
+ workspace: &mut Workspace,
+ &MoveDock(new_anchor): &MoveDock,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx);
+ }
+
+ pub fn render(
+ &self,
+ theme: &Theme,
+ anchor: DockAnchor,
+ cx: &mut RenderContext<Workspace>,
+ ) -> Option<ElementBox> {
+ let style = &theme.workspace.dock;
+
+ self.position
+ .is_visible()
+ .then(|| self.position.anchor())
+ .filter(|current_anchor| *current_anchor == anchor)
+ .map(|anchor| match anchor {
+ DockAnchor::Bottom | DockAnchor::Right => {
+ let mut panel_style = style.panel.clone();
+ let (resize_side, initial_size) = if anchor == DockAnchor::Bottom {
+ panel_style.margin = Margin {
+ top: panel_style.margin.top,
+ ..Default::default()
+ };
+
+ (Side::Top, style.initial_size_bottom)
+ } else {
+ panel_style.margin = Margin {
+ left: panel_style.margin.left,
+ ..Default::default()
+ };
+ (Side::Left, style.initial_size_right)
+ };
+
+ enum DockResizeHandle {}
+
+ let resizable = Container::new(ChildView::new(self.pane.clone()).boxed())
+ .with_style(panel_style)
+ .with_resize_handle::<DockResizeHandle, _>(
+ resize_side as usize,
+ resize_side,
+ 4.,
+ self.panel_sizes
+ .get(&anchor)
+ .copied()
+ .unwrap_or(initial_size),
+ cx,
+ );
+
+ let size = resizable.current_size();
+ let workspace = cx.handle();
+ cx.defer(move |cx| {
+ if let Some(workspace) = workspace.upgrade(cx) {
+ workspace.update(cx, |workspace, _| {
+ workspace.dock.panel_sizes.insert(anchor, size);
+ })
+ }
+ });
+
+ resizable.flex(style.flex, false).boxed()
+ }
+ DockAnchor::Expanded => {
+ enum ExpandedDockWash {}
+ enum ExpandedDockPane {}
+ Container::new(
+ MouseEventHandler::<ExpandedDockWash>::new(0, cx, |_state, cx| {
+ MouseEventHandler::<ExpandedDockPane>::new(0, cx, |_state, _cx| {
+ ChildView::new(self.pane.clone()).boxed()
+ })
+ .capture_all()
+ .contained()
+ .with_style(style.maximized)
+ .boxed()
+ })
+ .capture_all()
+ .on_down(MouseButton::Left, |_, cx| {
+ cx.dispatch_action(ToggleDock);
+ })
+ .with_cursor_style(CursorStyle::Arrow)
+ .boxed(),
+ )
+ .with_background_color(style.wash_color)
+ .boxed()
+ }
+ })
+ }
+}
+
+pub struct ToggleDockButton {
+ workspace: WeakViewHandle<Workspace>,
+}
+
+impl ToggleDockButton {
+ pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+ // When dock moves, redraw so that the icon and toggle status matches.
+ cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
+
+ Self {
+ workspace: workspace.downgrade(),
+ }
+ }
+}
+
+impl Entity for ToggleDockButton {
+ type Event = ();
+}
+
+impl View for ToggleDockButton {
+ fn ui_name() -> &'static str {
+ "Dock Toggle"
+ }
+
+ fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+ let workspace = self.workspace.upgrade(cx);
+
+ if workspace.is_none() {
+ return Empty::new().boxed();
+ }
+
+ let dock_position = workspace.unwrap().read(cx).dock.position;
+
+ let theme = cx.global::<Settings>().theme.clone();
+ MouseEventHandler::<Self>::new(0, cx, {
+ let theme = theme.clone();
+ move |state, _| {
+ let style = theme
+ .workspace
+ .status_bar
+ .sidebar_buttons
+ .item
+ .style_for(state, dock_position.is_visible());
+
+ Svg::new(icon_for_dock_anchor(dock_position.anchor()))
+ .with_color(style.icon_color)
+ .constrained()
+ .with_width(style.icon_size)
+ .with_height(style.icon_size)
+ .contained()
+ .with_style(style.container)
+ .boxed()
+ }
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, |_, cx| {
+ cx.dispatch_action(ToggleDock);
+ })
+ .with_tooltip::<Self, _>(
+ 0,
+ "Toggle Dock".to_string(),
+ Some(Box::new(ToggleDock)),
+ theme.tooltip.clone(),
+ cx,
+ )
+ .boxed()
+ }
+}
+
+impl StatusItemView for ToggleDockButton {
+ fn set_active_pane_item(
+ &mut self,
+ _active_pane_item: Option<&dyn crate::ItemHandle>,
+ _cx: &mut ViewContext<Self>,
+ ) {
+ //Not applicable
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::ops::{Deref, DerefMut};
+
+ use gpui::{AppContext, TestAppContext, UpdateView, ViewContext};
+ use project::{FakeFs, Project};
+ use settings::Settings;
+
+ use super::*;
+ use crate::{sidebar::Sidebar, tests::TestItem, ItemHandle, Workspace};
+
+ pub fn default_item_factory(
+ _workspace: &mut Workspace,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Box<dyn ItemHandle> {
+ Box::new(cx.add_view(|_| TestItem::new()))
+ }
+
+ #[gpui::test]
+ async fn test_dock_hides_when_pane_empty(cx: &mut TestAppContext) {
+ let mut cx = DockTestContext::new(cx).await;
+
+ // Closing the last item in the dock hides the dock
+ cx.move_dock(DockAnchor::Right);
+ let old_items = cx.dock_items();
+ assert!(!old_items.is_empty());
+ cx.close_dock_items().await;
+ cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
+
+ // Reopening the dock adds a new item
+ cx.move_dock(DockAnchor::Right);
+ let new_items = cx.dock_items();
+ assert!(!new_items.is_empty());
+ assert!(new_items
+ .into_iter()
+ .all(|new_item| !old_items.contains(&new_item)));
+ }
+
+ #[gpui::test]
+ async fn test_dock_panel_collisions(cx: &mut TestAppContext) {
+ let mut cx = DockTestContext::new(cx).await;
+
+ // Dock closes when expanded for either panel
+ cx.move_dock(DockAnchor::Expanded);
+ cx.open_sidebar(SidebarSide::Left);
+ cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
+ cx.close_sidebar(SidebarSide::Left);
+ cx.move_dock(DockAnchor::Expanded);
+ cx.open_sidebar(SidebarSide::Right);
+ cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
+
+ // Dock closes in the right position if the right sidebar is opened
+ cx.move_dock(DockAnchor::Right);
+ cx.open_sidebar(SidebarSide::Left);
+ cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
+ cx.open_sidebar(SidebarSide::Right);
+ cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
+ cx.close_sidebar(SidebarSide::Right);
+
+ // Dock in bottom position ignores sidebars
+ cx.move_dock(DockAnchor::Bottom);
+ cx.open_sidebar(SidebarSide::Left);
+ cx.open_sidebar(SidebarSide::Right);
+ cx.assert_dock_position(DockPosition::Shown(DockAnchor::Bottom));
+
+ // Opening the dock in the right position closes the right sidebar
+ cx.move_dock(DockAnchor::Right);
+ cx.assert_sidebar_closed(SidebarSide::Right);
+ }
+
+ #[gpui::test]
+ async fn test_focusing_panes_shows_and_hides_dock(cx: &mut TestAppContext) {
+ let mut cx = DockTestContext::new(cx).await;
+
+ // Focusing an item not in the dock when expanded hides the dock
+ let center_item = cx.add_item_to_center_pane();
+ cx.move_dock(DockAnchor::Expanded);
+ let dock_item = cx
+ .dock_items()
+ .get(0)
+ .cloned()
+ .expect("Dock should have an item at this point");
+ center_item.update(&mut cx, |_, cx| cx.focus_self());
+ cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
+
+ // Focusing an item not in the dock when not expanded, leaves the dock open but inactive
+ cx.move_dock(DockAnchor::Right);
+ center_item.update(&mut cx, |_, cx| cx.focus_self());
+ cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
+ cx.assert_dock_pane_inactive();
+ cx.assert_workspace_pane_active();
+
+ // Focusing an item in the dock activates it's pane
+ dock_item.update(&mut cx, |_, cx| cx.focus_self());
+ cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
+ cx.assert_dock_pane_active();
+ cx.assert_workspace_pane_inactive();
+ }
+
+ #[gpui::test]
+ async fn test_toggle_dock_focus(cx: &mut TestAppContext) {
+ let cx = DockTestContext::new(cx).await;
+
+ cx.move_dock(DockAnchor::Right);
+ cx.assert_dock_pane_active();
+ cx.toggle_dock();
+ cx.move_dock(DockAnchor::Right);
+ cx.assert_dock_pane_active();
+ }
+
+ struct DockTestContext<'a> {
+ pub cx: &'a mut TestAppContext,
+ pub window_id: usize,
+ pub workspace: ViewHandle<Workspace>,
+ }
+
+ impl<'a> DockTestContext<'a> {
+ pub async fn new(cx: &'a mut TestAppContext) -> DockTestContext<'a> {
+ Settings::test_async(cx);
+ let fs = FakeFs::new(cx.background());
+
+ cx.update(|cx| init(cx));
+ let project = Project::test(fs, [], cx).await;
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
+
+ workspace.update(cx, |workspace, cx| {
+ let left_panel = cx.add_view(|_| TestItem::new());
+ workspace.left_sidebar().update(cx, |sidebar, cx| {
+ sidebar.add_item(
+ "icons/folder_tree_16.svg",
+ "Left Test Panel".to_string(),
+ left_panel.clone(),
+ cx,
+ );
+ });
+
+ let right_panel = cx.add_view(|_| TestItem::new());
+ workspace.right_sidebar().update(cx, |sidebar, cx| {
+ sidebar.add_item(
+ "icons/folder_tree_16.svg",
+ "Right Test Panel".to_string(),
+ right_panel.clone(),
+ cx,
+ );
+ });
+ });
+
+ Self {
+ cx,
+ window_id,
+ workspace,
+ }
+ }
+
+ pub fn workspace<F, T>(&self, read: F) -> T
+ where
+ F: FnOnce(&Workspace, &AppContext) -> T,
+ {
+ self.workspace.read_with(self.cx, read)
+ }
+
+ pub fn update_workspace<F, T>(&mut self, update: F) -> T
+ where
+ F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
+ {
+ self.workspace.update(self.cx, update)
+ }
+
+ pub fn sidebar<F, T>(&self, sidebar_side: SidebarSide, read: F) -> T
+ where
+ F: FnOnce(&Sidebar, &AppContext) -> T,
+ {
+ self.workspace(|workspace, cx| {
+ let sidebar = match sidebar_side {
+ SidebarSide::Left => workspace.left_sidebar(),
+ SidebarSide::Right => workspace.right_sidebar(),
+ }
+ .read(cx);
+
+ read(sidebar, cx)
+ })
+ }
+
+ pub fn center_pane_handle(&self) -> ViewHandle<Pane> {
+ self.workspace(|workspace, _| {
+ workspace
+ .last_active_center_pane
+ .clone()
+ .unwrap_or_else(|| workspace.center.panes()[0].clone())
+ })
+ }
+
+ pub fn add_item_to_center_pane(&mut self) -> ViewHandle<TestItem> {
+ self.update_workspace(|workspace, cx| {
+ let item = cx.add_view(|_| TestItem::new());
+ let pane = workspace
+ .last_active_center_pane
+ .clone()
+ .unwrap_or_else(|| workspace.center.panes()[0].clone());
+ Pane::add_item(
+ workspace,
+ &pane,
+ Box::new(item.clone()),
+ true,
+ true,
+ None,
+ cx,
+ );
+ item
+ })
+ }
+
+ pub fn dock_pane<F, T>(&self, read: F) -> T
+ where
+ F: FnOnce(&Pane, &AppContext) -> T,
+ {
+ self.workspace(|workspace, cx| {
+ let dock_pane = workspace.dock_pane().read(cx);
+ read(dock_pane, cx)
+ })
+ }
+
+ pub fn move_dock(&self, anchor: DockAnchor) {
+ self.cx.dispatch_action(self.window_id, MoveDock(anchor));
+ }
+
+ pub fn toggle_dock(&self) {
+ self.cx.dispatch_action(self.window_id, ToggleDock);
+ }
+
+ pub fn open_sidebar(&mut self, sidebar_side: SidebarSide) {
+ if !self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
+ self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
+ }
+ }
+
+ pub fn close_sidebar(&mut self, sidebar_side: SidebarSide) {
+ if self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
+ self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
+ }
+ }
+
+ pub fn dock_items(&self) -> Vec<ViewHandle<TestItem>> {
+ self.dock_pane(|pane, cx| {
+ pane.items()
+ .map(|item| {
+ item.act_as::<TestItem>(cx)
+ .expect("Dock Test Context uses TestItems in the dock")
+ })
+ .collect()
+ })
+ }
+
+ pub async fn close_dock_items(&mut self) {
+ self.update_workspace(|workspace, cx| {
+ Pane::close_items(workspace, workspace.dock_pane().clone(), cx, |_| true)
+ })
+ .await
+ .expect("Could not close dock items")
+ }
+
+ pub fn assert_dock_position(&self, expected_position: DockPosition) {
+ self.workspace(|workspace, _| assert_eq!(workspace.dock.position, expected_position));
+ }
+
+ pub fn assert_sidebar_closed(&self, sidebar_side: SidebarSide) {
+ assert!(!self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()));
+ }
+
+ pub fn assert_workspace_pane_active(&self) {
+ assert!(self
+ .center_pane_handle()
+ .read_with(self.cx, |pane, _| pane.is_active()));
+ }
+
+ pub fn assert_workspace_pane_inactive(&self) {
+ assert!(!self
+ .center_pane_handle()
+ .read_with(self.cx, |pane, _| pane.is_active()));
+ }
+
+ pub fn assert_dock_pane_active(&self) {
+ assert!(self.dock_pane(|pane, _| pane.is_active()))
+ }
+
+ pub fn assert_dock_pane_inactive(&self) {
+ assert!(!self.dock_pane(|pane, _| pane.is_active()))
+ }
+ }
+
+ impl<'a> Deref for DockTestContext<'a> {
+ type Target = gpui::TestAppContext;
+
+ fn deref(&self) -> &Self::Target {
+ self.cx
+ }
+ }
+
+ impl<'a> DerefMut for DockTestContext<'a> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.cx
+ }
+ }
+
+ impl<'a> UpdateView for DockTestContext<'a> {
+ fn update_view<T, S>(
+ &mut self,
+ handle: &ViewHandle<T>,
+ update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
+ ) -> S
+ where
+ T: View,
+ {
+ handle.update(self.cx, update)
+ }
+ }
+}
@@ -1,5 +1,9 @@
use super::{ItemHandle, SplitDirection};
-use crate::{toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace};
+use crate::{
+ dock::{icon_for_dock_anchor, MoveDock, ToggleDock},
+ toolbar::Toolbar,
+ Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace,
+};
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
use context_menu::{ContextMenu, ContextMenuItem};
@@ -15,13 +19,13 @@ use gpui::{
},
impl_actions, impl_internal_actions,
platform::{CursorStyle, NavigationDirection},
- AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
+ Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use project::{Project, ProjectEntryId, ProjectPath};
use serde::Deserialize;
-use settings::{Autosave, Settings};
+use settings::{Autosave, DockAnchor, Settings};
use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
use theme::Theme;
use util::ResultExt;
@@ -76,13 +80,27 @@ pub struct DeploySplitMenu {
position: Vector2F,
}
+#[derive(Clone, PartialEq)]
+pub struct DeployDockMenu {
+ position: Vector2F,
+}
+
#[derive(Clone, PartialEq)]
pub struct DeployNewMenu {
position: Vector2F,
}
impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
-impl_internal_actions!(pane, [CloseItem, DeploySplitMenu, DeployNewMenu, MoveItem]);
+impl_internal_actions!(
+ pane,
+ [
+ CloseItem,
+ DeploySplitMenu,
+ DeployNewMenu,
+ DeployDockMenu,
+ MoveItem
+ ]
+);
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
@@ -141,6 +159,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
cx.add_action(Pane::deploy_split_menu);
cx.add_action(Pane::deploy_new_menu);
+ cx.add_action(Pane::deploy_dock_menu);
cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
Pane::reopen_closed_item(workspace, cx).detach();
});
@@ -168,6 +187,7 @@ pub fn init(cx: &mut MutableAppContext) {
});
}
+#[derive(Debug)]
pub enum Event {
Focused,
ActivateItem { local: bool },
@@ -185,7 +205,8 @@ pub struct Pane {
autoscroll: bool,
nav_history: Rc<RefCell<NavHistory>>,
toolbar: ViewHandle<Toolbar>,
- context_menu: ViewHandle<ContextMenu>,
+ tab_bar_context_menu: ViewHandle<ContextMenu>,
+ docked: Option<DockAnchor>,
}
pub struct ItemNavHistory {
@@ -235,7 +256,7 @@ pub enum ReorderBehavior {
}
impl Pane {
- pub fn new(cx: &mut ViewContext<Self>) -> Self {
+ pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
let handle = cx.weak_handle();
let context_menu = cx.add_view(ContextMenu::new);
Self {
@@ -253,15 +274,25 @@ impl Pane {
pane: handle.clone(),
})),
toolbar: cx.add_view(|_| Toolbar::new(handle)),
- context_menu,
+ tab_bar_context_menu: context_menu,
+ docked,
}
}
+ pub fn is_active(&self) -> bool {
+ self.is_active
+ }
+
pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
self.is_active = is_active;
cx.notify();
}
+ pub fn set_docked(&mut self, docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) {
+ self.docked = docked;
+ cx.notify();
+ }
+
pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
ItemNavHistory {
history: self.nav_history.clone(),
@@ -675,7 +706,7 @@ impl Pane {
pane: ViewHandle<Pane>,
item_id_to_close: usize,
cx: &mut ViewContext<Workspace>,
- ) -> Task<Result<bool>> {
+ ) -> Task<Result<()>> {
Self::close_items(workspace, pane, cx, move |view_id| {
view_id == item_id_to_close
})
@@ -686,7 +717,7 @@ impl Pane {
pane: ViewHandle<Pane>,
cx: &mut ViewContext<Workspace>,
should_close: impl 'static + Fn(usize) -> bool,
- ) -> Task<Result<bool>> {
+ ) -> Task<Result<()>> {
let project = workspace.project().clone();
// Find the items to close.
@@ -759,7 +790,7 @@ impl Pane {
}
pane.update(&mut cx, |_, cx| cx.notify());
- Ok(true)
+ Ok(())
})
}
@@ -962,9 +993,10 @@ impl Pane {
}
fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
- self.context_menu.update(cx, |menu, cx| {
+ self.tab_bar_context_menu.update(cx, |menu, cx| {
menu.show(
action.position,
+ AnchorCorner::TopRight,
vec![
ContextMenuItem::item("Split Right", SplitRight),
ContextMenuItem::item("Split Left", SplitLeft),
@@ -976,10 +1008,26 @@ impl Pane {
});
}
+ fn deploy_dock_menu(&mut self, action: &DeployDockMenu, cx: &mut ViewContext<Self>) {
+ self.tab_bar_context_menu.update(cx, |menu, cx| {
+ menu.show(
+ action.position,
+ AnchorCorner::TopRight,
+ vec![
+ ContextMenuItem::item("Anchor Dock Right", MoveDock(DockAnchor::Right)),
+ ContextMenuItem::item("Anchor Dock Bottom", MoveDock(DockAnchor::Bottom)),
+ ContextMenuItem::item("Expand Dock", MoveDock(DockAnchor::Expanded)),
+ ],
+ cx,
+ );
+ });
+ }
+
fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
- self.context_menu.update(cx, |menu, cx| {
+ self.tab_bar_context_menu.update(cx, |menu, cx| {
menu.show(
action.position,
+ AnchorCorner::TopRight,
vec![
ContextMenuItem::item("New File", NewFile),
ContextMenuItem::item("New Terminal", NewTerminal),
@@ -1004,7 +1052,7 @@ impl Pane {
});
}
- fn render_tab_bar(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
+ fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
let theme = cx.global::<Settings>().theme.clone();
let filler_index = self.items.len();
@@ -1012,7 +1060,7 @@ impl Pane {
enum Tab {}
enum Filler {}
let pane = cx.handle();
- MouseEventHandler::new::<Tabs, _, _>(0, cx, |_, cx| {
+ MouseEventHandler::<Tabs>::new(0, cx, |_, cx| {
let autoscroll = if mem::take(&mut self.autoscroll) {
Some(self.active_item_index)
} else {
@@ -1033,7 +1081,7 @@ impl Pane {
let tab_active = ix == self.active_item_index;
row.add_child({
- MouseEventHandler::new::<Tab, _, _>(ix, cx, {
+ MouseEventHandler::<Tab>::new(ix, cx, {
let item = item.clone();
let pane = pane.clone();
let detail = detail.clone();
@@ -1108,7 +1156,7 @@ impl Pane {
// the filler
let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
row.add_child(
- MouseEventHandler::new::<Filler, _, _>(0, cx, |mouse_state, cx| {
+ MouseEventHandler::<Filler>::new(0, cx, |mouse_state, cx| {
let mut filler = Empty::new()
.contained()
.with_style(filler_style.container)
@@ -1230,17 +1278,13 @@ impl Pane {
let item_id = item.id();
enum TabCloseButton {}
let icon = Svg::new("icons/x_mark_thin_8.svg");
- MouseEventHandler::new::<TabCloseButton, _, _>(
- item_id,
- cx,
- |mouse_state, _| {
- if mouse_state.hovered {
- icon.with_color(tab_style.icon_close_active).boxed()
- } else {
- icon.with_color(tab_style.icon_close).boxed()
- }
- },
- )
+ MouseEventHandler::<TabCloseButton>::new(item_id, cx, |mouse_state, _| {
+ if mouse_state.hovered {
+ icon.with_color(tab_style.icon_close_active).boxed()
+ } else {
+ icon.with_color(tab_style.icon_close).boxed()
+ }
+ })
.with_padding(Padding::uniform(4.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, {
@@ -1316,120 +1360,121 @@ impl View for Pane {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- enum SplitIcon {}
-
let this = cx.handle();
+ enum MouseNavigationHandler {}
+
Stack::new()
.with_child(
- EventHandler::new(if let Some(active_item) = self.active_item() {
- Flex::column()
- .with_child({
- let mut tab_row = Flex::row()
- .with_child(self.render_tab_bar(cx).flex(1., true).named("tabs"));
-
- if self.is_active {
- tab_row.add_children([
- MouseEventHandler::new::<SplitIcon, _, _>(
- 0,
- cx,
- |mouse_state, cx| {
- let theme =
- &cx.global::<Settings>().theme.workspace.tab_bar;
- let style =
- theme.pane_button.style_for(mouse_state, false);
- Svg::new("icons/plus_12.svg")
- .with_color(style.color)
- .constrained()
- .with_width(style.icon_width)
- .aligned()
- .contained()
- .with_style(style.container)
- .constrained()
- .with_width(style.button_width)
- .with_height(style.button_width)
- .aligned()
- .boxed()
- },
- )
- .with_cursor_style(CursorStyle::PointingHand)
- .on_down(MouseButton::Left, |e, cx| {
- cx.dispatch_action(DeployNewMenu {
- position: e.position,
- });
- })
- .boxed(),
- MouseEventHandler::new::<SplitIcon, _, _>(
- 1,
- cx,
- |mouse_state, cx| {
- let theme =
- &cx.global::<Settings>().theme.workspace.tab_bar;
- let style =
- theme.pane_button.style_for(mouse_state, false);
- Svg::new("icons/split_12.svg")
- .with_color(style.color)
- .constrained()
- .with_width(style.icon_width)
- .aligned()
- .contained()
- .with_style(style.container)
- .constrained()
- .with_width(style.button_width)
- .with_height(style.button_width)
- .aligned()
- .boxed()
- },
+ MouseEventHandler::<MouseNavigationHandler>::new(0, cx, |_, cx| {
+ if let Some(active_item) = self.active_item() {
+ Flex::column()
+ .with_child({
+ let mut tab_row = Flex::row()
+ .with_child(self.render_tabs(cx).flex(1.0, true).named("tabs"));
+
+ // Render pane buttons
+ let theme = cx.global::<Settings>().theme.clone();
+ if self.is_active {
+ tab_row.add_child(
+ Flex::row()
+ // New menu
+ .with_child(tab_bar_button(
+ 0,
+ "icons/plus_12.svg",
+ cx,
+ |position| DeployNewMenu { position },
+ ))
+ .with_child(
+ self.docked
+ .map(|anchor| {
+ // Add the dock menu button if this pane is a dock
+ let dock_icon =
+ icon_for_dock_anchor(anchor);
+
+ tab_bar_button(
+ 1,
+ dock_icon,
+ cx,
+ |position| DeployDockMenu { position },
+ )
+ })
+ .unwrap_or_else(|| {
+ // Add the split menu if this pane is not a dock
+ tab_bar_button(
+ 2,
+ "icons/split_12.svg",
+ cx,
+ |position| DeployNewMenu { position },
+ )
+ }),
+ )
+ // Add the close dock button if this pane is a dock
+ .with_children(self.docked.map(|_| {
+ tab_bar_button(
+ 3,
+ "icons/x_mark_thin_8.svg",
+ cx,
+ |_| ToggleDock,
+ )
+ }))
+ .contained()
+ .with_style(theme.workspace.tab_bar.container)
+ .flex(1., false)
+ .boxed(),
)
- .with_cursor_style(CursorStyle::PointingHand)
- .on_down(MouseButton::Left, |e, cx| {
- cx.dispatch_action(DeploySplitMenu {
- position: e.position,
- });
- })
- .boxed(),
- ])
- }
+ }
- tab_row
- .constrained()
- .with_height(cx.global::<Settings>().theme.workspace.tab_bar.height)
- .named("tab bar")
+ tab_row
+ .constrained()
+ .with_height(theme.workspace.tab_bar.height)
+ .contained()
+ .with_style(theme.workspace.tab_bar.container)
+ .flex(1., false)
+ .named("tab bar")
+ })
+ .with_child(ChildView::new(&self.toolbar).expanded().boxed())
+ .with_child(ChildView::new(active_item).flex(1., true).boxed())
+ .boxed()
+ } else {
+ enum EmptyPane {}
+ let theme = cx.global::<Settings>().theme.clone();
+
+ MouseEventHandler::<EmptyPane>::new(0, cx, |_, _| {
+ Empty::new()
+ .contained()
+ .with_background_color(theme.workspace.background)
+ .boxed()
+ })
+ .on_down(MouseButton::Left, |_, cx| {
+ cx.focus_parent_view();
+ })
+ .on_up(MouseButton::Left, {
+ let pane = this.clone();
+ move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, 0, cx)
})
- .with_child(ChildView::new(&self.toolbar).boxed())
- .with_child(ChildView::new(active_item).flex(1., true).boxed())
.boxed()
- } else {
- enum EmptyPane {}
- let theme = cx.global::<Settings>().theme.clone();
-
- MouseEventHandler::new::<EmptyPane, _, _>(0, cx, |_, _| {
- Empty::new()
- .contained()
- .with_background_color(theme.workspace.background)
- .boxed()
- })
- .on_down(MouseButton::Left, |_, cx| {
- cx.focus_parent_view();
- })
- .boxed()
+ }
})
- .on_navigate_mouse_down(move |direction, cx| {
+ .on_down(MouseButton::Navigate(NavigationDirection::Back), {
let this = this.clone();
- match direction {
- NavigationDirection::Back => {
- cx.dispatch_action(GoBack { pane: Some(this) })
- }
- NavigationDirection::Forward => {
- cx.dispatch_action(GoForward { pane: Some(this) })
- }
+ move |_, cx| {
+ cx.dispatch_action(GoBack {
+ pane: Some(this.clone()),
+ });
+ }
+ })
+ .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
+ let this = this.clone();
+ move |_, cx| {
+ cx.dispatch_action(GoForward {
+ pane: Some(this.clone()),
+ })
}
-
- true
})
.boxed(),
)
- .with_child(ChildView::new(&self.context_menu).boxed())
+ .with_child(ChildView::new(&self.tab_bar_context_menu).boxed())
.named("pane")
}
@@ -1451,6 +1496,36 @@ impl View for Pane {
}
}
+fn tab_bar_button<A: Action>(
+ index: usize,
+ icon: &'static str,
+ cx: &mut RenderContext<Pane>,
+ action_builder: impl 'static + Fn(Vector2F) -> A,
+) -> ElementBox {
+ enum TabBarButton {}
+
+ MouseEventHandler::<TabBarButton>::new(index, cx, |mouse_state, cx| {
+ let theme = &cx.global::<Settings>().theme.workspace.tab_bar;
+ let style = theme.pane_button.style_for(mouse_state, false);
+ Svg::new(icon)
+ .with_color(style.color)
+ .constrained()
+ .with_width(style.icon_width)
+ .aligned()
+ .constrained()
+ .with_width(style.button_width)
+ .with_height(style.button_width)
+ // .aligned()
+ .boxed()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |e, cx| {
+ cx.dispatch_action(action_builder(e.region.lower_right()));
+ })
+ .flex(1., false)
+ .boxed()
+}
+
impl ItemNavHistory {
pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
self.history.borrow_mut().push(data, self.item.clone(), cx);
@@ -1566,7 +1641,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// 1. Add with a destination index
@@ -1654,7 +1730,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// 1. Add with a destination index
@@ -1730,7 +1807,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// singleton view
@@ -38,6 +38,10 @@ impl PaneGroup {
}
}
+ /// Returns:
+ /// - Ok(true) if it found and removed a pane
+ /// - Ok(false) if it found but did not remove the pane
+ /// - Err(_) if it did not find the pane
pub fn remove(&mut self, pane: &ViewHandle<Pane>) -> Result<bool> {
match &mut self.root {
Member::Pane(_) => Ok(false),
@@ -1,77 +0,0 @@
-// TODO: Need to put this basic structure in workspace, and make 'program handles'
-// based off of the 'searchable item' pattern except with models. This way, the workspace's clients
-// can register their models as programs with a specific identity and capable of notifying the workspace
-// Programs are:
-// - Kept alive by the program manager, they need to emit an event to get dropped from it
-// - Can be interacted with directly, (closed, activated, etc.) by the program manager, bypassing
-// associated view(s)
-// - Have special rendering methods that the program manager requires them to implement to fill out
-// the status bar
-// - Can emit events for the program manager which:
-// - Add a jewel (notification, change, etc.)
-// - Drop the program
-// - ???
-// - Program Manager is kept in a global, listens for window drop so it can drop all it's program handles
-
-use collections::HashMap;
-use gpui::{AnyModelHandle, Entity, ModelHandle, View, ViewContext};
-
-/// This struct is going to be the starting point for the 'program manager' feature that will
-/// eventually be implemented to provide a collaborative way of engaging with identity-having
-/// features like the terminal.
-pub struct ProgramManager {
- // TODO: Make this a hashset or something
- modals: HashMap<usize, AnyModelHandle>,
-}
-
-impl ProgramManager {
- pub fn insert_or_replace<T: Entity, V: View>(
- window: usize,
- program: ModelHandle<T>,
- cx: &mut ViewContext<V>,
- ) -> Option<AnyModelHandle> {
- cx.update_global::<ProgramManager, _, _>(|pm, _| {
- pm.insert_or_replace_internal::<T>(window, program)
- })
- }
-
- pub fn remove<T: Entity, V: View>(
- window: usize,
- cx: &mut ViewContext<V>,
- ) -> Option<ModelHandle<T>> {
- cx.update_global::<ProgramManager, _, _>(|pm, _| pm.remove_internal::<T>(window))
- }
-
- pub fn new() -> Self {
- Self {
- modals: Default::default(),
- }
- }
-
- /// Inserts or replaces the model at the given location.
- fn insert_or_replace_internal<T: Entity>(
- &mut self,
- window: usize,
- program: ModelHandle<T>,
- ) -> Option<AnyModelHandle> {
- self.modals.insert(window, AnyModelHandle::from(program))
- }
-
- /// Remove the program associated with this window, if it's of the given type
- fn remove_internal<T: Entity>(&mut self, window: usize) -> Option<ModelHandle<T>> {
- let program = self.modals.remove(&window);
- if let Some(program) = program {
- if program.is::<T>() {
- // Guaranteed to be some, but leave it in the option
- // anyway for the API
- program.downcast()
- } else {
- // Model is of the incorrect type, put it back
- self.modals.insert(window, program);
- None
- }
- } else {
- None
- }
- }
-}
@@ -5,14 +5,15 @@ use gpui::{
};
use serde::Deserialize;
use settings::Settings;
-use std::{cell::RefCell, rc::Rc};
-use theme::Theme;
+use std::rc::Rc;
pub trait SidebarItem: View {
fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
false
}
- fn should_show_badge(&self, cx: &AppContext) -> bool;
+ fn should_show_badge(&self, _: &AppContext) -> bool {
+ false
+ }
fn contains_focused_view(&self, _: &AppContext) -> bool {
false
}
@@ -53,20 +54,27 @@ impl From<&dyn SidebarItemHandle> for AnyViewHandle {
}
pub struct Sidebar {
- side: Side,
+ sidebar_side: SidebarSide,
items: Vec<Item>,
is_open: bool,
active_item_ix: usize,
- actual_width: Rc<RefCell<f32>>,
- custom_width: Rc<RefCell<f32>>,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
-pub enum Side {
+pub enum SidebarSide {
Left,
Right,
}
+impl SidebarSide {
+ fn to_resizable_side(self) -> Side {
+ match self {
+ Self::Left => Side::Right,
+ Self::Right => Side::Left,
+ }
+ }
+}
+
struct Item {
icon_path: &'static str,
tooltip: String,
@@ -80,21 +88,19 @@ pub struct SidebarButtons {
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct ToggleSidebarItem {
- pub side: Side,
+ pub sidebar_side: SidebarSide,
pub item_index: usize,
}
impl_actions!(workspace, [ToggleSidebarItem]);
impl Sidebar {
- pub fn new(side: Side) -> Self {
+ pub fn new(sidebar_side: SidebarSide) -> Self {
Self {
- side,
+ sidebar_side,
items: Default::default(),
active_item_ix: 0,
is_open: false,
- actual_width: Rc::new(RefCell::new(260.)),
- custom_width: Rc::new(RefCell::new(260.)),
}
}
@@ -171,38 +177,6 @@ impl Sidebar {
None
}
}
-
- fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
- let actual_width = self.actual_width.clone();
- let custom_width = self.custom_width.clone();
- let side = self.side;
- MouseEventHandler::new::<Self, _, _>(side as usize, cx, |_, _| {
- Empty::new()
- .contained()
- .with_style(theme.workspace.sidebar_resize_handle)
- .boxed()
- })
- .with_padding(Padding {
- left: 4.,
- right: 4.,
- ..Default::default()
- })
- .with_cursor_style(CursorStyle::ResizeLeftRight)
- .on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
- .on_drag(MouseButton::Left, move |e, cx| {
- let delta = e.position.x() - e.prev_mouse_position.x();
- let prev_width = *actual_width.borrow();
- *custom_width.borrow_mut() = 0f32
- .max(match side {
- Side::Left => prev_width + delta,
- Side::Right => prev_width - delta,
- })
- .round();
-
- cx.notify();
- })
- .boxed()
- }
}
impl Entity for Sidebar {
@@ -215,31 +189,20 @@ impl View for Sidebar {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let theme = cx.global::<Settings>().theme.clone();
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(&theme, cx));
- }
-
- container.add_child(
- Hook::new(
- ChildView::new(active_item.to_any())
- .constrained()
- .with_max_width(*self.custom_width.borrow())
- .boxed(),
+ enum ResizeHandleTag {}
+ let style = &cx.global::<Settings>().theme.workspace.sidebar;
+ ChildView::new(active_item.to_any())
+ .contained()
+ .with_style(style.container)
+ .with_resize_handle::<ResizeHandleTag, _>(
+ self.sidebar_side as usize,
+ self.sidebar_side.to_resizable_side(),
+ 4.,
+ style.initial_size,
+ cx,
)
- .on_after_layout({
- let actual_width = self.actual_width.clone();
- move |size, _| *actual_width.borrow_mut() = size.x()
- })
- .flex(1., false)
- .boxed(),
- );
- if matches!(self.side, Side::Left) {
- container.add_child(self.render_resize_handle(&theme, cx));
- }
- container.boxed()
+ .boxed()
} else {
Empty::new().boxed()
}
@@ -271,10 +234,10 @@ impl View for SidebarButtons {
let badge_style = theme.badge;
let active_ix = sidebar.active_item_ix;
let is_open = sidebar.is_open;
- let side = sidebar.side;
- let group_style = match side {
- Side::Left => theme.group_left,
- Side::Right => theme.group_right,
+ let sidebar_side = sidebar.sidebar_side;
+ let group_style = match sidebar_side {
+ SidebarSide::Left => theme.group_left,
+ SidebarSide::Right => theme.group_right,
};
#[allow(clippy::needless_collect)]
@@ -288,10 +251,10 @@ impl View for SidebarButtons {
.with_children(items.into_iter().enumerate().map(
|(ix, (icon_path, tooltip, item_view))| {
let action = ToggleSidebarItem {
- side,
+ sidebar_side,
item_index: ix,
};
- MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, cx| {
+ MouseEventHandler::<Self>::new(ix, cx, move |state, cx| {
let is_active = is_open && ix == active_ix;
let style = item_style.style_for(state, is_active);
Stack::new()
@@ -166,7 +166,7 @@ fn nav_button<A: Action + Clone>(
action_name: &str,
cx: &mut RenderContext<Toolbar>,
) -> ElementBox {
- MouseEventHandler::new::<A, _, _>(0, cx, |state, _| {
+ MouseEventHandler::<A>::new(0, cx, |state, _| {
let style = if enabled {
style.style_for(state, false)
} else {
@@ -1,4 +1,4 @@
-use crate::{sidebar::Side, AppState, ToggleFollow, Workspace};
+use crate::{sidebar::SidebarSide, AppState, ToggleFollow, Workspace};
use anyhow::Result;
use client::{proto, Client, Contact};
use gpui::{
@@ -74,82 +74,84 @@ impl WaitingRoom {
) -> Self {
let project_id = contact.projects[project_index].id;
let client = app_state.client.clone();
- let _join_task =
- cx.spawn_weak({
- let contact = contact.clone();
- |this, mut cx| async move {
- let project = Project::remote(
- project_id,
- app_state.client.clone(),
- app_state.user_store.clone(),
- app_state.project_store.clone(),
- app_state.languages.clone(),
- app_state.fs.clone(),
- cx.clone(),
- )
- .await;
+ let _join_task = cx.spawn_weak({
+ let contact = contact.clone();
+ |this, mut cx| async move {
+ let project = Project::remote(
+ project_id,
+ app_state.client.clone(),
+ app_state.user_store.clone(),
+ app_state.project_store.clone(),
+ app_state.languages.clone(),
+ app_state.fs.clone(),
+ cx.clone(),
+ )
+ .await;
- if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, cx| {
- this.waiting = false;
- match project {
- Ok(project) => {
- cx.replace_root_view(|cx| {
- let mut workspace = Workspace::new(project, cx);
- (app_state.initialize_workspace)(
- &mut workspace,
- &app_state,
- cx,
- );
- workspace.toggle_sidebar(Side::Left, cx);
- if let Some((host_peer_id, _)) =
- workspace.project.read(cx).collaborators().iter().find(
- |(_, collaborator)| collaborator.replica_id == 0,
- )
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| {
+ this.waiting = false;
+ match project {
+ Ok(project) => {
+ cx.replace_root_view(|cx| {
+ let mut workspace =
+ Workspace::new(project, app_state.default_item_factory, cx);
+ (app_state.initialize_workspace)(
+ &mut workspace,
+ &app_state,
+ cx,
+ );
+ workspace.toggle_sidebar(SidebarSide::Left, cx);
+ if let Some((host_peer_id, _)) = workspace
+ .project
+ .read(cx)
+ .collaborators()
+ .iter()
+ .find(|(_, collaborator)| collaborator.replica_id == 0)
+ {
+ if let Some(follow) = workspace
+ .toggle_follow(&ToggleFollow(*host_peer_id), cx)
{
- if let Some(follow) = workspace
- .toggle_follow(&ToggleFollow(*host_peer_id), cx)
- {
- follow.detach_and_log_err(cx);
- }
- }
- workspace
- });
- }
- Err(error) => {
- let login = &contact.user.github_login;
- let message = match error {
- project::JoinProjectError::HostDeclined => {
- format!("@{} declined your request.", login)
+ follow.detach_and_log_err(cx);
}
- project::JoinProjectError::HostClosedProject => {
- format!(
- "@{} closed their copy of {}.",
- login,
- humanize_list(
- &contact.projects[project_index]
- .visible_worktree_root_names
- )
+ }
+ workspace
+ });
+ }
+ Err(error) => {
+ let login = &contact.user.github_login;
+ let message = match error {
+ project::JoinProjectError::HostDeclined => {
+ format!("@{} declined your request.", login)
+ }
+ project::JoinProjectError::HostClosedProject => {
+ format!(
+ "@{} closed their copy of {}.",
+ login,
+ humanize_list(
+ &contact.projects[project_index]
+ .visible_worktree_root_names
)
- }
- project::JoinProjectError::HostWentOffline => {
- format!("@{} went offline.", login)
- }
- project::JoinProjectError::Other(error) => {
- log::error!("error joining project: {}", error);
- "An error occurred.".to_string()
- }
- };
- this.message = message;
- cx.notify();
- }
+ )
+ }
+ project::JoinProjectError::HostWentOffline => {
+ format!("@{} went offline.", login)
+ }
+ project::JoinProjectError::Other(error) => {
+ log::error!("error joining project: {}", error);
+ "An error occurred.".to_string()
+ }
+ };
+ this.message = message;
+ cx.notify();
}
- })
- }
-
- Ok(())
+ }
+ })
}
- });
+
+ Ok(())
+ }
+ });
Self {
project_id,
@@ -3,9 +3,9 @@
///
/// This may cause issues when you're trying to write tests that use workspace focus to add items at
/// specific locations.
+pub mod dock;
pub mod pane;
pub mod pane_group;
-pub mod programs;
pub mod searchable;
pub mod sidebar;
mod status_bar;
@@ -18,6 +18,7 @@ use client::{
};
use clock::ReplicaId;
use collections::{hash_map, HashMap, HashSet};
+use dock::{DefaultItemFactory, Dock, ToggleDockButton};
use drag_and_drop::DragAndDrop;
use futures::{channel::oneshot, FutureExt};
use gpui::{
@@ -37,12 +38,11 @@ use log::error;
pub use pane::*;
pub use pane_group::*;
use postage::prelude::Stream;
-use programs::ProgramManager;
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
use searchable::SearchableItemHandle;
use serde::Deserialize;
-use settings::{Autosave, Settings};
-use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
+use settings::{Autosave, DockAnchor, Settings};
+use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
use smallvec::SmallVec;
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
@@ -146,10 +146,8 @@ impl_internal_actions!(
impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
- // Initialize the program manager immediately
- cx.set_global(ProgramManager::new());
-
pane::init(cx);
+ dock::init(cx);
cx.add_global_action(open);
cx.add_global_action({
@@ -217,10 +215,10 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
workspace.activate_next_pane(cx)
});
cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
- workspace.toggle_sidebar(Side::Left, cx);
+ workspace.toggle_sidebar(SidebarSide::Left, cx);
});
cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| {
- workspace.toggle_sidebar(Side::Right, cx);
+ workspace.toggle_sidebar(SidebarSide::Right, cx);
});
cx.add_action(Workspace::activate_pane_at_index);
@@ -265,6 +263,7 @@ pub struct AppState {
pub fs: Arc<dyn fs::Fs>,
pub build_window_options: fn() -> WindowOptions<'static>,
pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
+ pub default_item_factory: DefaultItemFactory,
}
#[derive(Eq, PartialEq, Hash)]
@@ -870,11 +869,13 @@ impl AppState {
project_store,
initialize_workspace: |_, _, _| {},
build_window_options: Default::default,
+ default_item_factory: |_, _| unimplemented!(),
})
}
}
pub enum Event {
+ DockAnchorChanged,
PaneAdded(ViewHandle<Pane>),
ContactRequestedJoin(u64),
}
@@ -892,7 +893,9 @@ pub struct Workspace {
panes: Vec<ViewHandle<Pane>>,
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
active_pane: ViewHandle<Pane>,
+ last_active_center_pane: Option<ViewHandle<Pane>>,
status_bar: ViewHandle<StatusBar>,
+ dock: Dock,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>,
leader_state: LeaderState,
@@ -922,7 +925,11 @@ enum FollowerItem {
}
impl Workspace {
- pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+ pub fn new(
+ project: ModelHandle<Project>,
+ dock_default_factory: DefaultItemFactory,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
cx.observe_window_activation(Self::on_window_activation_changed)
@@ -949,14 +956,14 @@ impl Workspace {
})
.detach();
- let pane = cx.add_view(Pane::new);
- let pane_id = pane.id();
- cx.subscribe(&pane, move |this, _, event, cx| {
+ let center_pane = cx.add_view(|cx| Pane::new(None, cx));
+ let pane_id = center_pane.id();
+ cx.subscribe(¢er_pane, move |this, _, event, cx| {
this.handle_pane_event(pane_id, event, cx)
})
.detach();
- cx.focus(&pane);
- cx.emit(Event::PaneAdded(pane.clone()));
+ cx.focus(¢er_pane);
+ cx.emit(Event::PaneAdded(center_pane.clone()));
let fs = project.read(cx).fs().clone();
let user_store = project.read(cx).user_store();
@@ -978,33 +985,44 @@ impl Workspace {
}
});
- let weak_self = cx.weak_handle();
+ let handle = cx.handle();
+ let weak_handle = cx.weak_handle();
- cx.emit_global(WorkspaceCreated(weak_self.clone()));
+ cx.emit_global(WorkspaceCreated(weak_handle.clone()));
- let left_sidebar = cx.add_view(|_| Sidebar::new(Side::Left));
- let right_sidebar = cx.add_view(|_| Sidebar::new(Side::Right));
+ let dock = Dock::new(cx, dock_default_factory);
+ let dock_pane = dock.pane().clone();
+
+ let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
+ let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
+ let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx));
let right_sidebar_buttons =
cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
let status_bar = cx.add_view(|cx| {
- let mut status_bar = StatusBar::new(&pane.clone(), cx);
+ let mut status_bar = StatusBar::new(¢er_pane.clone(), cx);
status_bar.add_left_item(left_sidebar_buttons, cx);
status_bar.add_right_item(right_sidebar_buttons, cx);
+ status_bar.add_right_item(toggle_dock, cx);
status_bar
});
cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
- drag_and_drop.register_container(weak_self.clone());
+ drag_and_drop.register_container(weak_handle.clone());
});
let mut this = Workspace {
modal: None,
- weak_self,
- center: PaneGroup::new(pane.clone()),
- panes: vec![pane.clone()],
+ weak_self: weak_handle,
+ center: PaneGroup::new(center_pane.clone()),
+ dock,
+ // When removing an item, the last element remaining in this array
+ // is used to find where focus should fallback to. As such, the order
+ // of these two variables is important.
+ panes: vec![dock_pane, center_pane.clone()],
panes_by_item: Default::default(),
- active_pane: pane.clone(),
+ active_pane: center_pane.clone(),
+ last_active_center_pane: Some(center_pane.clone()),
status_bar,
notifications: Default::default(),
client,
@@ -1078,6 +1096,7 @@ impl Workspace {
app_state.fs.clone(),
cx,
),
+ app_state.default_item_factory,
cx,
);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
@@ -1459,24 +1478,31 @@ impl Workspace {
}
}
- pub fn toggle_sidebar(&mut self, side: Side, cx: &mut ViewContext<Self>) {
- let sidebar = match side {
- Side::Left => &mut self.left_sidebar,
- Side::Right => &mut self.right_sidebar,
+ pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
+ let sidebar = match sidebar_side {
+ SidebarSide::Left => &mut self.left_sidebar,
+ SidebarSide::Right => &mut self.right_sidebar,
};
- sidebar.update(cx, |sidebar, cx| {
- sidebar.set_open(!sidebar.is_open(), cx);
+ let open = sidebar.update(cx, |sidebar, cx| {
+ let open = !sidebar.is_open();
+ sidebar.set_open(open, cx);
+ open
});
+
+ if open {
+ Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
+ }
+
cx.focus_self();
cx.notify();
}
pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
- let sidebar = match action.side {
- Side::Left => &mut self.left_sidebar,
- Side::Right => &mut self.right_sidebar,
+ let sidebar = match action.sidebar_side {
+ SidebarSide::Left => &mut self.left_sidebar,
+ SidebarSide::Right => &mut self.right_sidebar,
};
- let active_item = sidebar.update(cx, |sidebar, cx| {
+ let active_item = sidebar.update(cx, move |sidebar, cx| {
if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
sidebar.set_open(false, cx);
None
@@ -1486,7 +1512,10 @@ impl Workspace {
sidebar.active_item().cloned()
}
});
+
if let Some(active_item) = active_item {
+ Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
+
if active_item.is_focused(cx) {
cx.focus_self();
} else {
@@ -1500,13 +1529,13 @@ impl Workspace {
pub fn toggle_sidebar_item_focus(
&mut self,
- side: Side,
+ sidebar_side: SidebarSide,
item_index: usize,
cx: &mut ViewContext<Self>,
) {
- let sidebar = match side {
- Side::Left => &mut self.left_sidebar,
- Side::Right => &mut self.right_sidebar,
+ let sidebar = match sidebar_side {
+ SidebarSide::Left => &mut self.left_sidebar,
+ SidebarSide::Right => &mut self.right_sidebar,
};
let active_item = sidebar.update(cx, |sidebar, cx| {
sidebar.set_open(true, cx);
@@ -1514,6 +1543,8 @@ impl Workspace {
sidebar.active_item().cloned()
});
if let Some(active_item) = active_item {
+ Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
+
if active_item.is_focused(cx) {
cx.focus_self();
} else {
@@ -1529,7 +1560,7 @@ impl Workspace {
}
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
- let pane = cx.add_view(Pane::new);
+ let pane = cx.add_view(|cx| Pane::new(None, cx));
let pane_id = pane.id();
cx.subscribe(&pane, move |this, _, event, cx| {
this.handle_pane_event(pane_id, event, cx)
@@ -1682,6 +1713,15 @@ impl Workspace {
status_bar.set_active_pane(&self.active_pane, cx);
});
self.active_item_path_changed(cx);
+
+ if &pane == self.dock_pane() {
+ Dock::show(self, cx);
+ } else {
+ self.last_active_center_pane = Some(pane.clone());
+ if self.dock.is_anchored_at(DockAnchor::Expanded) {
+ Dock::hide(self, cx);
+ }
+ }
cx.notify();
}
@@ -1701,21 +1741,19 @@ impl Workspace {
cx: &mut ViewContext<Self>,
) {
if let Some(pane) = self.pane(pane_id) {
+ let is_dock = &pane == self.dock.pane();
match event {
- pane::Event::Split(direction) => {
+ pane::Event::Split(direction) if !is_dock => {
self.split_pane(pane, *direction, cx);
}
- pane::Event::Remove => {
- self.remove_pane(pane, cx);
- }
- pane::Event::Focused => {
- self.handle_pane_focused(pane, cx);
- }
+ pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
+ pane::Event::Remove if is_dock => Dock::hide(self, cx),
+ pane::Event::Focused => self.handle_pane_focused(pane, cx),
pane::Event::ActivateItem { local } => {
if *local {
self.unfollow(&pane, cx);
}
- if pane == self.active_pane {
+ if &pane == self.active_pane() {
self.active_item_path_changed(cx);
}
}
@@ -1733,8 +1771,9 @@ impl Workspace {
}
}
}
+ _ => {}
}
- } else {
+ } else if self.dock.visible_pane().is_none() {
error!("pane {} not found", pane_id);
}
}
@@ -1765,6 +1804,10 @@ impl Workspace {
for removed_item in pane.read(cx).items() {
self.panes_by_item.remove(&removed_item.id());
}
+ if self.last_active_center_pane == Some(pane) {
+ self.last_active_center_pane = None;
+ }
+
cx.notify();
} else {
self.active_item_path_changed(cx);
@@ -1783,6 +1826,10 @@ impl Workspace {
&self.active_pane
}
+ pub fn dock_pane(&self) -> &ViewHandle<Pane> {
+ self.dock.pane()
+ }
+
fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
if let Some(remote_id) = remote_id {
self.remote_entity_subscription =
@@ -1975,8 +2022,9 @@ impl Workspace {
theme.workspace.titlebar.container
};
+ enum TitleBar {}
ConstrainedBox::new(
- MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
+ MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
Container::new(
Stack::new()
.with_child(
@@ -2105,7 +2153,7 @@ impl Workspace {
None
} else {
Some(
- MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
+ MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
let style = theme
.workspace
.titlebar
@@ -2165,7 +2213,7 @@ impl Workspace {
.boxed();
if let Some((peer_id, peer_github_login)) = peer {
- MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
+ MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleFollow(peer_id))
@@ -2191,7 +2239,7 @@ impl Workspace {
if self.project.read(cx).is_read_only() {
enum DisconnectedOverlay {}
Some(
- MouseEventHandler::new::<DisconnectedOverlay, _, _>(0, cx, |_, cx| {
+ MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme;
Label::new(
"Your connection to the remote project has been lost.".to_string(),
@@ -2202,6 +2250,7 @@ impl Workspace {
.with_style(theme.workspace.disconnected_overlay.container)
.boxed()
})
+ .with_cursor_style(CursorStyle::Arrow)
.capture_all()
.boxed(),
)
@@ -2557,14 +2606,28 @@ impl View for Workspace {
},
)
.with_child(
- FlexItem::new(self.center.render(
- &theme,
- &self.follower_states_by_leader,
- self.project.read(cx).collaborators(),
- ))
+ FlexItem::new(
+ Flex::column()
+ .with_child(
+ FlexItem::new(self.center.render(
+ &theme,
+ &self.follower_states_by_leader,
+ self.project.read(cx).collaborators(),
+ ))
+ .flex(1., true)
+ .boxed(),
+ )
+ .with_children(self.dock.render(
+ &theme,
+ DockAnchor::Bottom,
+ cx,
+ ))
+ .boxed(),
+ )
.flex(1., true)
.boxed(),
)
+ .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
.with_children(
if self.right_sidebar.read(cx).active_item().is_some() {
Some(
@@ -2578,15 +2641,27 @@ impl View for Workspace {
)
.boxed()
})
- .with_children(self.modal.as_ref().map(|m| {
- ChildView::new(m)
- .contained()
- .with_style(theme.workspace.modal)
- .aligned()
- .top()
- .boxed()
- }))
- .with_children(self.render_notifications(&theme.workspace))
+ .with_child(
+ Overlay::new(
+ Stack::new()
+ .with_children(self.dock.render(
+ &theme,
+ DockAnchor::Expanded,
+ cx,
+ ))
+ .with_children(self.modal.as_ref().map(|m| {
+ ChildView::new(m)
+ .contained()
+ .with_style(theme.workspace.modal)
+ .aligned()
+ .top()
+ .boxed()
+ }))
+ .with_children(self.render_notifications(&theme.workspace))
+ .boxed(),
+ )
+ .boxed(),
+ )
.flex(1.0, true)
.boxed(),
)
@@ -2785,10 +2860,10 @@ pub fn open_paths(
cx,
);
new_project = Some(project.clone());
- let mut workspace = Workspace::new(project, cx);
+ let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
if contains_directory {
- workspace.toggle_sidebar(Side::Left, cx);
+ workspace.toggle_sidebar(SidebarSide::Left, cx);
}
workspace
})
@@ -2846,6 +2921,7 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
app_state.fs.clone(),
cx,
),
+ app_state.default_item_factory,
cx,
);
(app_state.initialize_workspace)(&mut workspace, app_state, cx);
@@ -2858,11 +2934,20 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
mod tests {
use std::cell::Cell;
+ use crate::sidebar::SidebarItem;
+
use super::*;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
use project::{FakeFs, Project, ProjectEntryId};
use serde_json::json;
+ pub fn default_item_factory(
+ _workspace: &mut Workspace,
+ _cx: &mut ViewContext<Workspace>,
+ ) -> Box<dyn ItemHandle> {
+ unimplemented!();
+ }
+
#[gpui::test]
async fn test_tab_disambiguation(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
@@ -2870,7 +2955,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
// Adding an item with no ambiguity renders the tab without detail.
let item1 = cx.add_view(&workspace, |_| {
@@ -2934,7 +3020,8 @@ mod tests {
.await;
let project = Project::test(fs, ["root1".as_ref()], cx).await;
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});
@@ -3030,7 +3117,8 @@ mod tests {
fs.insert_tree("/root", json!({ "one": "" })).await;
let project = Project::test(fs, ["root".as_ref()], cx).await;
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
// When there are no dirty items, there's nothing to do.
let item1 = cx.add_view(&workspace, |_| TestItem::new());
@@ -3070,7 +3158,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, None, cx).await;
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
let item1 = cx.add_view(&workspace, |_| {
let mut item = TestItem::new();
@@ -3165,7 +3254,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
// Create several workspace items with single project entries, and two
// workspace items with multiple project entries.
@@ -3266,7 +3356,8 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
let item = cx.add_view(&workspace, |_| {
let mut item = TestItem::new();
@@ -3383,7 +3474,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
let item = cx.add_view(&workspace, |_| {
let mut item = TestItem::new();
@@ -3635,4 +3726,6 @@ mod tests {
vec![ItemEvent::UpdateTab, ItemEvent::Edit]
}
}
+
+ impl SidebarItem for TestItem {}
}
@@ -21,7 +21,7 @@ impl View for FeedbackLink {
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> gpui::ElementBox {
- MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
+ MouseEventHandler::<Self>::new(0, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme;
let theme = &theme.workspace.status_bar.feedback;
Text::new(
@@ -20,20 +20,21 @@ use futures::{
channel::{mpsc, oneshot},
FutureExt, SinkExt, StreamExt,
};
-use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task};
+use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext};
use isahc::{config::Configurable, AsyncBody, Request};
use language::LanguageRegistry;
use log::LevelFilter;
use parking_lot::Mutex;
use project::{Fs, ProjectStore};
use serde_json::json;
-use settings::{self, KeymapFileContent, Settings, SettingsFileContent};
+use settings::{self, KeymapFileContent, Settings, SettingsFileContent, WorkingDirectory};
use smol::process::Command;
use std::{env, ffi::OsStr, fs, panic, path::PathBuf, sync::Arc, thread, time::Duration};
+use terminal::terminal_container_view::{get_working_directory, TerminalContainer};
use theme::ThemeRegistry;
use util::{ResultExt, TryFutureExt};
-use workspace::{self, AppState, NewFile, OpenPaths};
+use workspace::{self, AppState, ItemHandle, NewFile, OpenPaths, Workspace};
use zed::{
self, build_window_options,
fs::RealFs,
@@ -152,6 +153,7 @@ fn main() {
fs,
build_window_options,
initialize_workspace,
+ default_item_factory,
});
auto_update::init(db, http, client::ZED_SERVER_URL.clone(), cx);
workspace::init(app_state.clone(), cx);
@@ -595,3 +597,20 @@ async fn handle_cli_connection(
}
}
}
+
+pub fn default_item_factory(
+ workspace: &mut Workspace,
+ cx: &mut ViewContext<Workspace>,
+) -> Box<dyn ItemHandle> {
+ let strategy = cx
+ .global::<Settings>()
+ .terminal_overrides
+ .working_directory
+ .clone()
+ .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
+
+ let working_directory = get_working_directory(workspace, cx, strategy);
+
+ let terminal_handle = cx.add_view(|cx| TerminalContainer::new(working_directory, false, cx));
+ Box::new(terminal_handle)
+}
@@ -33,7 +33,7 @@ use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
use std::{env, path::Path, str, sync::Arc};
use util::ResultExt;
pub use workspace;
-use workspace::{sidebar::Side, AppState, Workspace};
+use workspace::{sidebar::SidebarSide, AppState, Workspace};
#[derive(Deserialize, Clone, PartialEq)]
struct OpenBrowser {
@@ -204,14 +204,14 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|workspace: &mut Workspace,
_: &project_panel::ToggleFocus,
cx: &mut ViewContext<Workspace>| {
- workspace.toggle_sidebar_item_focus(Side::Left, 0, cx);
+ workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
},
);
cx.add_action(
|workspace: &mut Workspace,
_: &contacts_panel::ToggleFocus,
cx: &mut ViewContext<Workspace>| {
- workspace.toggle_sidebar_item_focus(Side::Right, 0, cx);
+ workspace.toggle_sidebar_item_focus(SidebarSide::Right, 0, cx);
},
);
@@ -243,6 +243,7 @@ pub fn initialize_workspace(
.detach();
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
+ cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
let settings = cx.global::<Settings>();
@@ -728,7 +729,8 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let entries = cx.read(|cx| workspace.file_project_paths(cx));
let file1 = entries[0].clone();
@@ -847,7 +849,8 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
// Open a file within an existing worktree.
cx.update(|cx| {
@@ -1006,7 +1009,8 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
// Open a file within an existing worktree.
cx.update(|cx| {
@@ -1048,7 +1052,8 @@ mod tests {
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(rust_lang()));
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
// Create a new untitled buffer
@@ -1137,7 +1142,8 @@ mod tests {
let project = Project::test(app_state.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(rust_lang()));
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
// Create a new untitled buffer
cx.dispatch_action(window_id, NewFile);
@@ -1190,7 +1196,8 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+ let (window_id, workspace) =
+ cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let entries = cx.read(|cx| workspace.file_project_paths(cx));
let file1 = entries[0].clone();
@@ -1226,7 +1233,7 @@ mod tests {
cx.foreground().run_until_parked();
workspace.read_with(cx, |workspace, _| {
- assert_eq!(workspace.panes().len(), 1);
+ assert_eq!(workspace.panes().len(), 2); //Center pane + Dock pane
assert_eq!(workspace.active_pane(), &pane_1);
});
@@ -1236,6 +1243,7 @@ mod tests {
cx.foreground().run_until_parked();
workspace.read_with(cx, |workspace, cx| {
+ assert_eq!(workspace.panes().len(), 2);
assert!(workspace.active_item(cx).is_none());
});
@@ -1263,7 +1271,8 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
let entries = cx.read(|cx| workspace.file_project_paths(cx));
let file1 = entries[0].clone();
@@ -1527,7 +1536,8 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
- let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+ let (_, workspace) =
+ cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let entries = cx.read(|cx| workspace.file_project_paths(cx));
@@ -46,6 +46,11 @@ export default function search(theme: Theme) {
background: backgroundColor(theme, "on500", "active"),
border: border(theme, "muted"),
},
+ clicked: {
+ ...text(theme, "mono", "active"),
+ background: backgroundColor(theme, "on300", "active"),
+ border: border(theme, "secondary"),
+ },
hover: {
...text(theme, "mono", "active"),
background: backgroundColor(theme, "on500", "hovered"),
@@ -96,7 +96,6 @@ export default function tabBar(theme: Theme) {
buttonWidth: activePaneActiveTab.height,
hover: {
color: iconColor(theme, "active"),
- background: backgroundColor(theme, 300),
},
},
}
@@ -37,11 +37,14 @@ export default function workspace(theme: Theme) {
},
cursor: "Arrow",
},
- sidebarResizeHandle: {
- background: border(theme, "primary").color,
- padding: {
- left: 1,
- },
+ sidebar: {
+ initialSize: 240,
+ border: {
+ color: border(theme, "primary").color,
+ width: 1,
+ left: true,
+ right: true,
+ }
},
paneDivider: {
color: border(theme, "secondary").color,
@@ -156,5 +159,19 @@ export default function workspace(theme: Theme) {
width: 400,
margin: { right: 10, bottom: 10 },
},
+ dock: {
+ initialSizeRight: 640,
+ initialSizeBottom: 480,
+ wash_color: withOpacity(theme.backgroundColor[500].base, 0.5),
+ flex: 0.5,
+ panel: {
+ margin: 4,
+ },
+ maximized: {
+ margin: 32,
+ border: border(theme, "secondary"),
+ shadow: modalShadow(theme),
+ }
+ }
};
}