commit
29f46539f0
62 changed files with 2463 additions and 1396 deletions
|
@ -310,7 +310,7 @@
|
||||||
"cmd-shift-m": "diagnostics::Deploy",
|
"cmd-shift-m": "diagnostics::Deploy",
|
||||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||||
"cmd-alt-s": "workspace::SaveAll",
|
"cmd-alt-s": "workspace::SaveAll",
|
||||||
"shift-escape": "terminal::DeployModal"
|
"shift-escape": "workspace::ActivateOrHideDock"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Bindings from Sublime Text
|
// Bindings from Sublime Text
|
||||||
|
@ -426,12 +426,5 @@
|
||||||
"cmd-v": "terminal::Paste",
|
"cmd-v": "terminal::Paste",
|
||||||
"cmd-k": "terminal::Clear"
|
"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:
|
// 4. Save when idle for a certain amount of time:
|
||||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||||
"autosave": "off",
|
"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
|
// How to auto-format modified buffers when saving them. This
|
||||||
// setting can take three values:
|
// setting can take three values:
|
||||||
//
|
//
|
||||||
|
|
|
@ -278,7 +278,7 @@ impl View for ActivityIndicator {
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
let (icon, message, action) = self.content_to_render(cx);
|
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
|
let theme = &cx
|
||||||
.global::<Settings>()
|
.global::<Settings>()
|
||||||
.theme
|
.theme
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl View for UpdateNotification {
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
let theme = &theme.update_notification;
|
let theme = &theme.update_notification;
|
||||||
|
|
||||||
MouseEventHandler::new::<ViewReleaseNotes, _, _>(0, cx, |state, cx| {
|
MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
|
@ -47,7 +47,7 @@ impl View for UpdateNotification {
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::new::<Cancel, _, _>(0, cx, |state, _| {
|
MouseEventHandler::<Cancel>::new(0, cx, |state, _| {
|
||||||
let style = theme.dismiss_button.style_for(state, false);
|
let style = theme.dismiss_button.style_for(state, false);
|
||||||
Svg::new("icons/x_mark_thin_8.svg")
|
Svg::new("icons/x_mark_thin_8.svg")
|
||||||
.with_color(style.color)
|
.with_color(style.color)
|
||||||
|
|
|
@ -308,7 +308,7 @@ impl ChatPanel {
|
||||||
enum SignInPromptLabel {}
|
enum SignInPromptLabel {}
|
||||||
|
|
||||||
Align::new(
|
Align::new(
|
||||||
MouseEventHandler::new::<SignInPromptLabel, _, _>(0, cx, |mouse_state, _| {
|
MouseEventHandler::<SignInPromptLabel>::new(0, cx, |mouse_state, _| {
|
||||||
Label::new(
|
Label::new(
|
||||||
"Sign in to use chat".to_string(),
|
"Sign in to use chat".to_string(),
|
||||||
if mouse_state.hovered {
|
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;
|
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()));
|
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
|
let editor_b = workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
workspace.open_path((worktree_id, "b.txt"), true, 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.
|
// Join the project as client B.
|
||||||
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).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
|
let editor_b = workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
workspace.open_path((worktree_id, "main.rs"), true, 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_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 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
|
let editor_b = workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
workspace.open_path((worktree_id, "one.rs"), true, cx)
|
workspace.open_path((worktree_id, "one.rs"), true, cx)
|
||||||
|
@ -5224,6 +5227,7 @@ impl TestServer {
|
||||||
fs: fs.clone(),
|
fs: fs.clone(),
|
||||||
build_window_options: Default::default,
|
build_window_options: Default::default,
|
||||||
initialize_workspace: |_, _, _| unimplemented!(),
|
initialize_workspace: |_, _, _| unimplemented!(),
|
||||||
|
default_item_factory: |_, _| unimplemented!(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Channel::init(&client);
|
Channel::init(&client);
|
||||||
|
@ -5459,7 +5463,9 @@ impl TestClient {
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> ViewHandle<Workspace> {
|
) -> ViewHandle<Workspace> {
|
||||||
let (_, root_view) = cx.add_window(|_| EmptyView);
|
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(
|
async fn simulate_host(
|
||||||
|
|
|
@ -350,7 +350,8 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
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 editor = cx.add_view(&workspace, |cx| {
|
||||||
let mut editor = Editor::single_line(None, cx);
|
let mut editor = Editor::single_line(None, cx);
|
||||||
editor.set_text("abc", cx);
|
editor.set_text("abc", cx);
|
||||||
|
|
|
@ -276,7 +276,7 @@ impl ContactsPanel {
|
||||||
Section::Offline => "Offline",
|
Section::Offline => "Offline",
|
||||||
};
|
};
|
||||||
let icon_size = theme.section_icon_size;
|
let icon_size = theme.section_icon_size;
|
||||||
MouseEventHandler::new::<Header, _, _>(section as usize, cx, |_, _| {
|
MouseEventHandler::<Header>::new(section as usize, cx, |_, _| {
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
Svg::new(if is_collapsed {
|
Svg::new(if is_collapsed {
|
||||||
|
@ -375,7 +375,7 @@ impl ContactsPanel {
|
||||||
let baseline_offset =
|
let baseline_offset =
|
||||||
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
|
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 tree_branch = *tree_branch.style_for(mouse_state, is_selected);
|
||||||
let row = theme.project_row.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;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let button = MouseEventHandler::new::<ToggleProjectOnline, _, _>(
|
let button = MouseEventHandler::<ToggleProjectOnline>::new(
|
||||||
project_id as usize,
|
project_id as usize,
|
||||||
cx,
|
cx,
|
||||||
|state, _| {
|
|state, _| {
|
||||||
|
@ -529,7 +529,7 @@ impl ContactsPanel {
|
||||||
enum ToggleOnline {}
|
enum ToggleOnline {}
|
||||||
|
|
||||||
let project_id = project_handle.id();
|
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 row = theme.project_row.style_for(state, is_selected);
|
||||||
let mut worktree_root_names = String::new();
|
let mut worktree_root_names = String::new();
|
||||||
let project = if let Some(project) = project_handle.upgrade(cx.deref_mut()) {
|
let project = if let Some(project) = project_handle.upgrade(cx.deref_mut()) {
|
||||||
|
@ -548,7 +548,7 @@ impl ContactsPanel {
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child({
|
.with_child({
|
||||||
let button =
|
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);
|
let mut style = *theme.private_button.style_for(state, false);
|
||||||
if is_going_online {
|
if is_going_online {
|
||||||
style.color = theme.disabled_button.color;
|
style.color = theme.disabled_button.color;
|
||||||
|
@ -636,7 +636,7 @@ impl ContactsPanel {
|
||||||
|
|
||||||
if is_incoming {
|
if is_incoming {
|
||||||
row.add_children([
|
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 {
|
let button_style = if is_contact_request_pending {
|
||||||
&theme.disabled_button
|
&theme.disabled_button
|
||||||
} else {
|
} else {
|
||||||
|
@ -658,7 +658,7 @@ impl ContactsPanel {
|
||||||
.contained()
|
.contained()
|
||||||
.with_margin_right(button_spacing)
|
.with_margin_right(button_spacing)
|
||||||
.boxed(),
|
.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 {
|
let button_style = if is_contact_request_pending {
|
||||||
&theme.disabled_button
|
&theme.disabled_button
|
||||||
} else {
|
} else {
|
||||||
|
@ -680,7 +680,7 @@ impl ContactsPanel {
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
row.add_child(
|
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 {
|
let button_style = if is_contact_request_pending {
|
||||||
&theme.disabled_button
|
&theme.disabled_button
|
||||||
} else {
|
} else {
|
||||||
|
@ -1071,7 +1071,7 @@ impl View for ContactsPanel {
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::new::<AddContact, _, _>(0, cx, |_, _| {
|
MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
|
||||||
Svg::new("icons/user_plus_16.svg")
|
Svg::new("icons/user_plus_16.svg")
|
||||||
.with_color(theme.add_contact_button.color)
|
.with_color(theme.add_contact_button.color)
|
||||||
.constrained()
|
.constrained()
|
||||||
|
@ -1102,35 +1102,31 @@ impl View for ContactsPanel {
|
||||||
|
|
||||||
if info.count > 0 {
|
if info.count > 0 {
|
||||||
Some(
|
Some(
|
||||||
MouseEventHandler::new::<InviteLink, _, _>(
|
MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
|
||||||
0,
|
let style =
|
||||||
cx,
|
theme.invite_row.style_for(state, false).clone();
|
||||||
|state, cx| {
|
|
||||||
let style =
|
|
||||||
theme.invite_row.style_for(state, false).clone();
|
|
||||||
|
|
||||||
let copied =
|
let copied =
|
||||||
cx.read_from_clipboard().map_or(false, |item| {
|
cx.read_from_clipboard().map_or(false, |item| {
|
||||||
item.text().as_str() == info.url.as_ref()
|
item.text().as_str() == info.url.as_ref()
|
||||||
});
|
});
|
||||||
|
|
||||||
Label::new(
|
Label::new(
|
||||||
format!(
|
format!(
|
||||||
"{} invite link ({} left)",
|
"{} invite link ({} left)",
|
||||||
if copied { "Copied" } else { "Copy" },
|
if copied { "Copied" } else { "Copy" },
|
||||||
info.count
|
info.count
|
||||||
),
|
),
|
||||||
style.label.clone(),
|
style.label.clone(),
|
||||||
)
|
)
|
||||||
.aligned()
|
.aligned()
|
||||||
.left()
|
.left()
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(theme.row_height)
|
.with_height(theme.row_height)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, move |_, cx| {
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
cx.write_to_clipboard(ClipboardItem::new(
|
cx.write_to_clipboard(ClipboardItem::new(
|
||||||
|
@ -1247,7 +1243,8 @@ mod tests {
|
||||||
.0
|
.0
|
||||||
.read_with(cx, |worktree, _| worktree.id().to_proto());
|
.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| {
|
let panel = cx.add_view(&workspace, |cx| {
|
||||||
ContactsPanel::new(
|
ContactsPanel::new(
|
||||||
user_store.clone(),
|
user_store.clone(),
|
||||||
|
|
|
@ -52,7 +52,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::new::<Dismiss, _, _>(user.id as usize, cx, |state, _| {
|
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
|
||||||
render_icon_button(
|
render_icon_button(
|
||||||
theme.dismiss_button.style_for(state, false),
|
theme.dismiss_button.style_for(state, false),
|
||||||
"icons/x_mark_thin_8.svg",
|
"icons/x_mark_thin_8.svg",
|
||||||
|
@ -90,7 +90,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(buttons.into_iter().enumerate().map(
|
.with_children(buttons.into_iter().enumerate().map(
|
||||||
|(ix, (message, action))| {
|
|(ix, (message, action))| {
|
||||||
MouseEventHandler::new::<Button, _, _>(ix, cx, |state, _| {
|
MouseEventHandler::<Button>::new(ix, cx, |state, _| {
|
||||||
let button = theme.button.style_for(state, false);
|
let button = theme.button.style_for(state, false);
|
||||||
Label::new(message.to_string(), button.text.clone())
|
Label::new(message.to_string(), button.text.clone())
|
||||||
.contained()
|
.contained()
|
||||||
|
|
|
@ -22,7 +22,6 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(ContextMenu::cancel);
|
cx.add_action(ContextMenu::cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
pub enum ContextMenuItem {
|
pub enum ContextMenuItem {
|
||||||
Item {
|
Item {
|
||||||
label: String,
|
label: String,
|
||||||
|
@ -57,7 +56,8 @@ impl ContextMenuItem {
|
||||||
|
|
||||||
pub struct ContextMenu {
|
pub struct ContextMenu {
|
||||||
show_count: usize,
|
show_count: usize,
|
||||||
position: Vector2F,
|
anchor_position: Vector2F,
|
||||||
|
anchor_corner: AnchorCorner,
|
||||||
items: Vec<ContextMenuItem>,
|
items: Vec<ContextMenuItem>,
|
||||||
selected_index: Option<usize>,
|
selected_index: Option<usize>,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
|
@ -100,9 +100,10 @@ impl View for ContextMenu {
|
||||||
.boxed();
|
.boxed();
|
||||||
|
|
||||||
Overlay::new(expanded_menu)
|
Overlay::new(expanded_menu)
|
||||||
.hoverable(true)
|
.with_hoverable(true)
|
||||||
.fit_mode(OverlayFitMode::SnapToWindow)
|
.with_fit_mode(OverlayFitMode::SnapToWindow)
|
||||||
.with_abs_position(self.position)
|
.with_anchor_position(self.anchor_position)
|
||||||
|
.with_anchor_corner(self.anchor_corner)
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +116,8 @@ impl ContextMenu {
|
||||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
show_count: 0,
|
show_count: 0,
|
||||||
position: Default::default(),
|
anchor_position: Default::default(),
|
||||||
|
anchor_corner: AnchorCorner::TopLeft,
|
||||||
items: Default::default(),
|
items: Default::default(),
|
||||||
selected_index: Default::default(),
|
selected_index: Default::default(),
|
||||||
visible: Default::default(),
|
visible: Default::default(),
|
||||||
|
@ -226,14 +228,16 @@ impl ContextMenu {
|
||||||
|
|
||||||
pub fn show(
|
pub fn show(
|
||||||
&mut self,
|
&mut self,
|
||||||
position: Vector2F,
|
anchor_position: Vector2F,
|
||||||
|
anchor_corner: AnchorCorner,
|
||||||
items: impl IntoIterator<Item = ContextMenuItem>,
|
items: impl IntoIterator<Item = ContextMenuItem>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let mut items = items.into_iter().peekable();
|
let mut items = items.into_iter().peekable();
|
||||||
if items.peek().is_some() {
|
if items.peek().is_some() {
|
||||||
self.items = items.collect();
|
self.items = items.collect();
|
||||||
self.position = position;
|
self.anchor_position = anchor_position;
|
||||||
|
self.anchor_corner = anchor_corner;
|
||||||
self.visible = true;
|
self.visible = true;
|
||||||
self.show_count += 1;
|
self.show_count += 1;
|
||||||
if !cx.is_self_focused() {
|
if !cx.is_self_focused() {
|
||||||
|
@ -310,13 +314,13 @@ impl ContextMenu {
|
||||||
enum Menu {}
|
enum Menu {}
|
||||||
enum MenuItem {}
|
enum MenuItem {}
|
||||||
let style = cx.global::<Settings>().theme.context_menu.clone();
|
let style = cx.global::<Settings>().theme.context_menu.clone();
|
||||||
MouseEventHandler::new::<Menu, _, _>(0, cx, |_, cx| {
|
MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||||
match item {
|
match item {
|
||||||
ContextMenuItem::Item { label, action } => {
|
ContextMenuItem::Item { label, action } => {
|
||||||
let action = action.boxed_clone();
|
let action = action.boxed_clone();
|
||||||
MouseEventHandler::new::<MenuItem, _, _>(ix, cx, |state, _| {
|
MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
|
||||||
let style =
|
let style =
|
||||||
style.item.style_for(state, Some(ix) == self.selected_index);
|
style.item.style_for(state, Some(ix) == self.selected_index);
|
||||||
|
|
||||||
|
|
|
@ -776,7 +776,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).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
|
// Create some diagnostics
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
|
|
|
@ -89,7 +89,7 @@ impl View for DiagnosticIndicator {
|
||||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||||
let in_progress = !self.in_progress_checks.is_empty();
|
let in_progress = !self.in_progress_checks.is_empty();
|
||||||
let mut element = Flex::row().with_child(
|
let mut element = Flex::row().with_child(
|
||||||
MouseEventHandler::new::<Summary, _, _>(0, cx, |state, cx| {
|
MouseEventHandler::<Summary>::new(0, cx, |state, cx| {
|
||||||
let style = cx
|
let style = cx
|
||||||
.global::<Settings>()
|
.global::<Settings>()
|
||||||
.theme
|
.theme
|
||||||
|
@ -190,7 +190,7 @@ impl View for DiagnosticIndicator {
|
||||||
} else if let Some(diagnostic) = &self.current_diagnostic {
|
} else if let Some(diagnostic) = &self.current_diagnostic {
|
||||||
let message_style = style.diagnostic_message.clone();
|
let message_style = style.diagnostic_message.clone();
|
||||||
element.add_child(
|
element.add_child(
|
||||||
MouseEventHandler::new::<Message, _, _>(1, cx, |state, _| {
|
MouseEventHandler::<Message>::new(1, cx, |state, _| {
|
||||||
Label::new(
|
Label::new(
|
||||||
diagnostic.message.split('\n').next().unwrap().to_string(),
|
diagnostic.message.split('\n').next().unwrap().to_string(),
|
||||||
message_style.style_for(state, false).text.clone(),
|
message_style.style_for(state, false).text.clone(),
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::{any::Any, rc::Rc};
|
||||||
|
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::{Container, MouseEventHandler},
|
elements::{MouseEventHandler, Overlay},
|
||||||
geometry::vector::Vector2F,
|
geometry::vector::Vector2F,
|
||||||
scene::DragRegionEvent,
|
scene::DragRegionEvent,
|
||||||
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
|
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
|
||||||
|
@ -114,30 +114,29 @@ impl<V: View> DragAndDrop<V> {
|
||||||
|
|
||||||
let position = position + region_offset;
|
let position = position + region_offset;
|
||||||
|
|
||||||
|
enum DraggedElementHandler {}
|
||||||
Some(
|
Some(
|
||||||
MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
|
Overlay::new(
|
||||||
Container::new(render(payload, cx))
|
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
|
||||||
.with_margin_left(position.x())
|
render(payload, cx)
|
||||||
.with_margin_top(position.y())
|
})
|
||||||
.aligned()
|
.with_cursor_style(CursorStyle::Arrow)
|
||||||
.top()
|
.on_up(MouseButton::Left, |_, cx| {
|
||||||
.left()
|
cx.defer(|cx| {
|
||||||
.boxed()
|
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
|
||||||
})
|
});
|
||||||
.with_cursor_style(CursorStyle::Arrow)
|
cx.propogate_event();
|
||||||
.on_up(MouseButton::Left, |_, cx| {
|
})
|
||||||
cx.defer(|cx| {
|
.on_up_out(MouseButton::Left, |_, cx| {
|
||||||
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
|
cx.defer(|cx| {
|
||||||
});
|
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
|
||||||
cx.propogate_event();
|
});
|
||||||
})
|
})
|
||||||
.on_up_out(MouseButton::Left, |_, cx| {
|
// Don't block hover events or invalidations
|
||||||
cx.defer(|cx| {
|
.with_hoverable(false)
|
||||||
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
|
.boxed(),
|
||||||
});
|
)
|
||||||
})
|
.with_anchor_position(position)
|
||||||
// Don't block hover events or invalidations
|
|
||||||
.with_hoverable(false)
|
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -174,7 +173,7 @@ pub trait Draggable {
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Draggable for MouseEventHandler {
|
impl<Tag> Draggable for MouseEventHandler<Tag> {
|
||||||
fn as_draggable<V: View, P: Any>(
|
fn as_draggable<V: View, P: Any>(
|
||||||
self,
|
self,
|
||||||
payload: P,
|
payload: P,
|
||||||
|
|
|
@ -682,7 +682,7 @@ impl CompletionsMenu {
|
||||||
let completion = &completions[mat.candidate_id];
|
let completion = &completions[mat.candidate_id];
|
||||||
let item_ix = start_ix + ix;
|
let item_ix = start_ix + ix;
|
||||||
items.push(
|
items.push(
|
||||||
MouseEventHandler::new::<CompletionTag, _, _>(
|
MouseEventHandler::<CompletionTag>::new(
|
||||||
mat.candidate_id,
|
mat.candidate_id,
|
||||||
cx,
|
cx,
|
||||||
|state, _| {
|
|state, _| {
|
||||||
|
@ -830,7 +830,7 @@ impl CodeActionsMenu {
|
||||||
for (ix, action) in actions[range].iter().enumerate() {
|
for (ix, action) in actions[range].iter().enumerate() {
|
||||||
let item_ix = start_ix + ix;
|
let item_ix = start_ix + ix;
|
||||||
items.push(
|
items.push(
|
||||||
MouseEventHandler::new::<ActionTag, _, _>(item_ix, cx, |state, _| {
|
MouseEventHandler::<ActionTag>::new(item_ix, cx, |state, _| {
|
||||||
let item_style = if item_ix == selected_item {
|
let item_style = if item_ix == selected_item {
|
||||||
style.autocomplete.selected_item
|
style.autocomplete.selected_item
|
||||||
} else if state.hovered {
|
} else if state.hovered {
|
||||||
|
@ -2735,7 +2735,7 @@ impl Editor {
|
||||||
if self.available_code_actions.is_some() {
|
if self.available_code_actions.is_some() {
|
||||||
enum Tag {}
|
enum Tag {}
|
||||||
Some(
|
Some(
|
||||||
MouseEventHandler::new::<Tag, _, _>(0, cx, |_, _| {
|
MouseEventHandler::<Tag>::new(0, cx, |_, _| {
|
||||||
Svg::new("icons/bolt_8.svg")
|
Svg::new("icons/bolt_8.svg")
|
||||||
.with_color(style.code_actions.indicator)
|
.with_color(style.code_actions.indicator)
|
||||||
.boxed()
|
.boxed()
|
||||||
|
@ -7100,7 +7100,7 @@ mod tests {
|
||||||
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||||
cx.set_global(Settings::test(cx));
|
cx.set_global(Settings::test(cx));
|
||||||
use workspace::Item;
|
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);
|
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
||||||
|
|
||||||
cx.add_view(&pane, |cx| {
|
cx.add_view(&pane, |cx| {
|
||||||
|
@ -7826,7 +7826,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
|
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.set_state("one «two threeˇ» four");
|
||||||
cx.update_editor(|editor, cx| {
|
cx.update_editor(|editor, cx| {
|
||||||
editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
|
editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
|
||||||
|
@ -7974,7 +7974,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_newline_below(cx: &mut gpui::TestAppContext) {
|
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(|cx| {
|
||||||
cx.update_global::<Settings, _, _>(|settings, _| {
|
cx.update_global::<Settings, _, _>(|settings, _| {
|
||||||
settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
|
settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
|
||||||
|
@ -8050,7 +8050,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_tab(cx: &mut gpui::TestAppContext) {
|
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(|cx| {
|
||||||
cx.update_global::<Settings, _, _>(|settings, _| {
|
cx.update_global::<Settings, _, _>(|settings, _| {
|
||||||
settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap());
|
settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap());
|
||||||
|
@ -8081,7 +8081,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_tab_on_blank_line_auto_indents(cx: &mut gpui::TestAppContext) {
|
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(
|
let language = Arc::new(
|
||||||
Language::new(
|
Language::new(
|
||||||
LanguageConfig::default(),
|
LanguageConfig::default(),
|
||||||
|
@ -8139,7 +8139,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
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! {"
|
cx.set_state(indoc! {"
|
||||||
«oneˇ» «twoˇ»
|
«oneˇ» «twoˇ»
|
||||||
|
@ -8208,7 +8208,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
|
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(|cx| {
|
||||||
cx.update_global::<Settings, _, _>(|settings, _| {
|
cx.update_global::<Settings, _, _>(|settings, _| {
|
||||||
settings.editor_overrides.hard_tabs = Some(true);
|
settings.editor_overrides.hard_tabs = Some(true);
|
||||||
|
@ -8416,7 +8416,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_backspace(cx: &mut gpui::TestAppContext) {
|
async fn test_backspace(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = EditorTestContext::new(cx).await;
|
let mut cx = EditorTestContext::new(cx);
|
||||||
|
|
||||||
// Basic backspace
|
// Basic backspace
|
||||||
cx.set_state(indoc! {"
|
cx.set_state(indoc! {"
|
||||||
|
@ -8463,7 +8463,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_delete(cx: &mut gpui::TestAppContext) {
|
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! {"
|
cx.set_state(indoc! {"
|
||||||
onˇe two three
|
onˇe two three
|
||||||
|
@ -8800,7 +8800,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_clipboard(cx: &mut gpui::TestAppContext) {
|
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.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
|
||||||
cx.update_editor(|e, cx| e.cut(&Cut, cx));
|
cx.update_editor(|e, cx| e.cut(&Cut, cx));
|
||||||
|
@ -8876,7 +8876,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
|
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(
|
let language = Arc::new(Language::new(
|
||||||
LanguageConfig::default(),
|
LanguageConfig::default(),
|
||||||
Some(tree_sitter_rust::language()),
|
Some(tree_sitter_rust::language()),
|
||||||
|
@ -9305,7 +9305,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_select_next(cx: &mut gpui::TestAppContext) {
|
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.set_state("abc\nˇabc abc\ndefabc\nabc");
|
||||||
|
|
||||||
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
|
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -20,6 +20,10 @@ use crate::{
|
||||||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
||||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
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)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct HoverAt {
|
pub struct HoverAt {
|
||||||
pub point: Option<DisplayPoint>,
|
pub point: Option<DisplayPoint>,
|
||||||
|
@ -312,7 +316,7 @@ pub struct InfoPopover {
|
||||||
|
|
||||||
impl InfoPopover {
|
impl InfoPopover {
|
||||||
pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
|
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);
|
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
||||||
flex.extend(self.contents.iter().map(|content| {
|
flex.extend(self.contents.iter().map(|content| {
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
|
@ -350,10 +354,11 @@ impl InfoPopover {
|
||||||
.with_style(style.hover_popover.container)
|
.with_style(style.hover_popover.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
|
.on_move(|_, _| {})
|
||||||
.with_cursor_style(CursorStyle::Arrow)
|
.with_cursor_style(CursorStyle::Arrow)
|
||||||
.with_padding(Padding {
|
.with_padding(Padding {
|
||||||
bottom: 5.,
|
bottom: HOVER_POPOVER_GAP,
|
||||||
top: 5.,
|
top: HOVER_POPOVER_GAP,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
|
@ -383,13 +388,19 @@ impl DiagnosticPopover {
|
||||||
|
|
||||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
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)
|
Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style)
|
||||||
.with_soft_wrap(true)
|
.with_soft_wrap(true)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(container_style)
|
.with_style(container_style)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
|
.with_padding(Padding {
|
||||||
|
top: HOVER_POPOVER_GAP,
|
||||||
|
bottom: HOVER_POPOVER_GAP,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.on_move(|_, _| {})
|
||||||
.on_click(MouseButton::Left, |_, cx| {
|
.on_click(MouseButton::Left, |_, cx| {
|
||||||
cx.dispatch_action(GoToDiagnostic)
|
cx.dispatch_action(GoToDiagnostic)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
use context_menu::ContextMenuItem;
|
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::{
|
use crate::{
|
||||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
|
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
|
||||||
|
@ -46,6 +49,7 @@ pub fn deploy_context_menu(
|
||||||
editor.mouse_context_menu.update(cx, |menu, cx| {
|
editor.mouse_context_menu.update(cx, |menu, cx| {
|
||||||
menu.show(
|
menu.show(
|
||||||
position,
|
position,
|
||||||
|
AnchorCorner::TopLeft,
|
||||||
vec![
|
vec![
|
||||||
ContextMenuItem::item("Rename Symbol", Rename),
|
ContextMenuItem::item("Rename Symbol", Rename),
|
||||||
ContextMenuItem::item("Go To Definition", GoToDefinition),
|
ContextMenuItem::item("Go To Definition", GoToDefinition),
|
||||||
|
|
|
@ -88,7 +88,7 @@ pub struct EditorTestContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> 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| {
|
let (window_id, editor) = cx.update(|cx| {
|
||||||
cx.set_global(Settings::test(cx));
|
cx.set_global(Settings::test(cx));
|
||||||
crate::init(cx);
|
crate::init(cx);
|
||||||
|
@ -364,7 +364,8 @@ impl<'a> EditorLspTestContext<'a> {
|
||||||
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
|
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
|
||||||
.await;
|
.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
|
project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree("/root", true, cx)
|
project.find_or_create_local_worktree("/root", true, cx)
|
||||||
|
|
|
@ -316,7 +316,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).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);
|
cx.dispatch_action(window_id, Toggle);
|
||||||
|
|
||||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||||
|
@ -370,7 +371,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).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) =
|
let (_, finder) =
|
||||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||||
|
|
||||||
|
@ -444,7 +446,8 @@ mod tests {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
let (_, workspace) =
|
||||||
|
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||||
let (_, finder) =
|
let (_, finder) =
|
||||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||||
finder
|
finder
|
||||||
|
@ -468,7 +471,8 @@ mod tests {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
let (_, workspace) =
|
||||||
|
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||||
let (_, finder) =
|
let (_, finder) =
|
||||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||||
|
|
||||||
|
@ -520,7 +524,8 @@ mod tests {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
let (_, workspace) =
|
||||||
|
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||||
let (_, finder) =
|
let (_, finder) =
|
||||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||||
|
|
||||||
|
@ -558,7 +563,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).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) =
|
let (_, finder) =
|
||||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||||
finder
|
finder
|
||||||
|
|
|
@ -1960,6 +1960,7 @@ impl MutableAppContext {
|
||||||
{
|
{
|
||||||
let mut app = self.upgrade();
|
let mut app = self.upgrade();
|
||||||
let presenter = Rc::downgrade(&presenter);
|
let presenter = Rc::downgrade(&presenter);
|
||||||
|
|
||||||
window.on_event(Box::new(move |event| {
|
window.on_event(Box::new(move |event| {
|
||||||
app.update(|cx| {
|
app.update(|cx| {
|
||||||
if let Some(presenter) = presenter.upgrade() {
|
if let Some(presenter) = presenter.upgrade() {
|
||||||
|
@ -4028,7 +4029,7 @@ pub struct RenderParams {
|
||||||
pub view_id: usize,
|
pub view_id: usize,
|
||||||
pub titlebar_height: f32,
|
pub titlebar_height: f32,
|
||||||
pub hovered_region_ids: HashSet<MouseRegionId>,
|
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 refreshing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4037,7 +4038,7 @@ pub struct RenderContext<'a, T: View> {
|
||||||
pub(crate) view_id: usize,
|
pub(crate) view_id: usize,
|
||||||
pub(crate) view_type: PhantomData<T>,
|
pub(crate) view_type: PhantomData<T>,
|
||||||
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
|
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 app: &'a mut MutableAppContext,
|
||||||
pub titlebar_height: f32,
|
pub titlebar_height: f32,
|
||||||
pub refreshing: bool,
|
pub refreshing: bool,
|
||||||
|
@ -4076,10 +4077,7 @@ impl<'a, V: View> RenderContext<'a, V> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_state<Tag: 'static>(&self, region_id: usize) -> MouseState {
|
pub fn mouse_state<Tag: 'static>(&self, region_id: usize) -> MouseState {
|
||||||
let region_id = MouseRegionId {
|
let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
|
||||||
view_id: self.view_id,
|
|
||||||
discriminant: (TypeId::of::<Tag>(), region_id),
|
|
||||||
};
|
|
||||||
MouseState {
|
MouseState {
|
||||||
hovered: self.hovered_region_ids.contains(®ion_id),
|
hovered: self.hovered_region_ids.contains(®ion_id),
|
||||||
clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| {
|
clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| {
|
||||||
|
@ -4092,9 +4090,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,
|
&mut self,
|
||||||
element_id: usize,
|
element_id: usize,
|
||||||
|
initial: T,
|
||||||
) -> ElementStateHandle<T> {
|
) -> ElementStateHandle<T> {
|
||||||
let id = ElementStateId {
|
let id = ElementStateId {
|
||||||
view_id: self.view_id(),
|
view_id: self.view_id(),
|
||||||
|
@ -4104,9 +4103,16 @@ impl<'a, V: View> RenderContext<'a, V> {
|
||||||
self.cx
|
self.cx
|
||||||
.element_states
|
.element_states
|
||||||
.entry(id)
|
.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)
|
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 {
|
impl AsRef<AppContext> for &AppContext {
|
||||||
|
@ -5229,6 +5235,10 @@ impl<T: 'static> ElementStateHandle<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> ElementStateId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
|
pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
|
||||||
cx.element_states
|
cx.element_states
|
||||||
.get(&self.id)
|
.get(&self.id)
|
||||||
|
@ -6032,12 +6042,12 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::View for View {
|
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();
|
let mouse_down_count = self.mouse_down_count.clone();
|
||||||
EventHandler::new(Empty::new().boxed())
|
MouseEventHandler::<Handler>::new(0, cx, |_, _| Empty::new().boxed())
|
||||||
.on_mouse_down(move |_| {
|
.on_down(MouseButton::Left, move |_, _| {
|
||||||
mouse_down_count.fetch_add(1, SeqCst);
|
mouse_down_count.fetch_add(1, SeqCst);
|
||||||
true
|
|
||||||
})
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ mod canvas;
|
||||||
mod constrained_box;
|
mod constrained_box;
|
||||||
mod container;
|
mod container;
|
||||||
mod empty;
|
mod empty;
|
||||||
mod event_handler;
|
|
||||||
mod expanded;
|
mod expanded;
|
||||||
mod flex;
|
mod flex;
|
||||||
mod hook;
|
mod hook;
|
||||||
|
@ -13,6 +12,7 @@ mod label;
|
||||||
mod list;
|
mod list;
|
||||||
mod mouse_event_handler;
|
mod mouse_event_handler;
|
||||||
mod overlay;
|
mod overlay;
|
||||||
|
mod resizable;
|
||||||
mod stack;
|
mod stack;
|
||||||
mod svg;
|
mod svg;
|
||||||
mod text;
|
mod text;
|
||||||
|
@ -21,8 +21,8 @@ mod uniform_list;
|
||||||
|
|
||||||
use self::expanded::Expanded;
|
use self::expanded::Expanded;
|
||||||
pub use self::{
|
pub use self::{
|
||||||
align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
|
align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*,
|
||||||
hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
|
keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*,
|
||||||
stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
|
stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
|
||||||
};
|
};
|
||||||
pub use crate::presenter::ChildView;
|
pub use crate::presenter::ChildView;
|
||||||
|
@ -187,6 +187,27 @@ pub trait Element {
|
||||||
{
|
{
|
||||||
Tooltip::new::<Tag, T>(id, text, action, style, self.boxed(), cx)
|
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> {
|
pub enum Lifecycle<T: Element> {
|
||||||
|
|
|
@ -373,6 +373,24 @@ pub struct Padding {
|
||||||
pub right: f32,
|
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 {
|
impl<'de> Deserialize<'de> for Padding {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
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,
|
Tag: 'static,
|
||||||
V: View,
|
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);
|
scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to);
|
||||||
self.scroll_state = Some(scroll_state);
|
self.scroll_state = Some(scroll_state);
|
||||||
self
|
self
|
||||||
|
|
|
@ -13,31 +13,32 @@ use crate::{
|
||||||
MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
|
MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
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,
|
child: ElementBox,
|
||||||
discriminant: (TypeId, usize),
|
region_id: usize,
|
||||||
cursor_style: Option<CursorStyle>,
|
cursor_style: Option<CursorStyle>,
|
||||||
handlers: HandlerSet,
|
handlers: HandlerSet,
|
||||||
hoverable: bool,
|
hoverable: bool,
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
|
_tag: PhantomData<Tag>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MouseEventHandler {
|
impl<Tag> MouseEventHandler<Tag> {
|
||||||
pub fn new<Tag, V, F>(id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
|
pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
|
||||||
where
|
where
|
||||||
Tag: 'static,
|
|
||||||
V: View,
|
V: View,
|
||||||
F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
|
F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
|
||||||
{
|
{
|
||||||
Self {
|
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,
|
cursor_style: None,
|
||||||
discriminant: (TypeId::of::<Tag>(), id),
|
|
||||||
handlers: Default::default(),
|
handlers: Default::default(),
|
||||||
hoverable: true,
|
hoverable: true,
|
||||||
padding: Default::default(),
|
padding: Default::default(),
|
||||||
|
_tag: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +141,7 @@ impl MouseEventHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for MouseEventHandler {
|
impl<Tag> Element for MouseEventHandler<Tag> {
|
||||||
type LayoutState = ();
|
type LayoutState = ();
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
|
@ -168,9 +169,9 @@ impl Element for MouseEventHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.scene.push_mouse_region(
|
cx.scene.push_mouse_region(
|
||||||
MouseRegion::from_handlers(
|
MouseRegion::from_handlers::<Tag>(
|
||||||
cx.current_view_id(),
|
cx.current_view_id(),
|
||||||
Some(self.discriminant),
|
self.region_id,
|
||||||
hit_bounds,
|
hit_bounds,
|
||||||
self.handlers.clone(),
|
self.handlers.clone(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,14 +4,15 @@ use crate::{
|
||||||
geometry::{rect::RectF, vector::Vector2F},
|
geometry::{rect::RectF, vector::Vector2F},
|
||||||
json::ToJson,
|
json::ToJson,
|
||||||
presenter::MeasurementContext,
|
presenter::MeasurementContext,
|
||||||
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
|
Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
|
||||||
PaintContext, SizeConstraint,
|
PaintContext, SizeConstraint,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
pub struct Overlay {
|
pub struct Overlay {
|
||||||
child: ElementBox,
|
child: ElementBox,
|
||||||
abs_position: Option<Vector2F>,
|
anchor_position: Option<Vector2F>,
|
||||||
|
anchor_corner: AnchorCorner,
|
||||||
fit_mode: OverlayFitMode,
|
fit_mode: OverlayFitMode,
|
||||||
hoverable: bool,
|
hoverable: bool,
|
||||||
}
|
}
|
||||||
|
@ -19,31 +20,79 @@ pub struct Overlay {
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub enum OverlayFitMode {
|
pub enum OverlayFitMode {
|
||||||
SnapToWindow,
|
SnapToWindow,
|
||||||
FlipAlignment,
|
SwitchAnchor,
|
||||||
None,
|
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 {
|
impl Overlay {
|
||||||
pub fn new(child: ElementBox) -> Self {
|
pub fn new(child: ElementBox) -> Self {
|
||||||
Self {
|
Self {
|
||||||
child,
|
child,
|
||||||
abs_position: None,
|
anchor_position: None,
|
||||||
|
anchor_corner: AnchorCorner::TopLeft,
|
||||||
fit_mode: OverlayFitMode::None,
|
fit_mode: OverlayFitMode::None,
|
||||||
hoverable: false,
|
hoverable: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_abs_position(mut self, position: Vector2F) -> Self {
|
pub fn with_anchor_position(mut self, position: Vector2F) -> Self {
|
||||||
self.abs_position = Some(position);
|
self.anchor_position = Some(position);
|
||||||
self
|
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.fit_mode = fit_mode;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hoverable(mut self, hoverable: bool) -> Self {
|
pub fn with_hoverable(mut self, hoverable: bool) -> Self {
|
||||||
self.hoverable = hoverable;
|
self.hoverable = hoverable;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -58,7 +107,7 @@ impl Element for Overlay {
|
||||||
constraint: SizeConstraint,
|
constraint: SizeConstraint,
|
||||||
cx: &mut LayoutContext,
|
cx: &mut LayoutContext,
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
) -> (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)
|
SizeConstraint::new(Vector2F::zero(), cx.window_size)
|
||||||
} else {
|
} else {
|
||||||
constraint
|
constraint
|
||||||
|
@ -74,45 +123,75 @@ impl Element for Overlay {
|
||||||
size: &mut Self::LayoutState,
|
size: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) {
|
) {
|
||||||
let mut bounds = RectF::new(self.abs_position.unwrap_or_else(|| bounds.origin()), *size);
|
let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
|
||||||
cx.scene.push_stacking_context(None);
|
let mut bounds = self.anchor_corner.get_bounds(anchor_position, *size);
|
||||||
|
|
||||||
if self.hoverable {
|
|
||||||
cx.scene.push_mouse_region(MouseRegion {
|
|
||||||
view_id: cx.current_view_id(),
|
|
||||||
bounds,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.fit_mode {
|
match self.fit_mode {
|
||||||
OverlayFitMode::SnapToWindow => {
|
OverlayFitMode::SnapToWindow => {
|
||||||
// Snap the right edge of the overlay to the right edge of the window if
|
// Snap the horizontal edges of the overlay to the horizontal edges of the window if
|
||||||
// its horizontal bounds overflow.
|
// its horizontal bounds overflow
|
||||||
if bounds.lower_right().x() > cx.window_size.x() {
|
if bounds.max_x() > cx.window_size.x() {
|
||||||
bounds.set_origin_x((cx.window_size.x() - bounds.width()).max(0.));
|
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.
|
// its vertical bounds overflow.
|
||||||
if bounds.lower_right().y() > cx.window_size.y() {
|
if bounds.max_y() > cx.window_size.y() {
|
||||||
bounds.set_origin_y((cx.window_size.y() - bounds.height()).max(0.));
|
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 => {
|
OverlayFitMode::SwitchAnchor => {
|
||||||
// Right-align overlay if its horizontal bounds overflow.
|
let mut anchor_corner = self.anchor_corner;
|
||||||
if bounds.lower_right().x() > cx.window_size.x() {
|
|
||||||
bounds.set_origin_x(bounds.origin_x() - bounds.width());
|
if bounds.max_x() > cx.window_size.x() {
|
||||||
|
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom-align overlay if its vertical bounds overflow.
|
if bounds.max_y() > cx.window_size.y() {
|
||||||
if bounds.lower_right().y() > cx.window_size.y() {
|
anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
|
||||||
bounds.set_origin_y(bounds.origin_y() - bounds.height());
|
}
|
||||||
|
|
||||||
|
if bounds.min_x() < 0. {
|
||||||
|
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => {}
|
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);
|
self.child.paint(bounds.origin(), bounds, cx);
|
||||||
cx.scene.pop_stacking_context();
|
cx.scene.pop_stacking_context();
|
||||||
}
|
}
|
||||||
|
@ -150,7 +229,7 @@ impl Element for Overlay {
|
||||||
) -> serde_json::Value {
|
) -> serde_json::Value {
|
||||||
json!({
|
json!({
|
||||||
"type": "Overlay",
|
"type": "Overlay",
|
||||||
"abs_position": self.abs_position.to_json(),
|
"abs_position": self.anchor_position.to_json(),
|
||||||
"child": self.child.debug(cx),
|
"child": self.child.debug(cx),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
225
crates/gpui/src/elements/resizable.rs
Normal file
225
crates/gpui/src/elements/resizable.rs
Normal file
|
@ -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 ElementState<Tag>(Tag);
|
||||||
struct MouseEventHandlerState<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 state = state_handle.read(cx).clone();
|
||||||
let tooltip = if state.visible.get() {
|
let tooltip = if state.visible.get() {
|
||||||
let mut collapsed_tooltip = Self::render_tooltip(
|
let mut collapsed_tooltip = Self::render_tooltip(
|
||||||
|
@ -84,42 +84,41 @@ impl Tooltip {
|
||||||
})
|
})
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.fit_mode(OverlayFitMode::FlipAlignment)
|
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||||
.with_abs_position(state.position.get())
|
.with_anchor_position(state.position.get())
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let child =
|
let child = MouseEventHandler::<MouseEventHandlerState<Tag>>::new(id, cx, |_, _| child)
|
||||||
MouseEventHandler::new::<MouseEventHandlerState<Tag>, _, _>(id, cx, |_, _| child)
|
.on_hover(move |e, cx| {
|
||||||
.on_hover(move |e, cx| {
|
let position = e.position;
|
||||||
let position = e.position;
|
let window_id = cx.window_id();
|
||||||
let window_id = cx.window_id();
|
if let Some(view_id) = cx.view_id() {
|
||||||
if let Some(view_id) = cx.view_id() {
|
if e.started {
|
||||||
if e.started {
|
if !state.visible.get() {
|
||||||
if !state.visible.get() {
|
state.position.set(position);
|
||||||
state.position.set(position);
|
|
||||||
|
|
||||||
let mut debounce = state.debounce.borrow_mut();
|
let mut debounce = state.debounce.borrow_mut();
|
||||||
if debounce.is_none() {
|
if debounce.is_none() {
|
||||||
*debounce = Some(cx.spawn({
|
*debounce = Some(cx.spawn({
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
|mut cx| async move {
|
|mut cx| async move {
|
||||||
cx.background().timer(DEBOUNCE_TIMEOUT).await;
|
cx.background().timer(DEBOUNCE_TIMEOUT).await;
|
||||||
state.visible.set(true);
|
state.visible.set(true);
|
||||||
cx.update(|cx| cx.notify_view(window_id, view_id));
|
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 {
|
Self {
|
||||||
child,
|
child,
|
||||||
tooltip,
|
tooltip,
|
||||||
|
|
|
@ -163,6 +163,7 @@ pub enum PromptLevel {
|
||||||
pub enum CursorStyle {
|
pub enum CursorStyle {
|
||||||
Arrow,
|
Arrow,
|
||||||
ResizeLeftRight,
|
ResizeLeftRight,
|
||||||
|
ResizeUpDown,
|
||||||
PointingHand,
|
PointingHand,
|
||||||
IBeam,
|
IBeam,
|
||||||
}
|
}
|
||||||
|
|
|
@ -681,6 +681,7 @@ impl platform::Platform for MacPlatform {
|
||||||
let cursor: id = match style {
|
let cursor: id = match style {
|
||||||
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
|
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
|
||||||
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||||
|
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||||
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
|
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
|
||||||
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
|
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,8 @@ use crate::{
|
||||||
platform::{CursorStyle, Event},
|
platform::{CursorStyle, Event},
|
||||||
scene::{
|
scene::{
|
||||||
ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
|
ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
|
||||||
HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
|
HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent,
|
||||||
|
UpOutRegionEvent, UpRegionEvent,
|
||||||
},
|
},
|
||||||
text_layout::TextLayoutCache,
|
text_layout::TextLayoutCache,
|
||||||
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
|
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
|
||||||
|
@ -36,7 +37,7 @@ pub struct Presenter {
|
||||||
asset_cache: Arc<AssetCache>,
|
asset_cache: Arc<AssetCache>,
|
||||||
last_mouse_moved_event: Option<Event>,
|
last_mouse_moved_event: Option<Event>,
|
||||||
hovered_region_ids: HashSet<MouseRegionId>,
|
hovered_region_ids: HashSet<MouseRegionId>,
|
||||||
clicked_regions: Vec<MouseRegion>,
|
clicked_region_ids: HashSet<MouseRegionId>,
|
||||||
clicked_button: Option<MouseButton>,
|
clicked_button: Option<MouseButton>,
|
||||||
mouse_position: Vector2F,
|
mouse_position: Vector2F,
|
||||||
titlebar_height: f32,
|
titlebar_height: f32,
|
||||||
|
@ -61,7 +62,7 @@ impl Presenter {
|
||||||
asset_cache,
|
asset_cache,
|
||||||
last_mouse_moved_event: None,
|
last_mouse_moved_event: None,
|
||||||
hovered_region_ids: Default::default(),
|
hovered_region_ids: Default::default(),
|
||||||
clicked_regions: Vec::new(),
|
clicked_region_ids: Default::default(),
|
||||||
clicked_button: None,
|
clicked_button: None,
|
||||||
mouse_position: vec2f(0., 0.),
|
mouse_position: vec2f(0., 0.),
|
||||||
titlebar_height,
|
titlebar_height,
|
||||||
|
@ -86,15 +87,9 @@ impl Presenter {
|
||||||
view_id: *view_id,
|
view_id: *view_id,
|
||||||
titlebar_height: self.titlebar_height,
|
titlebar_height: self.titlebar_height,
|
||||||
hovered_region_ids: self.hovered_region_ids.clone(),
|
hovered_region_ids: self.hovered_region_ids.clone(),
|
||||||
clicked_region_ids: self.clicked_button.map(|button| {
|
clicked_region_ids: self
|
||||||
(
|
.clicked_button
|
||||||
self.clicked_regions
|
.map(|button| (self.clicked_region_ids.clone(), button)),
|
||||||
.iter()
|
|
||||||
.filter_map(MouseRegion::id)
|
|
||||||
.collect(),
|
|
||||||
button,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
@ -112,15 +107,9 @@ impl Presenter {
|
||||||
view_id: *view_id,
|
view_id: *view_id,
|
||||||
titlebar_height: self.titlebar_height,
|
titlebar_height: self.titlebar_height,
|
||||||
hovered_region_ids: self.hovered_region_ids.clone(),
|
hovered_region_ids: self.hovered_region_ids.clone(),
|
||||||
clicked_region_ids: self.clicked_button.map(|button| {
|
clicked_region_ids: self
|
||||||
(
|
.clicked_button
|
||||||
self.clicked_regions
|
.map(|button| (self.clicked_region_ids.clone(), button)),
|
||||||
.iter()
|
|
||||||
.filter_map(MouseRegion::id)
|
|
||||||
.collect(),
|
|
||||||
button,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
refreshing: true,
|
refreshing: true,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -184,15 +173,9 @@ impl Presenter {
|
||||||
view_stack: Vec::new(),
|
view_stack: Vec::new(),
|
||||||
refreshing,
|
refreshing,
|
||||||
hovered_region_ids: self.hovered_region_ids.clone(),
|
hovered_region_ids: self.hovered_region_ids.clone(),
|
||||||
clicked_region_ids: self.clicked_button.map(|button| {
|
clicked_region_ids: self
|
||||||
(
|
.clicked_button
|
||||||
self.clicked_regions
|
.map(|button| (self.clicked_region_ids.clone(), button)),
|
||||||
.iter()
|
|
||||||
.filter_map(MouseRegion::id)
|
|
||||||
.collect(),
|
|
||||||
button,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
titlebar_height: self.titlebar_height,
|
titlebar_height: self.titlebar_height,
|
||||||
window_size,
|
window_size,
|
||||||
app: cx,
|
app: cx,
|
||||||
|
@ -235,6 +218,7 @@ impl Presenter {
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
|
if let Some(root_view_id) = cx.root_view_id(self.window_id) {
|
||||||
let mut events_to_send = Vec::new();
|
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
|
// 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?]
|
// -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
|
||||||
|
@ -248,16 +232,23 @@ impl Presenter {
|
||||||
|
|
||||||
// If there is already clicked_button stored, don't replace it.
|
// If there is already clicked_button stored, don't replace it.
|
||||||
if self.clicked_button.is_none() {
|
if self.clicked_button.is_none() {
|
||||||
self.clicked_regions = self
|
self.clicked_region_ids = self
|
||||||
.mouse_regions
|
.mouse_regions
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(region, _)| {
|
.filter_map(|(region, _)| {
|
||||||
region
|
if region.bounds.contains_point(e.position) {
|
||||||
.bounds
|
Some(region.id())
|
||||||
.contains_point(e.position)
|
} else {
|
||||||
.then(|| region.clone())
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.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);
|
self.clicked_button = Some(e.button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,6 +332,12 @@ impl Presenter {
|
||||||
|
|
||||||
self.last_mouse_moved_event = Some(event.clone());
|
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(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -349,7 +346,6 @@ impl Presenter {
|
||||||
self.mouse_position = position;
|
self.mouse_position = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut invalidated_views: HashSet<usize> = Default::default();
|
|
||||||
let mut any_event_handled = false;
|
let mut any_event_handled = false;
|
||||||
// 2. Process the raw mouse events into region events
|
// 2. Process the raw mouse events into region events
|
||||||
for mut region_event in events_to_send {
|
for mut region_event in events_to_send {
|
||||||
|
@ -375,23 +371,21 @@ impl Presenter {
|
||||||
top_most_depth = Some(depth);
|
top_most_depth = Some(depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(region_id) = region.id() {
|
// This unwrap relies on short circuiting boolean expressions
|
||||||
// This unwrap relies on short circuiting boolean expressions
|
// The right side of the && is only executed when contains_mouse
|
||||||
// The right side of the && is only executed when contains_mouse
|
// is true, and we know above that when contains_mouse is true
|
||||||
// is true, and we know above that when contains_mouse is true
|
// top_most_depth is set
|
||||||
// top_most_depth is set
|
if contains_mouse && depth == top_most_depth.unwrap() {
|
||||||
if contains_mouse && depth == top_most_depth.unwrap() {
|
//Ensure that hover entrance events aren't sent twice
|
||||||
//Ensure that hover entrance events aren't sent twice
|
if self.hovered_region_ids.insert(region.id()) {
|
||||||
if self.hovered_region_ids.insert(region_id) {
|
valid_regions.push(region.clone());
|
||||||
valid_regions.push(region.clone());
|
invalidated_views.insert(region.id().view_id());
|
||||||
invalidated_views.insert(region.view_id);
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
// Ensure that hover exit events aren't sent twice
|
||||||
// Ensure that hover exit events aren't sent twice
|
if self.hovered_region_ids.remove(®ion.id()) {
|
||||||
if self.hovered_region_ids.remove(®ion_id) {
|
valid_regions.push(region.clone());
|
||||||
valid_regions.push(region.clone());
|
invalidated_views.insert(region.id().view_id());
|
||||||
invalidated_views.insert(region.view_id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -404,21 +398,30 @@ impl Presenter {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
// Clear clicked regions and clicked button
|
// Clear clicked regions and clicked button
|
||||||
let clicked_regions =
|
let clicked_region_ids =
|
||||||
std::mem::replace(&mut self.clicked_regions, Vec::new());
|
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;
|
self.clicked_button = None;
|
||||||
|
|
||||||
// Find regions which still overlap with the mouse since the last MouseDown happened
|
// Find regions which still overlap with the mouse since the last MouseDown happened
|
||||||
for clicked_region in clicked_regions.into_iter().rev() {
|
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
||||||
if clicked_region.bounds.contains_point(e.position) {
|
if clicked_region_ids.contains(&mouse_region.id()) {
|
||||||
valid_regions.push(clicked_region);
|
if mouse_region.bounds.contains_point(self.mouse_position) {
|
||||||
|
valid_regions.push(mouse_region.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MouseRegionEvent::Drag(_) => {
|
MouseRegionEvent::Drag(_) => {
|
||||||
for clicked_region in self.clicked_regions.iter().rev() {
|
for (mouse_region, _) in self.mouse_regions.iter().rev() {
|
||||||
valid_regions.push(clicked_region.clone());
|
if self.clicked_region_ids.contains(&mouse_region.id()) {
|
||||||
|
valid_regions.push(mouse_region.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,18 +450,18 @@ impl Presenter {
|
||||||
|
|
||||||
region_event.set_region(valid_region.bounds);
|
region_event.set_region(valid_region.bounds);
|
||||||
if let MouseRegionEvent::Hover(e) = &mut region_event {
|
if let MouseRegionEvent::Hover(e) = &mut region_event {
|
||||||
e.started = valid_region
|
e.started = hovered_region_ids.contains(&valid_region.id())
|
||||||
.id()
|
|
||||||
.map(|region_id| hovered_region_ids.contains(®ion_id))
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
// 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.
|
// 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
|
// This behavior can be overridden by adding a Down handler that calls cx.propogate_event
|
||||||
if let MouseRegionEvent::Down(e) = ®ion_event {
|
if let MouseRegionEvent::Down(e) = ®ion_event {
|
||||||
if valid_region
|
if valid_region
|
||||||
.handlers
|
.handlers
|
||||||
.contains_handler(MouseRegionEvent::click_disc(), Some(e.button))
|
.contains_handler(MouseRegionEvent::click_disc(), Some(e.button))
|
||||||
|
|| valid_region
|
||||||
|
.handlers
|
||||||
|
.contains_handler(MouseRegionEvent::drag_disc(), Some(e.button))
|
||||||
{
|
{
|
||||||
event_cx.handled = true;
|
event_cx.handled = true;
|
||||||
}
|
}
|
||||||
|
@ -466,8 +469,10 @@ impl Presenter {
|
||||||
|
|
||||||
if let Some(callback) = valid_region.handlers.get(®ion_event.handler_key()) {
|
if let Some(callback) = valid_region.handlers.get(®ion_event.handler_key()) {
|
||||||
event_cx.handled = true;
|
event_cx.handled = true;
|
||||||
event_cx.invalidated_views.insert(valid_region.view_id);
|
event_cx
|
||||||
event_cx.with_current_view(valid_region.view_id, {
|
.invalidated_views
|
||||||
|
.insert(valid_region.id().view_id());
|
||||||
|
event_cx.with_current_view(valid_region.id().view_id(), {
|
||||||
let region_event = region_event.clone();
|
let region_event = region_event.clone();
|
||||||
|cx| {
|
|cx| {
|
||||||
callback(region_event, cx);
|
callback(region_event, cx);
|
||||||
|
@ -546,7 +551,7 @@ pub struct LayoutContext<'a> {
|
||||||
pub window_size: Vector2F,
|
pub window_size: Vector2F,
|
||||||
titlebar_height: f32,
|
titlebar_height: f32,
|
||||||
hovered_region_ids: HashSet<MouseRegionId>,
|
hovered_region_ids: HashSet<MouseRegionId>,
|
||||||
clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
|
clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> LayoutContext<'a> {
|
impl<'a> LayoutContext<'a> {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
mod mouse_region;
|
mod mouse_region;
|
||||||
mod mouse_region_event;
|
mod mouse_region_event;
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
use collections::HashSet;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
@ -20,6 +22,8 @@ pub struct Scene {
|
||||||
scale_factor: f32,
|
scale_factor: f32,
|
||||||
stacking_contexts: Vec<StackingContext>,
|
stacking_contexts: Vec<StackingContext>,
|
||||||
active_stacking_context_stack: Vec<usize>,
|
active_stacking_context_stack: Vec<usize>,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
mouse_region_ids: HashSet<MouseRegionId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StackingContext {
|
struct StackingContext {
|
||||||
|
@ -177,6 +181,8 @@ impl Scene {
|
||||||
scale_factor,
|
scale_factor,
|
||||||
stacking_contexts: vec![stacking_context],
|
stacking_contexts: vec![stacking_context],
|
||||||
active_stacking_context_stack: vec![0],
|
active_stacking_context_stack: vec![0],
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
mouse_region_ids: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,7 +247,24 @@ impl Scene {
|
||||||
|
|
||||||
pub fn push_mouse_region(&mut self, region: MouseRegion) {
|
pub fn push_mouse_region(&mut self, region: MouseRegion) {
|
||||||
if can_draw(region.bounds) {
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,15 +387,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
|
if let Some(bounds) = region
|
||||||
.bounds
|
.bounds
|
||||||
.intersection(self.clip_bounds.unwrap_or(region.bounds))
|
.intersection(self.clip_bounds.unwrap_or(region.bounds))
|
||||||
{
|
{
|
||||||
if can_draw(bounds) {
|
if can_draw(bounds) {
|
||||||
self.mouse_regions.push(region);
|
self.mouse_regions.push(region);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_underline(&mut self, underline: Underline) {
|
fn push_underline(&mut self, underline: Underline) {
|
||||||
|
@ -536,11 +561,8 @@ impl ToJson for Border {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MouseRegion {
|
impl MouseRegion {
|
||||||
pub fn id(&self) -> Option<MouseRegionId> {
|
pub fn id(&self) -> MouseRegionId {
|
||||||
self.discriminant.map(|discriminant| MouseRegionId {
|
self.id
|
||||||
view_id: self.view_id,
|
|
||||||
discriminant,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
use collections::HashMap;
|
||||||
|
|
||||||
|
@ -6,54 +6,55 @@ use pathfinder_geometry::rect::RectF;
|
||||||
|
|
||||||
use crate::{EventContext, MouseButton};
|
use crate::{EventContext, MouseButton};
|
||||||
|
|
||||||
use super::mouse_region_event::{
|
use super::{
|
||||||
ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
|
mouse_region_event::{
|
||||||
MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
|
ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
|
||||||
|
MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
|
||||||
|
},
|
||||||
|
ScrollWheelRegionEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone)]
|
||||||
pub struct MouseRegion {
|
pub struct MouseRegion {
|
||||||
pub view_id: usize,
|
pub id: MouseRegionId,
|
||||||
pub discriminant: Option<(TypeId, usize)>,
|
|
||||||
pub bounds: RectF,
|
pub bounds: RectF,
|
||||||
pub handlers: HandlerSet,
|
pub handlers: HandlerSet,
|
||||||
pub hoverable: bool,
|
pub hoverable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MouseRegion {
|
impl MouseRegion {
|
||||||
pub fn new(view_id: usize, discriminant: Option<(TypeId, usize)>, bounds: RectF) -> Self {
|
/// Region ID is used to track semantically equivalent mouse regions across render passes.
|
||||||
Self::from_handlers(view_id, discriminant, bounds, Default::default())
|
/// 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(
|
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 from_handlers<Tag: 'static>(
|
||||||
view_id: usize,
|
view_id: usize,
|
||||||
discriminant: Option<(TypeId, usize)>,
|
region_id: usize,
|
||||||
bounds: RectF,
|
bounds: RectF,
|
||||||
handlers: HandlerSet,
|
handlers: HandlerSet,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
view_id,
|
id: MouseRegionId {
|
||||||
discriminant,
|
view_id,
|
||||||
|
tag: TypeId::of::<Tag>(),
|
||||||
|
region_id,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
tag_type_name: std::any::type_name::<Tag>(),
|
||||||
|
},
|
||||||
bounds,
|
bounds,
|
||||||
handlers,
|
handlers,
|
||||||
hoverable: true,
|
hoverable: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_all(
|
|
||||||
view_id: usize,
|
|
||||||
discriminant: Option<(TypeId, usize)>,
|
|
||||||
bounds: RectF,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
view_id,
|
|
||||||
discriminant,
|
|
||||||
bounds,
|
|
||||||
handlers: HandlerSet::capture_all(),
|
|
||||||
hoverable: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_down(
|
pub fn on_down(
|
||||||
mut self,
|
mut self,
|
||||||
button: MouseButton,
|
button: MouseButton,
|
||||||
|
@ -124,6 +125,14 @@ impl MouseRegion {
|
||||||
self
|
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 {
|
pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
|
||||||
self.hoverable = is_hoverable;
|
self.hoverable = is_hoverable;
|
||||||
self
|
self
|
||||||
|
@ -132,8 +141,32 @@ impl MouseRegion {
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||||
pub struct MouseRegionId {
|
pub struct MouseRegionId {
|
||||||
pub view_id: usize,
|
view_id: usize,
|
||||||
pub discriminant: (TypeId, 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)]
|
#[derive(Clone, Default)]
|
||||||
|
@ -345,4 +378,22 @@ impl HandlerSet {
|
||||||
}));
|
}));
|
||||||
self
|
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 {
|
pub fn is_capturable(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
MouseRegionEvent::Move(_) => true,
|
MouseRegionEvent::Move(_) => true,
|
||||||
MouseRegionEvent::Drag(_) => false,
|
MouseRegionEvent::Drag(_) => true,
|
||||||
MouseRegionEvent::Hover(_) => false,
|
MouseRegionEvent::Hover(_) => false,
|
||||||
MouseRegionEvent::Down(_) => true,
|
MouseRegionEvent::Down(_) => true,
|
||||||
MouseRegionEvent::Up(_) => true,
|
MouseRegionEvent::Up(_) => true,
|
||||||
|
|
|
@ -109,7 +109,7 @@ impl View for Select {
|
||||||
Default::default()
|
Default::default()
|
||||||
};
|
};
|
||||||
let mut result = Flex::column().with_child(
|
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)(
|
Container::new((self.render_item)(
|
||||||
self.selected_item_ix,
|
self.selected_item_ix,
|
||||||
ItemType::Header,
|
ItemType::Header,
|
||||||
|
@ -137,22 +137,18 @@ impl View for Select {
|
||||||
let selected_item_ix = this.selected_item_ix;
|
let selected_item_ix = this.selected_item_ix;
|
||||||
range.end = range.end.min(this.item_count);
|
range.end = range.end.min(this.item_count);
|
||||||
items.extend(range.map(|ix| {
|
items.extend(range.map(|ix| {
|
||||||
MouseEventHandler::new::<Item, _, _>(
|
MouseEventHandler::<Item>::new(ix, cx, |mouse_state, cx| {
|
||||||
ix,
|
(this.render_item)(
|
||||||
cx,
|
ix,
|
||||||
|mouse_state, cx| {
|
if ix == selected_item_ix {
|
||||||
(this.render_item)(
|
ItemType::Selected
|
||||||
ix,
|
} else {
|
||||||
if ix == selected_item_ix {
|
ItemType::Unselected
|
||||||
ItemType::Selected
|
},
|
||||||
} else {
|
mouse_state.hovered,
|
||||||
ItemType::Unselected
|
cx,
|
||||||
},
|
)
|
||||||
mouse_state.hovered,
|
})
|
||||||
cx,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.on_click(MouseButton::Left, move |_, cx| {
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
cx.dispatch_action(SelectItem(ix))
|
cx.dispatch_action(SelectItem(ix))
|
||||||
})
|
})
|
||||||
|
|
|
@ -85,7 +85,7 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||||
let selected_ix = delegate.read(cx).selected_index();
|
let selected_ix = delegate.read(cx).selected_index();
|
||||||
range.end = cmp::min(range.end, delegate.read(cx).match_count());
|
range.end = cmp::min(range.end, delegate.read(cx).match_count());
|
||||||
items.extend(range.map(move |ix| {
|
items.extend(range.map(move |ix| {
|
||||||
MouseEventHandler::new::<D, _, _>(ix, cx, |state, cx| {
|
MouseEventHandler::<D>::new(ix, cx, |state, cx| {
|
||||||
delegate
|
delegate
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.render_match(ix, state, ix == selected_ix, cx)
|
.render_match(ix, state, ix == selected_ix, cx)
|
||||||
|
|
|
@ -5,8 +5,8 @@ use gpui::{
|
||||||
actions,
|
actions,
|
||||||
anyhow::{anyhow, Result},
|
anyhow::{anyhow, Result},
|
||||||
elements::{
|
elements::{
|
||||||
ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement,
|
AnchorCorner, ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler,
|
||||||
ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||||
},
|
},
|
||||||
geometry::vector::Vector2F,
|
geometry::vector::Vector2F,
|
||||||
impl_internal_actions, keymap,
|
impl_internal_actions, keymap,
|
||||||
|
@ -302,7 +302,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.context_menu.update(cx, |menu, cx| {
|
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();
|
cx.notify();
|
||||||
|
@ -1012,7 +1012,7 @@ impl ProjectPanel {
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
let kind = details.kind;
|
let kind = details.kind;
|
||||||
let show_editor = details.is_editing && !details.is_processing;
|
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 padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
|
||||||
let mut style = theme.entry.style_for(state, details.is_selected).clone();
|
let mut style = theme.entry.style_for(state, details.is_selected).clone();
|
||||||
if details.is_ignored {
|
if details.is_ignored {
|
||||||
|
@ -1107,7 +1107,7 @@ impl View for ProjectPanel {
|
||||||
let last_worktree_root_id = self.last_worktree_root_id;
|
let last_worktree_root_id = self.last_worktree_root_id;
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::new::<Tag, _, _>(0, cx, |_, cx| {
|
MouseEventHandler::<Tag>::new(0, cx, |_, cx| {
|
||||||
UniformList::new(
|
UniformList::new(
|
||||||
self.list.clone(),
|
self.list.clone(),
|
||||||
self.visible_entries
|
self.visible_entries
|
||||||
|
@ -1243,7 +1243,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).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));
|
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
visible_entries_as_strings(&panel, 0..50, cx),
|
visible_entries_as_strings(&panel, 0..50, cx),
|
||||||
|
@ -1335,7 +1336,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).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));
|
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
||||||
|
|
||||||
select_path(&panel, "root1", cx);
|
select_path(&panel, "root1", cx);
|
||||||
|
|
|
@ -319,7 +319,7 @@ impl BufferSearchBar {
|
||||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||||
let is_active = self.is_search_option_enabled(option);
|
let is_active = self.is_search_option_enabled(option);
|
||||||
Some(
|
Some(
|
||||||
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
|
MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
|
||||||
let style = &cx
|
let style = &cx
|
||||||
.global::<Settings>()
|
.global::<Settings>()
|
||||||
.theme
|
.theme
|
||||||
|
@ -367,7 +367,7 @@ impl BufferSearchBar {
|
||||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||||
|
|
||||||
enum NavButton {}
|
enum NavButton {}
|
||||||
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
|
MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
|
||||||
let style = &cx
|
let style = &cx
|
||||||
.global::<Settings>()
|
.global::<Settings>()
|
||||||
.theme
|
.theme
|
||||||
|
|
|
@ -176,7 +176,7 @@ impl View for ProjectSearchView {
|
||||||
} else {
|
} else {
|
||||||
"No results"
|
"No results"
|
||||||
};
|
};
|
||||||
MouseEventHandler::new::<Status, _, _>(0, cx, |_, _| {
|
MouseEventHandler::<Status>::new(0, cx, |_, _| {
|
||||||
Label::new(text.to_string(), theme.search.results_status.clone())
|
Label::new(text.to_string(), theme.search.results_status.clone())
|
||||||
.aligned()
|
.aligned()
|
||||||
.contained()
|
.contained()
|
||||||
|
@ -723,7 +723,7 @@ impl ProjectSearchBar {
|
||||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||||
|
|
||||||
enum NavButton {}
|
enum NavButton {}
|
||||||
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
|
MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
|
||||||
let style = &cx
|
let style = &cx
|
||||||
.global::<Settings>()
|
.global::<Settings>()
|
||||||
.theme
|
.theme
|
||||||
|
@ -758,7 +758,7 @@ impl ProjectSearchBar {
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||||
let is_active = self.is_option_enabled(option, cx);
|
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
|
let style = &cx
|
||||||
.global::<Settings>()
|
.global::<Settings>()
|
||||||
.theme
|
.theme
|
||||||
|
|
|
@ -29,6 +29,7 @@ pub struct Settings {
|
||||||
pub show_completions_on_input: bool,
|
pub show_completions_on_input: bool,
|
||||||
pub vim_mode: bool,
|
pub vim_mode: bool,
|
||||||
pub autosave: Autosave,
|
pub autosave: Autosave,
|
||||||
|
pub default_dock_anchor: DockAnchor,
|
||||||
pub editor_defaults: EditorSettings,
|
pub editor_defaults: EditorSettings,
|
||||||
pub editor_overrides: EditorSettings,
|
pub editor_overrides: EditorSettings,
|
||||||
pub terminal_defaults: TerminalSettings,
|
pub terminal_defaults: TerminalSettings,
|
||||||
|
@ -151,6 +152,15 @@ pub enum WorkingDirectory {
|
||||||
Always { directory: String },
|
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)]
|
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||||
pub struct SettingsFileContent {
|
pub struct SettingsFileContent {
|
||||||
pub experiments: Option<FeatureFlags>,
|
pub experiments: Option<FeatureFlags>,
|
||||||
|
@ -168,6 +178,8 @@ pub struct SettingsFileContent {
|
||||||
pub vim_mode: Option<bool>,
|
pub vim_mode: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub autosave: Option<Autosave>,
|
pub autosave: Option<Autosave>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub default_dock_anchor: Option<DockAnchor>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub editor: EditorSettings,
|
pub editor: EditorSettings,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -217,6 +229,7 @@ impl Settings {
|
||||||
projects_online_by_default: defaults.projects_online_by_default.unwrap(),
|
projects_online_by_default: defaults.projects_online_by_default.unwrap(),
|
||||||
vim_mode: defaults.vim_mode.unwrap(),
|
vim_mode: defaults.vim_mode.unwrap(),
|
||||||
autosave: defaults.autosave.unwrap(),
|
autosave: defaults.autosave.unwrap(),
|
||||||
|
default_dock_anchor: defaults.default_dock_anchor.unwrap(),
|
||||||
editor_defaults: EditorSettings {
|
editor_defaults: EditorSettings {
|
||||||
tab_size: required(defaults.editor.tab_size),
|
tab_size: required(defaults.editor.tab_size),
|
||||||
hard_tabs: required(defaults.editor.hard_tabs),
|
hard_tabs: required(defaults.editor.hard_tabs),
|
||||||
|
@ -269,6 +282,8 @@ impl Settings {
|
||||||
merge(&mut self.autosave, data.autosave);
|
merge(&mut self.autosave, data.autosave);
|
||||||
merge(&mut self.experiments, data.experiments);
|
merge(&mut self.experiments, data.experiments);
|
||||||
merge(&mut self.staff_mode, data.staff_mode);
|
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
|
// Ensure terminal font is loaded, so we can request it in terminal_element layout
|
||||||
if let Some(terminal_font) = &data.terminal.font_family {
|
if let Some(terminal_font) = &data.terminal.font_family {
|
||||||
font_cache.load_family(&[terminal_font]).log_err();
|
font_cache.load_family(&[terminal_font]).log_err();
|
||||||
|
@ -337,6 +352,7 @@ impl Settings {
|
||||||
show_completions_on_input: true,
|
show_completions_on_input: true,
|
||||||
vim_mode: false,
|
vim_mode: false,
|
||||||
autosave: Autosave::Off,
|
autosave: Autosave::Off,
|
||||||
|
default_dock_anchor: DockAnchor::Bottom,
|
||||||
editor_defaults: EditorSettings {
|
editor_defaults: EditorSettings {
|
||||||
tab_size: Some(4.try_into().unwrap()),
|
tab_size: Some(4.try_into().unwrap()),
|
||||||
hard_tabs: Some(false),
|
hard_tabs: Some(false),
|
||||||
|
|
|
@ -6,6 +6,7 @@ use alacritty_terminal::grid::Dimensions;
|
||||||
/// with modifications for our circumstances
|
/// with modifications for our circumstances
|
||||||
use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point, Side};
|
use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point, Side};
|
||||||
use alacritty_terminal::term::TermMode;
|
use alacritty_terminal::term::TermMode;
|
||||||
|
use gpui::scene::ScrollWheelRegionEvent;
|
||||||
use gpui::{geometry::vector::Vector2F, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
|
use gpui::{geometry::vector::Vector2F, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
|
||||||
|
|
||||||
use crate::TerminalSize;
|
use crate::TerminalSize;
|
||||||
|
@ -114,7 +115,7 @@ impl MouseButton {
|
||||||
pub fn scroll_report(
|
pub fn scroll_report(
|
||||||
point: Point,
|
point: Point,
|
||||||
scroll_lines: i32,
|
scroll_lines: i32,
|
||||||
e: &ScrollWheelEvent,
|
e: &ScrollWheelRegionEvent,
|
||||||
mode: TermMode,
|
mode: TermMode,
|
||||||
) -> Option<impl Iterator<Item = Vec<u8>>> {
|
) -> Option<impl Iterator<Item = Vec<u8>>> {
|
||||||
if mode.intersects(TermMode::MOUSE_MODE) {
|
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 mappings;
|
||||||
pub mod modal;
|
|
||||||
pub mod terminal_container_view;
|
pub mod terminal_container_view;
|
||||||
pub mod terminal_element;
|
pub mod terminal_element;
|
||||||
pub mod terminal_view;
|
pub mod terminal_view;
|
||||||
|
@ -32,7 +31,6 @@ use futures::{
|
||||||
use mappings::mouse::{
|
use mappings::mouse::{
|
||||||
alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
|
alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
|
||||||
};
|
};
|
||||||
use modal::deploy_modal;
|
|
||||||
|
|
||||||
use procinfo::LocalProcessInfo;
|
use procinfo::LocalProcessInfo;
|
||||||
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
|
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
|
||||||
|
@ -51,9 +49,10 @@ use thiserror::Error;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
geometry::vector::{vec2f, Vector2F},
|
geometry::vector::{vec2f, Vector2F},
|
||||||
keymap::Keystroke,
|
keymap::Keystroke,
|
||||||
scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
|
scene::{
|
||||||
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
|
ClickRegionEvent, DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent,
|
||||||
ScrollWheelEvent, Task,
|
},
|
||||||
|
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::mappings::{
|
use crate::mappings::{
|
||||||
|
@ -63,8 +62,6 @@ use crate::mappings::{
|
||||||
|
|
||||||
///Initialize and register all of our action handlers
|
///Initialize and register all of our action handlers
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(deploy_modal);
|
|
||||||
|
|
||||||
terminal_view::init(cx);
|
terminal_view::init(cx);
|
||||||
terminal_container_view::init(cx);
|
terminal_container_view::init(cx);
|
||||||
}
|
}
|
||||||
|
@ -908,10 +905,10 @@ impl Terminal {
|
||||||
}
|
}
|
||||||
|
|
||||||
///Scroll the 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);
|
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 {
|
if mouse_mode {
|
||||||
let point = mouse_point(
|
let point = mouse_point(
|
||||||
e.position.sub(origin),
|
e.position.sub(origin),
|
||||||
|
@ -920,7 +917,7 @@ impl Terminal {
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(scrolls) =
|
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 {
|
for scroll in scrolls {
|
||||||
self.pty_tx.notify(scroll);
|
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 };
|
let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
|
||||||
|
|
||||||
match e.phase {
|
match e.phase {
|
||||||
|
|
|
@ -366,7 +366,7 @@ impl TerminalElement {
|
||||||
) {
|
) {
|
||||||
let connection = self.terminal;
|
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:
|
// Terminal Emulator controlled behavior:
|
||||||
region = region
|
region = region
|
||||||
|
@ -427,7 +427,14 @@ impl TerminalElement {
|
||||||
position: e.position,
|
position: e.position,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.on_scroll(TerminalElement::generic_button_handler(
|
||||||
|
connection,
|
||||||
|
origin,
|
||||||
|
move |terminal, origin, e, _cx| {
|
||||||
|
terminal.scroll_wheel(e, origin);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
// Mouse mode handlers:
|
// Mouse mode handlers:
|
||||||
// All mouse modes need the extra click handlers
|
// All mouse modes need the extra click handlers
|
||||||
|
@ -742,24 +749,13 @@ impl Element for TerminalElement {
|
||||||
fn dispatch_event(
|
fn dispatch_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &gpui::Event,
|
event: &gpui::Event,
|
||||||
bounds: gpui::geometry::rect::RectF,
|
_bounds: gpui::geometry::rect::RectF,
|
||||||
visible_bounds: gpui::geometry::rect::RectF,
|
_visible_bounds: gpui::geometry::rect::RectF,
|
||||||
layout: &mut Self::LayoutState,
|
_layout: &mut Self::LayoutState,
|
||||||
_paint: &mut Self::PaintState,
|
_paint: &mut Self::PaintState,
|
||||||
cx: &mut gpui::EventContext,
|
cx: &mut gpui::EventContext,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match event {
|
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, .. }) => {
|
Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
|
||||||
if !cx.is_parent_view_focused() {
|
if !cx.is_parent_view_focused() {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -4,7 +4,7 @@ use alacritty_terminal::{index::Point, term::TermMode};
|
||||||
use context_menu::{ContextMenu, ContextMenuItem};
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::{ChildView, ParentElement, Stack},
|
elements::{AnchorCorner, ChildView, ParentElement, Stack},
|
||||||
geometry::vector::Vector2F,
|
geometry::vector::Vector2F,
|
||||||
impl_internal_actions,
|
impl_internal_actions,
|
||||||
keymap::Keystroke,
|
keymap::Keystroke,
|
||||||
|
@ -139,8 +139,9 @@ impl TerminalView {
|
||||||
ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
|
ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
|
||||||
];
|
];
|
||||||
|
|
||||||
self.context_menu
|
self.context_menu.update(cx, |menu, cx| {
|
||||||
.update(cx, |menu, cx| menu.show(action.position, menu_entries, cx));
|
menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx)
|
||||||
|
});
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,9 @@ impl<'a> TerminalTestContext<'a> {
|
||||||
let params = self.cx.update(AppState::test);
|
let params = self.cx.update(AppState::test);
|
||||||
|
|
||||||
let project = Project::test(params.fs.clone(), [], self.cx).await;
|
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)
|
(project, workspace)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ pub struct Workspace {
|
||||||
pub pane_divider: Border,
|
pub pane_divider: Border,
|
||||||
pub leader_border_opacity: f32,
|
pub leader_border_opacity: f32,
|
||||||
pub leader_border_width: f32,
|
pub leader_border_width: f32,
|
||||||
pub sidebar_resize_handle: ContainerStyle,
|
pub sidebar: Sidebar,
|
||||||
pub status_bar: StatusBar,
|
pub status_bar: StatusBar,
|
||||||
pub toolbar: Toolbar,
|
pub toolbar: Toolbar,
|
||||||
pub disconnected_overlay: ContainedText,
|
pub disconnected_overlay: ContainedText,
|
||||||
|
@ -57,6 +57,7 @@ pub struct Workspace {
|
||||||
pub notifications: Notifications,
|
pub notifications: Notifications,
|
||||||
pub joining_project_avatar: ImageStyle,
|
pub joining_project_avatar: ImageStyle,
|
||||||
pub joining_project_message: ContainedText,
|
pub joining_project_message: ContainedText,
|
||||||
|
pub dock: Dock,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
@ -149,6 +150,16 @@ pub struct Toolbar {
|
||||||
pub nav_button: Interactive<IconButton>,
|
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)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
pub struct Notifications {
|
pub struct Notifications {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
|
@ -231,7 +242,9 @@ pub struct StatusBarLspStatus {
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
#[derive(Deserialize, Default)]
|
||||||
pub struct Sidebar {
|
pub struct Sidebar {
|
||||||
pub resize_handle: ContainerStyle,
|
pub initial_size: f32,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub container: ContainerStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Deserialize, Default)]
|
#[derive(Clone, Copy, Deserialize, Default)]
|
||||||
|
@ -558,6 +571,7 @@ pub struct CodeActions {
|
||||||
pub struct Interactive<T> {
|
pub struct Interactive<T> {
|
||||||
pub default: T,
|
pub default: T,
|
||||||
pub hover: Option<T>,
|
pub hover: Option<T>,
|
||||||
|
pub clicked: Option<T>,
|
||||||
pub active: Option<T>,
|
pub active: Option<T>,
|
||||||
pub disabled: Option<T>,
|
pub disabled: Option<T>,
|
||||||
}
|
}
|
||||||
|
@ -566,6 +580,8 @@ impl<T> Interactive<T> {
|
||||||
pub fn style_for(&self, state: MouseState, active: bool) -> &T {
|
pub fn style_for(&self, state: MouseState, active: bool) -> &T {
|
||||||
if active {
|
if active {
|
||||||
self.active.as_ref().unwrap_or(&self.default)
|
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 {
|
} else if state.hovered {
|
||||||
self.hover.as_ref().unwrap_or(&self.default)
|
self.hover.as_ref().unwrap_or(&self.default)
|
||||||
} else {
|
} else {
|
||||||
|
@ -588,6 +604,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
default: Value,
|
default: Value,
|
||||||
hover: Option<Value>,
|
hover: Option<Value>,
|
||||||
|
clicked: Option<Value>,
|
||||||
active: Option<Value>,
|
active: Option<Value>,
|
||||||
disabled: Option<Value>,
|
disabled: Option<Value>,
|
||||||
}
|
}
|
||||||
|
@ -614,6 +631,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let hover = deserialize_state(json.hover)?;
|
let hover = deserialize_state(json.hover)?;
|
||||||
|
let clicked = deserialize_state(json.clicked)?;
|
||||||
let active = deserialize_state(json.active)?;
|
let active = deserialize_state(json.active)?;
|
||||||
let disabled = deserialize_state(json.disabled)?;
|
let disabled = deserialize_state(json.disabled)?;
|
||||||
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
|
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
|
||||||
|
@ -621,6 +639,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
||||||
Ok(Interactive {
|
Ok(Interactive {
|
||||||
default,
|
default,
|
||||||
hover,
|
hover,
|
||||||
|
clicked,
|
||||||
active,
|
active,
|
||||||
disabled,
|
disabled,
|
||||||
})
|
})
|
||||||
|
|
|
@ -39,7 +39,8 @@ impl<'a> VimTestContext<'a> {
|
||||||
.insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
|
.insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
|
||||||
.await;
|
.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
|
// Setup search toolbars
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
|
693
crates/workspace/src/dock.rs
Normal file
693
crates/workspace/src/dock.rs
Normal file
|
@ -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 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 anyhow::Result;
|
||||||
use collections::{HashMap, HashSet, VecDeque};
|
use collections::{HashMap, HashSet, VecDeque};
|
||||||
use context_menu::{ContextMenu, ContextMenuItem};
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
|
@ -15,13 +19,13 @@ use gpui::{
|
||||||
},
|
},
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
platform::{CursorStyle, NavigationDirection},
|
platform::{CursorStyle, NavigationDirection},
|
||||||
AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
|
Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
|
||||||
ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
|
ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
|
||||||
ViewContext, ViewHandle, WeakViewHandle,
|
ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use project::{Project, ProjectEntryId, ProjectPath};
|
use project::{Project, ProjectEntryId, ProjectPath};
|
||||||
use serde::Deserialize;
|
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 std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
|
||||||
use theme::Theme;
|
use theme::Theme;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
@ -76,13 +80,27 @@ pub struct DeploySplitMenu {
|
||||||
position: Vector2F,
|
position: Vector2F,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct DeployDockMenu {
|
||||||
|
position: Vector2F,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct DeployNewMenu {
|
pub struct DeployNewMenu {
|
||||||
position: Vector2F,
|
position: Vector2F,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
|
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;
|
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: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
|
||||||
cx.add_action(Pane::deploy_split_menu);
|
cx.add_action(Pane::deploy_split_menu);
|
||||||
cx.add_action(Pane::deploy_new_menu);
|
cx.add_action(Pane::deploy_new_menu);
|
||||||
|
cx.add_action(Pane::deploy_dock_menu);
|
||||||
cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
|
cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
|
||||||
Pane::reopen_closed_item(workspace, cx).detach();
|
Pane::reopen_closed_item(workspace, cx).detach();
|
||||||
});
|
});
|
||||||
|
@ -168,6 +187,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Focused,
|
Focused,
|
||||||
ActivateItem { local: bool },
|
ActivateItem { local: bool },
|
||||||
|
@ -185,7 +205,8 @@ pub struct Pane {
|
||||||
autoscroll: bool,
|
autoscroll: bool,
|
||||||
nav_history: Rc<RefCell<NavHistory>>,
|
nav_history: Rc<RefCell<NavHistory>>,
|
||||||
toolbar: ViewHandle<Toolbar>,
|
toolbar: ViewHandle<Toolbar>,
|
||||||
context_menu: ViewHandle<ContextMenu>,
|
tab_bar_context_menu: ViewHandle<ContextMenu>,
|
||||||
|
docked: Option<DockAnchor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ItemNavHistory {
|
pub struct ItemNavHistory {
|
||||||
|
@ -235,7 +256,7 @@ pub enum ReorderBehavior {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pane {
|
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 handle = cx.weak_handle();
|
||||||
let context_menu = cx.add_view(ContextMenu::new);
|
let context_menu = cx.add_view(ContextMenu::new);
|
||||||
Self {
|
Self {
|
||||||
|
@ -253,15 +274,25 @@ impl Pane {
|
||||||
pane: handle.clone(),
|
pane: handle.clone(),
|
||||||
})),
|
})),
|
||||||
toolbar: cx.add_view(|_| Toolbar::new(handle)),
|
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>) {
|
pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
|
||||||
self.is_active = is_active;
|
self.is_active = is_active;
|
||||||
cx.notify();
|
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 {
|
pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
|
||||||
ItemNavHistory {
|
ItemNavHistory {
|
||||||
history: self.nav_history.clone(),
|
history: self.nav_history.clone(),
|
||||||
|
@ -675,7 +706,7 @@ impl Pane {
|
||||||
pane: ViewHandle<Pane>,
|
pane: ViewHandle<Pane>,
|
||||||
item_id_to_close: usize,
|
item_id_to_close: usize,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) -> Task<Result<bool>> {
|
) -> Task<Result<()>> {
|
||||||
Self::close_items(workspace, pane, cx, move |view_id| {
|
Self::close_items(workspace, pane, cx, move |view_id| {
|
||||||
view_id == item_id_to_close
|
view_id == item_id_to_close
|
||||||
})
|
})
|
||||||
|
@ -686,7 +717,7 @@ impl Pane {
|
||||||
pane: ViewHandle<Pane>,
|
pane: ViewHandle<Pane>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
should_close: impl 'static + Fn(usize) -> bool,
|
should_close: impl 'static + Fn(usize) -> bool,
|
||||||
) -> Task<Result<bool>> {
|
) -> Task<Result<()>> {
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
|
|
||||||
// Find the items to close.
|
// Find the items to close.
|
||||||
|
@ -759,7 +790,7 @@ impl Pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
pane.update(&mut cx, |_, cx| cx.notify());
|
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>) {
|
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(
|
menu.show(
|
||||||
action.position,
|
action.position,
|
||||||
|
AnchorCorner::TopRight,
|
||||||
vec![
|
vec![
|
||||||
ContextMenuItem::item("Split Right", SplitRight),
|
ContextMenuItem::item("Split Right", SplitRight),
|
||||||
ContextMenuItem::item("Split Left", SplitLeft),
|
ContextMenuItem::item("Split Left", SplitLeft),
|
||||||
|
@ -976,10 +1008,26 @@ impl Pane {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
|
fn deploy_dock_menu(&mut self, action: &DeployDockMenu, cx: &mut ViewContext<Self>) {
|
||||||
self.context_menu.update(cx, |menu, cx| {
|
self.tab_bar_context_menu.update(cx, |menu, cx| {
|
||||||
menu.show(
|
menu.show(
|
||||||
action.position,
|
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.tab_bar_context_menu.update(cx, |menu, cx| {
|
||||||
|
menu.show(
|
||||||
|
action.position,
|
||||||
|
AnchorCorner::TopRight,
|
||||||
vec![
|
vec![
|
||||||
ContextMenuItem::item("New File", NewFile),
|
ContextMenuItem::item("New File", NewFile),
|
||||||
ContextMenuItem::item("New Terminal", NewTerminal),
|
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 theme = cx.global::<Settings>().theme.clone();
|
||||||
let filler_index = self.items.len();
|
let filler_index = self.items.len();
|
||||||
|
|
||||||
|
@ -1012,7 +1060,7 @@ impl Pane {
|
||||||
enum Tab {}
|
enum Tab {}
|
||||||
enum Filler {}
|
enum Filler {}
|
||||||
let pane = cx.handle();
|
let pane = cx.handle();
|
||||||
MouseEventHandler::new::<Tabs, _, _>(0, cx, |_, cx| {
|
MouseEventHandler::<Tabs>::new(0, cx, |_, cx| {
|
||||||
let autoscroll = if mem::take(&mut self.autoscroll) {
|
let autoscroll = if mem::take(&mut self.autoscroll) {
|
||||||
Some(self.active_item_index)
|
Some(self.active_item_index)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1033,7 +1081,7 @@ impl Pane {
|
||||||
let tab_active = ix == self.active_item_index;
|
let tab_active = ix == self.active_item_index;
|
||||||
|
|
||||||
row.add_child({
|
row.add_child({
|
||||||
MouseEventHandler::new::<Tab, _, _>(ix, cx, {
|
MouseEventHandler::<Tab>::new(ix, cx, {
|
||||||
let item = item.clone();
|
let item = item.clone();
|
||||||
let pane = pane.clone();
|
let pane = pane.clone();
|
||||||
let detail = detail.clone();
|
let detail = detail.clone();
|
||||||
|
@ -1108,7 +1156,7 @@ impl Pane {
|
||||||
// the filler
|
// the filler
|
||||||
let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
|
let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
|
||||||
row.add_child(
|
row.add_child(
|
||||||
MouseEventHandler::new::<Filler, _, _>(0, cx, |mouse_state, cx| {
|
MouseEventHandler::<Filler>::new(0, cx, |mouse_state, cx| {
|
||||||
let mut filler = Empty::new()
|
let mut filler = Empty::new()
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(filler_style.container)
|
.with_style(filler_style.container)
|
||||||
|
@ -1230,17 +1278,13 @@ impl Pane {
|
||||||
let item_id = item.id();
|
let item_id = item.id();
|
||||||
enum TabCloseButton {}
|
enum TabCloseButton {}
|
||||||
let icon = Svg::new("icons/x_mark_thin_8.svg");
|
let icon = Svg::new("icons/x_mark_thin_8.svg");
|
||||||
MouseEventHandler::new::<TabCloseButton, _, _>(
|
MouseEventHandler::<TabCloseButton>::new(item_id, cx, |mouse_state, _| {
|
||||||
item_id,
|
if mouse_state.hovered {
|
||||||
cx,
|
icon.with_color(tab_style.icon_close_active).boxed()
|
||||||
|mouse_state, _| {
|
} else {
|
||||||
if mouse_state.hovered {
|
icon.with_color(tab_style.icon_close).boxed()
|
||||||
icon.with_color(tab_style.icon_close_active).boxed()
|
}
|
||||||
} else {
|
})
|
||||||
icon.with_color(tab_style.icon_close).boxed()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.with_padding(Padding::uniform(4.))
|
.with_padding(Padding::uniform(4.))
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, {
|
.on_click(MouseButton::Left, {
|
||||||
|
@ -1316,120 +1360,121 @@ impl View for Pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
enum SplitIcon {}
|
|
||||||
|
|
||||||
let this = cx.handle();
|
let this = cx.handle();
|
||||||
|
|
||||||
|
enum MouseNavigationHandler {}
|
||||||
|
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(
|
.with_child(
|
||||||
EventHandler::new(if let Some(active_item) = self.active_item() {
|
MouseEventHandler::<MouseNavigationHandler>::new(0, cx, |_, cx| {
|
||||||
Flex::column()
|
if let Some(active_item) = self.active_item() {
|
||||||
.with_child({
|
Flex::column()
|
||||||
let mut tab_row = Flex::row()
|
.with_child({
|
||||||
.with_child(self.render_tab_bar(cx).flex(1., true).named("tabs"));
|
let mut tab_row = Flex::row()
|
||||||
|
.with_child(self.render_tabs(cx).flex(1.0, true).named("tabs"));
|
||||||
|
|
||||||
if self.is_active {
|
// Render pane buttons
|
||||||
tab_row.add_children([
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
MouseEventHandler::new::<SplitIcon, _, _>(
|
if self.is_active {
|
||||||
0,
|
tab_row.add_child(
|
||||||
cx,
|
Flex::row()
|
||||||
|mouse_state, cx| {
|
// New menu
|
||||||
let theme =
|
.with_child(tab_bar_button(
|
||||||
&cx.global::<Settings>().theme.workspace.tab_bar;
|
0,
|
||||||
let style =
|
"icons/plus_12.svg",
|
||||||
theme.pane_button.style_for(mouse_state, false);
|
cx,
|
||||||
Svg::new("icons/plus_12.svg")
|
|position| DeployNewMenu { position },
|
||||||
.with_color(style.color)
|
))
|
||||||
.constrained()
|
.with_child(
|
||||||
.with_width(style.icon_width)
|
self.docked
|
||||||
.aligned()
|
.map(|anchor| {
|
||||||
.contained()
|
// Add the dock menu button if this pane is a dock
|
||||||
.with_style(style.container)
|
let dock_icon =
|
||||||
.constrained()
|
icon_for_dock_anchor(anchor);
|
||||||
.with_width(style.button_width)
|
|
||||||
.with_height(style.button_width)
|
tab_bar_button(
|
||||||
.aligned()
|
1,
|
||||||
.boxed()
|
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(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()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
|
||||||
.on_down(MouseButton::Left, |e, cx| {
|
|
||||||
cx.dispatch_action(DeploySplitMenu {
|
|
||||||
position: e.position,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.boxed(),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
tab_row
|
tab_row
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(cx.global::<Settings>().theme.workspace.tab_bar.height)
|
.with_height(theme.workspace.tab_bar.height)
|
||||||
.named("tab bar")
|
.contained()
|
||||||
})
|
.with_style(theme.workspace.tab_bar.container)
|
||||||
.with_child(ChildView::new(&self.toolbar).boxed())
|
.flex(1., false)
|
||||||
.with_child(ChildView::new(active_item).flex(1., true).boxed())
|
.named("tab bar")
|
||||||
.boxed()
|
})
|
||||||
} else {
|
.with_child(ChildView::new(&self.toolbar).expanded().boxed())
|
||||||
enum EmptyPane {}
|
.with_child(ChildView::new(active_item).flex(1., true).boxed())
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
|
||||||
|
|
||||||
MouseEventHandler::new::<EmptyPane, _, _>(0, cx, |_, _| {
|
|
||||||
Empty::new()
|
|
||||||
.contained()
|
|
||||||
.with_background_color(theme.workspace.background)
|
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
} else {
|
||||||
.on_down(MouseButton::Left, |_, cx| {
|
enum EmptyPane {}
|
||||||
cx.focus_parent_view();
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
})
|
|
||||||
.on_navigate_mouse_down(move |direction, cx| {
|
|
||||||
let this = this.clone();
|
|
||||||
match direction {
|
|
||||||
NavigationDirection::Back => {
|
|
||||||
cx.dispatch_action(GoBack { pane: Some(this) })
|
|
||||||
}
|
|
||||||
NavigationDirection::Forward => {
|
|
||||||
cx.dispatch_action(GoForward { pane: Some(this) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
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)
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_down(MouseButton::Navigate(NavigationDirection::Back), {
|
||||||
|
let this = this.clone();
|
||||||
|
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()),
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_child(ChildView::new(&self.context_menu).boxed())
|
.with_child(ChildView::new(&self.tab_bar_context_menu).boxed())
|
||||||
.named("pane")
|
.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 {
|
impl ItemNavHistory {
|
||||||
pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
|
pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
|
||||||
self.history.borrow_mut().push(data, self.item.clone(), cx);
|
self.history.borrow_mut().push(data, self.item.clone(), cx);
|
||||||
|
@ -1566,7 +1641,8 @@ mod tests {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
let project = Project::test(fs, None, cx).await;
|
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());
|
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
|
||||||
// 1. Add with a destination index
|
// 1. Add with a destination index
|
||||||
|
@ -1654,7 +1730,8 @@ mod tests {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
let project = Project::test(fs, None, cx).await;
|
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());
|
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
|
||||||
// 1. Add with a destination index
|
// 1. Add with a destination index
|
||||||
|
@ -1730,7 +1807,8 @@ mod tests {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
let project = Project::test(fs, None, cx).await;
|
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());
|
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
|
||||||
// singleton view
|
// 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> {
|
pub fn remove(&mut self, pane: &ViewHandle<Pane>) -> Result<bool> {
|
||||||
match &mut self.root {
|
match &mut self.root {
|
||||||
Member::Pane(_) => Ok(false),
|
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 serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::rc::Rc;
|
||||||
use theme::Theme;
|
|
||||||
|
|
||||||
pub trait SidebarItem: View {
|
pub trait SidebarItem: View {
|
||||||
fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
|
fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
fn should_show_badge(&self, cx: &AppContext) -> bool;
|
fn should_show_badge(&self, _: &AppContext) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
fn contains_focused_view(&self, _: &AppContext) -> bool {
|
fn contains_focused_view(&self, _: &AppContext) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -53,20 +54,27 @@ impl From<&dyn SidebarItemHandle> for AnyViewHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Sidebar {
|
pub struct Sidebar {
|
||||||
side: Side,
|
sidebar_side: SidebarSide,
|
||||||
items: Vec<Item>,
|
items: Vec<Item>,
|
||||||
is_open: bool,
|
is_open: bool,
|
||||||
active_item_ix: usize,
|
active_item_ix: usize,
|
||||||
actual_width: Rc<RefCell<f32>>,
|
|
||||||
custom_width: Rc<RefCell<f32>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
|
||||||
pub enum Side {
|
pub enum SidebarSide {
|
||||||
Left,
|
Left,
|
||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SidebarSide {
|
||||||
|
fn to_resizable_side(self) -> Side {
|
||||||
|
match self {
|
||||||
|
Self::Left => Side::Right,
|
||||||
|
Self::Right => Side::Left,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Item {
|
struct Item {
|
||||||
icon_path: &'static str,
|
icon_path: &'static str,
|
||||||
tooltip: String,
|
tooltip: String,
|
||||||
|
@ -80,21 +88,19 @@ pub struct SidebarButtons {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
pub struct ToggleSidebarItem {
|
pub struct ToggleSidebarItem {
|
||||||
pub side: Side,
|
pub sidebar_side: SidebarSide,
|
||||||
pub item_index: usize,
|
pub item_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_actions!(workspace, [ToggleSidebarItem]);
|
impl_actions!(workspace, [ToggleSidebarItem]);
|
||||||
|
|
||||||
impl Sidebar {
|
impl Sidebar {
|
||||||
pub fn new(side: Side) -> Self {
|
pub fn new(sidebar_side: SidebarSide) -> Self {
|
||||||
Self {
|
Self {
|
||||||
side,
|
sidebar_side,
|
||||||
items: Default::default(),
|
items: Default::default(),
|
||||||
active_item_ix: 0,
|
active_item_ix: 0,
|
||||||
is_open: false,
|
is_open: false,
|
||||||
actual_width: Rc::new(RefCell::new(260.)),
|
|
||||||
custom_width: Rc::new(RefCell::new(260.)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,38 +177,6 @@ impl Sidebar {
|
||||||
None
|
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 {
|
impl Entity for Sidebar {
|
||||||
|
@ -215,31 +189,20 @@ impl View for Sidebar {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
|
||||||
if let Some(active_item) = self.active_item() {
|
if let Some(active_item) = self.active_item() {
|
||||||
let mut container = Flex::row();
|
enum ResizeHandleTag {}
|
||||||
if matches!(self.side, Side::Right) {
|
let style = &cx.global::<Settings>().theme.workspace.sidebar;
|
||||||
container.add_child(self.render_resize_handle(&theme, cx));
|
ChildView::new(active_item.to_any())
|
||||||
}
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
container.add_child(
|
.with_resize_handle::<ResizeHandleTag, _>(
|
||||||
Hook::new(
|
self.sidebar_side as usize,
|
||||||
ChildView::new(active_item.to_any())
|
self.sidebar_side.to_resizable_side(),
|
||||||
.constrained()
|
4.,
|
||||||
.with_max_width(*self.custom_width.borrow())
|
style.initial_size,
|
||||||
.boxed(),
|
cx,
|
||||||
)
|
)
|
||||||
.on_after_layout({
|
.boxed()
|
||||||
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()
|
|
||||||
} else {
|
} else {
|
||||||
Empty::new().boxed()
|
Empty::new().boxed()
|
||||||
}
|
}
|
||||||
|
@ -271,10 +234,10 @@ impl View for SidebarButtons {
|
||||||
let badge_style = theme.badge;
|
let badge_style = theme.badge;
|
||||||
let active_ix = sidebar.active_item_ix;
|
let active_ix = sidebar.active_item_ix;
|
||||||
let is_open = sidebar.is_open;
|
let is_open = sidebar.is_open;
|
||||||
let side = sidebar.side;
|
let sidebar_side = sidebar.sidebar_side;
|
||||||
let group_style = match side {
|
let group_style = match sidebar_side {
|
||||||
Side::Left => theme.group_left,
|
SidebarSide::Left => theme.group_left,
|
||||||
Side::Right => theme.group_right,
|
SidebarSide::Right => theme.group_right,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(clippy::needless_collect)]
|
#[allow(clippy::needless_collect)]
|
||||||
|
@ -288,10 +251,10 @@ impl View for SidebarButtons {
|
||||||
.with_children(items.into_iter().enumerate().map(
|
.with_children(items.into_iter().enumerate().map(
|
||||||
|(ix, (icon_path, tooltip, item_view))| {
|
|(ix, (icon_path, tooltip, item_view))| {
|
||||||
let action = ToggleSidebarItem {
|
let action = ToggleSidebarItem {
|
||||||
side,
|
sidebar_side,
|
||||||
item_index: ix,
|
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 is_active = is_open && ix == active_ix;
|
||||||
let style = item_style.style_for(state, is_active);
|
let style = item_style.style_for(state, is_active);
|
||||||
Stack::new()
|
Stack::new()
|
||||||
|
|
|
@ -166,7 +166,7 @@ fn nav_button<A: Action + Clone>(
|
||||||
action_name: &str,
|
action_name: &str,
|
||||||
cx: &mut RenderContext<Toolbar>,
|
cx: &mut RenderContext<Toolbar>,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
MouseEventHandler::new::<A, _, _>(0, cx, |state, _| {
|
MouseEventHandler::<A>::new(0, cx, |state, _| {
|
||||||
let style = if enabled {
|
let style = if enabled {
|
||||||
style.style_for(state, false)
|
style.style_for(state, false)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{sidebar::Side, AppState, ToggleFollow, Workspace};
|
use crate::{sidebar::SidebarSide, AppState, ToggleFollow, Workspace};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::{proto, Client, Contact};
|
use client::{proto, Client, Contact};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -74,82 +74,84 @@ impl WaitingRoom {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let project_id = contact.projects[project_index].id;
|
let project_id = contact.projects[project_index].id;
|
||||||
let client = app_state.client.clone();
|
let client = app_state.client.clone();
|
||||||
let _join_task =
|
let _join_task = cx.spawn_weak({
|
||||||
cx.spawn_weak({
|
let contact = contact.clone();
|
||||||
let contact = contact.clone();
|
|this, mut cx| async move {
|
||||||
|this, mut cx| async move {
|
let project = Project::remote(
|
||||||
let project = Project::remote(
|
project_id,
|
||||||
project_id,
|
app_state.client.clone(),
|
||||||
app_state.client.clone(),
|
app_state.user_store.clone(),
|
||||||
app_state.user_store.clone(),
|
app_state.project_store.clone(),
|
||||||
app_state.project_store.clone(),
|
app_state.languages.clone(),
|
||||||
app_state.languages.clone(),
|
app_state.fs.clone(),
|
||||||
app_state.fs.clone(),
|
cx.clone(),
|
||||||
cx.clone(),
|
)
|
||||||
)
|
.await;
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
match project {
|
match project {
|
||||||
Ok(project) => {
|
Ok(project) => {
|
||||||
cx.replace_root_view(|cx| {
|
cx.replace_root_view(|cx| {
|
||||||
let mut workspace = Workspace::new(project, cx);
|
let mut workspace =
|
||||||
(app_state.initialize_workspace)(
|
Workspace::new(project, app_state.default_item_factory, cx);
|
||||||
&mut workspace,
|
(app_state.initialize_workspace)(
|
||||||
&app_state,
|
&mut workspace,
|
||||||
cx,
|
&app_state,
|
||||||
);
|
cx,
|
||||||
workspace.toggle_sidebar(Side::Left, cx);
|
);
|
||||||
if let Some((host_peer_id, _)) =
|
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
||||||
workspace.project.read(cx).collaborators().iter().find(
|
if let Some((host_peer_id, _)) = workspace
|
||||||
|(_, collaborator)| collaborator.replica_id == 0,
|
.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
|
follow.detach_and_log_err(cx);
|
||||||
.toggle_follow(&ToggleFollow(*host_peer_id), cx)
|
|
||||||
{
|
|
||||||
follow.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
workspace
|
}
|
||||||
});
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
Err(error) => {
|
||||||
}
|
let login = &contact.user.github_login;
|
||||||
|
let message = match error {
|
||||||
Ok(())
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
project_id,
|
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
|
/// This may cause issues when you're trying to write tests that use workspace focus to add items at
|
||||||
/// specific locations.
|
/// specific locations.
|
||||||
|
pub mod dock;
|
||||||
pub mod pane;
|
pub mod pane;
|
||||||
pub mod pane_group;
|
pub mod pane_group;
|
||||||
pub mod programs;
|
|
||||||
pub mod searchable;
|
pub mod searchable;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
mod status_bar;
|
mod status_bar;
|
||||||
|
@ -18,6 +18,7 @@ use client::{
|
||||||
};
|
};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
use collections::{hash_map, HashMap, HashSet};
|
||||||
|
use dock::{DefaultItemFactory, Dock, ToggleDockButton};
|
||||||
use drag_and_drop::DragAndDrop;
|
use drag_and_drop::DragAndDrop;
|
||||||
use futures::{channel::oneshot, FutureExt};
|
use futures::{channel::oneshot, FutureExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -37,12 +38,11 @@ use log::error;
|
||||||
pub use pane::*;
|
pub use pane::*;
|
||||||
pub use pane_group::*;
|
pub use pane_group::*;
|
||||||
use postage::prelude::Stream;
|
use postage::prelude::Stream;
|
||||||
use programs::ProgramManager;
|
|
||||||
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
||||||
use searchable::SearchableItemHandle;
|
use searchable::SearchableItemHandle;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::{Autosave, Settings};
|
use settings::{Autosave, DockAnchor, Settings};
|
||||||
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
|
use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use status_bar::StatusBar;
|
use status_bar::StatusBar;
|
||||||
pub use status_bar::StatusItemView;
|
pub use status_bar::StatusItemView;
|
||||||
|
@ -146,10 +146,8 @@ impl_internal_actions!(
|
||||||
impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
|
impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
|
||||||
|
|
||||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
// Initialize the program manager immediately
|
|
||||||
cx.set_global(ProgramManager::new());
|
|
||||||
|
|
||||||
pane::init(cx);
|
pane::init(cx);
|
||||||
|
dock::init(cx);
|
||||||
|
|
||||||
cx.add_global_action(open);
|
cx.add_global_action(open);
|
||||||
cx.add_global_action({
|
cx.add_global_action({
|
||||||
|
@ -217,10 +215,10 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
workspace.activate_next_pane(cx)
|
workspace.activate_next_pane(cx)
|
||||||
});
|
});
|
||||||
cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, 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| {
|
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);
|
cx.add_action(Workspace::activate_pane_at_index);
|
||||||
|
|
||||||
|
@ -265,6 +263,7 @@ pub struct AppState {
|
||||||
pub fs: Arc<dyn fs::Fs>,
|
pub fs: Arc<dyn fs::Fs>,
|
||||||
pub build_window_options: fn() -> WindowOptions<'static>,
|
pub build_window_options: fn() -> WindowOptions<'static>,
|
||||||
pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
|
pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
|
||||||
|
pub default_item_factory: DefaultItemFactory,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash)]
|
#[derive(Eq, PartialEq, Hash)]
|
||||||
|
@ -870,11 +869,13 @@ impl AppState {
|
||||||
project_store,
|
project_store,
|
||||||
initialize_workspace: |_, _, _| {},
|
initialize_workspace: |_, _, _| {},
|
||||||
build_window_options: Default::default,
|
build_window_options: Default::default,
|
||||||
|
default_item_factory: |_, _| unimplemented!(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
|
DockAnchorChanged,
|
||||||
PaneAdded(ViewHandle<Pane>),
|
PaneAdded(ViewHandle<Pane>),
|
||||||
ContactRequestedJoin(u64),
|
ContactRequestedJoin(u64),
|
||||||
}
|
}
|
||||||
|
@ -892,7 +893,9 @@ pub struct Workspace {
|
||||||
panes: Vec<ViewHandle<Pane>>,
|
panes: Vec<ViewHandle<Pane>>,
|
||||||
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
|
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
|
||||||
active_pane: ViewHandle<Pane>,
|
active_pane: ViewHandle<Pane>,
|
||||||
|
last_active_center_pane: Option<ViewHandle<Pane>>,
|
||||||
status_bar: ViewHandle<StatusBar>,
|
status_bar: ViewHandle<StatusBar>,
|
||||||
|
dock: Dock,
|
||||||
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
leader_state: LeaderState,
|
leader_state: LeaderState,
|
||||||
|
@ -922,7 +925,11 @@ enum FollowerItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Workspace {
|
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_fullscreen(|_, _, cx| cx.notify()).detach();
|
||||||
|
|
||||||
cx.observe_window_activation(Self::on_window_activation_changed)
|
cx.observe_window_activation(Self::on_window_activation_changed)
|
||||||
|
@ -949,14 +956,14 @@ impl Workspace {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let pane = cx.add_view(Pane::new);
|
let center_pane = cx.add_view(|cx| Pane::new(None, cx));
|
||||||
let pane_id = pane.id();
|
let pane_id = center_pane.id();
|
||||||
cx.subscribe(&pane, move |this, _, event, cx| {
|
cx.subscribe(¢er_pane, move |this, _, event, cx| {
|
||||||
this.handle_pane_event(pane_id, event, cx)
|
this.handle_pane_event(pane_id, event, cx)
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
cx.focus(&pane);
|
cx.focus(¢er_pane);
|
||||||
cx.emit(Event::PaneAdded(pane.clone()));
|
cx.emit(Event::PaneAdded(center_pane.clone()));
|
||||||
|
|
||||||
let fs = project.read(cx).fs().clone();
|
let fs = project.read(cx).fs().clone();
|
||||||
let user_store = project.read(cx).user_store();
|
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 dock = Dock::new(cx, dock_default_factory);
|
||||||
let right_sidebar = cx.add_view(|_| Sidebar::new(Side::Right));
|
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 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 =
|
let right_sidebar_buttons =
|
||||||
cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
|
cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
|
||||||
let status_bar = cx.add_view(|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_left_item(left_sidebar_buttons, cx);
|
||||||
status_bar.add_right_item(right_sidebar_buttons, cx);
|
status_bar.add_right_item(right_sidebar_buttons, cx);
|
||||||
|
status_bar.add_right_item(toggle_dock, cx);
|
||||||
status_bar
|
status_bar
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
|
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 {
|
let mut this = Workspace {
|
||||||
modal: None,
|
modal: None,
|
||||||
weak_self,
|
weak_self: weak_handle,
|
||||||
center: PaneGroup::new(pane.clone()),
|
center: PaneGroup::new(center_pane.clone()),
|
||||||
panes: vec![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(),
|
panes_by_item: Default::default(),
|
||||||
active_pane: pane.clone(),
|
active_pane: center_pane.clone(),
|
||||||
|
last_active_center_pane: Some(center_pane.clone()),
|
||||||
status_bar,
|
status_bar,
|
||||||
notifications: Default::default(),
|
notifications: Default::default(),
|
||||||
client,
|
client,
|
||||||
|
@ -1078,6 +1096,7 @@ impl Workspace {
|
||||||
app_state.fs.clone(),
|
app_state.fs.clone(),
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
|
app_state.default_item_factory,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
(app_state.initialize_workspace)(&mut workspace, &app_state, 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>) {
|
pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
|
||||||
let sidebar = match side {
|
let sidebar = match sidebar_side {
|
||||||
Side::Left => &mut self.left_sidebar,
|
SidebarSide::Left => &mut self.left_sidebar,
|
||||||
Side::Right => &mut self.right_sidebar,
|
SidebarSide::Right => &mut self.right_sidebar,
|
||||||
};
|
};
|
||||||
sidebar.update(cx, |sidebar, cx| {
|
let open = sidebar.update(cx, |sidebar, cx| {
|
||||||
sidebar.set_open(!sidebar.is_open(), 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.focus_self();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
|
pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
|
||||||
let sidebar = match action.side {
|
let sidebar = match action.sidebar_side {
|
||||||
Side::Left => &mut self.left_sidebar,
|
SidebarSide::Left => &mut self.left_sidebar,
|
||||||
Side::Right => &mut self.right_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 {
|
if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
|
||||||
sidebar.set_open(false, cx);
|
sidebar.set_open(false, cx);
|
||||||
None
|
None
|
||||||
|
@ -1486,7 +1512,10 @@ impl Workspace {
|
||||||
sidebar.active_item().cloned()
|
sidebar.active_item().cloned()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(active_item) = active_item {
|
if let Some(active_item) = active_item {
|
||||||
|
Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
|
||||||
|
|
||||||
if active_item.is_focused(cx) {
|
if active_item.is_focused(cx) {
|
||||||
cx.focus_self();
|
cx.focus_self();
|
||||||
} else {
|
} else {
|
||||||
|
@ -1500,13 +1529,13 @@ impl Workspace {
|
||||||
|
|
||||||
pub fn toggle_sidebar_item_focus(
|
pub fn toggle_sidebar_item_focus(
|
||||||
&mut self,
|
&mut self,
|
||||||
side: Side,
|
sidebar_side: SidebarSide,
|
||||||
item_index: usize,
|
item_index: usize,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let sidebar = match side {
|
let sidebar = match sidebar_side {
|
||||||
Side::Left => &mut self.left_sidebar,
|
SidebarSide::Left => &mut self.left_sidebar,
|
||||||
Side::Right => &mut self.right_sidebar,
|
SidebarSide::Right => &mut self.right_sidebar,
|
||||||
};
|
};
|
||||||
let active_item = sidebar.update(cx, |sidebar, cx| {
|
let active_item = sidebar.update(cx, |sidebar, cx| {
|
||||||
sidebar.set_open(true, cx);
|
sidebar.set_open(true, cx);
|
||||||
|
@ -1514,6 +1543,8 @@ impl Workspace {
|
||||||
sidebar.active_item().cloned()
|
sidebar.active_item().cloned()
|
||||||
});
|
});
|
||||||
if let Some(active_item) = active_item {
|
if let Some(active_item) = active_item {
|
||||||
|
Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
|
||||||
|
|
||||||
if active_item.is_focused(cx) {
|
if active_item.is_focused(cx) {
|
||||||
cx.focus_self();
|
cx.focus_self();
|
||||||
} else {
|
} else {
|
||||||
|
@ -1529,7 +1560,7 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
|
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();
|
let pane_id = pane.id();
|
||||||
cx.subscribe(&pane, move |this, _, event, cx| {
|
cx.subscribe(&pane, move |this, _, event, cx| {
|
||||||
this.handle_pane_event(pane_id, 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);
|
status_bar.set_active_pane(&self.active_pane, cx);
|
||||||
});
|
});
|
||||||
self.active_item_path_changed(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();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1701,21 +1741,19 @@ impl Workspace {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if let Some(pane) = self.pane(pane_id) {
|
if let Some(pane) = self.pane(pane_id) {
|
||||||
|
let is_dock = &pane == self.dock.pane();
|
||||||
match event {
|
match event {
|
||||||
pane::Event::Split(direction) => {
|
pane::Event::Split(direction) if !is_dock => {
|
||||||
self.split_pane(pane, *direction, cx);
|
self.split_pane(pane, *direction, cx);
|
||||||
}
|
}
|
||||||
pane::Event::Remove => {
|
pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
|
||||||
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::Focused => {
|
|
||||||
self.handle_pane_focused(pane, cx);
|
|
||||||
}
|
|
||||||
pane::Event::ActivateItem { local } => {
|
pane::Event::ActivateItem { local } => {
|
||||||
if *local {
|
if *local {
|
||||||
self.unfollow(&pane, cx);
|
self.unfollow(&pane, cx);
|
||||||
}
|
}
|
||||||
if pane == self.active_pane {
|
if &pane == self.active_pane() {
|
||||||
self.active_item_path_changed(cx);
|
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);
|
error!("pane {} not found", pane_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1765,6 +1804,10 @@ impl Workspace {
|
||||||
for removed_item in pane.read(cx).items() {
|
for removed_item in pane.read(cx).items() {
|
||||||
self.panes_by_item.remove(&removed_item.id());
|
self.panes_by_item.remove(&removed_item.id());
|
||||||
}
|
}
|
||||||
|
if self.last_active_center_pane == Some(pane) {
|
||||||
|
self.last_active_center_pane = None;
|
||||||
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
} else {
|
} else {
|
||||||
self.active_item_path_changed(cx);
|
self.active_item_path_changed(cx);
|
||||||
|
@ -1783,6 +1826,10 @@ impl Workspace {
|
||||||
&self.active_pane
|
&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>) {
|
fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(remote_id) = remote_id {
|
if let Some(remote_id) = remote_id {
|
||||||
self.remote_entity_subscription =
|
self.remote_entity_subscription =
|
||||||
|
@ -1975,8 +2022,9 @@ impl Workspace {
|
||||||
theme.workspace.titlebar.container
|
theme.workspace.titlebar.container
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum TitleBar {}
|
||||||
ConstrainedBox::new(
|
ConstrainedBox::new(
|
||||||
MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
|
MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
|
||||||
Container::new(
|
Container::new(
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(
|
.with_child(
|
||||||
|
@ -2105,7 +2153,7 @@ impl Workspace {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(
|
Some(
|
||||||
MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
|
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
|
||||||
let style = theme
|
let style = theme
|
||||||
.workspace
|
.workspace
|
||||||
.titlebar
|
.titlebar
|
||||||
|
@ -2165,7 +2213,7 @@ impl Workspace {
|
||||||
.boxed();
|
.boxed();
|
||||||
|
|
||||||
if let Some((peer_id, peer_github_login)) = peer {
|
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)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, move |_, cx| {
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
cx.dispatch_action(ToggleFollow(peer_id))
|
cx.dispatch_action(ToggleFollow(peer_id))
|
||||||
|
@ -2191,7 +2239,7 @@ impl Workspace {
|
||||||
if self.project.read(cx).is_read_only() {
|
if self.project.read(cx).is_read_only() {
|
||||||
enum DisconnectedOverlay {}
|
enum DisconnectedOverlay {}
|
||||||
Some(
|
Some(
|
||||||
MouseEventHandler::new::<DisconnectedOverlay, _, _>(0, cx, |_, cx| {
|
MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
|
||||||
let theme = &cx.global::<Settings>().theme;
|
let theme = &cx.global::<Settings>().theme;
|
||||||
Label::new(
|
Label::new(
|
||||||
"Your connection to the remote project has been lost.".to_string(),
|
"Your connection to the remote project has been lost.".to_string(),
|
||||||
|
@ -2202,6 +2250,7 @@ impl Workspace {
|
||||||
.with_style(theme.workspace.disconnected_overlay.container)
|
.with_style(theme.workspace.disconnected_overlay.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
|
.with_cursor_style(CursorStyle::Arrow)
|
||||||
.capture_all()
|
.capture_all()
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
|
@ -2557,14 +2606,28 @@ impl View for Workspace {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
FlexItem::new(self.center.render(
|
FlexItem::new(
|
||||||
&theme,
|
Flex::column()
|
||||||
&self.follower_states_by_leader,
|
.with_child(
|
||||||
self.project.read(cx).collaborators(),
|
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)
|
.flex(1., true)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
|
.with_children(self.dock.render(&theme, DockAnchor::Right, cx))
|
||||||
.with_children(
|
.with_children(
|
||||||
if self.right_sidebar.read(cx).active_item().is_some() {
|
if self.right_sidebar.read(cx).active_item().is_some() {
|
||||||
Some(
|
Some(
|
||||||
|
@ -2578,15 +2641,27 @@ impl View for Workspace {
|
||||||
)
|
)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_children(self.modal.as_ref().map(|m| {
|
.with_child(
|
||||||
ChildView::new(m)
|
Overlay::new(
|
||||||
.contained()
|
Stack::new()
|
||||||
.with_style(theme.workspace.modal)
|
.with_children(self.dock.render(
|
||||||
.aligned()
|
&theme,
|
||||||
.top()
|
DockAnchor::Expanded,
|
||||||
.boxed()
|
cx,
|
||||||
}))
|
))
|
||||||
.with_children(self.render_notifications(&theme.workspace))
|
.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)
|
.flex(1.0, true)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
|
@ -2785,10 +2860,10 @@ pub fn open_paths(
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
new_project = Some(project.clone());
|
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);
|
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||||
if contains_directory {
|
if contains_directory {
|
||||||
workspace.toggle_sidebar(Side::Left, cx);
|
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
||||||
}
|
}
|
||||||
workspace
|
workspace
|
||||||
})
|
})
|
||||||
|
@ -2846,6 +2921,7 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
app_state.fs.clone(),
|
app_state.fs.clone(),
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
|
app_state.default_item_factory,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
(app_state.initialize_workspace)(&mut workspace, app_state, 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 {
|
mod tests {
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
use crate::sidebar::SidebarItem;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
||||||
use project::{FakeFs, Project, ProjectEntryId};
|
use project::{FakeFs, Project, ProjectEntryId};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub fn default_item_factory(
|
||||||
|
_workspace: &mut Workspace,
|
||||||
|
_cx: &mut ViewContext<Workspace>,
|
||||||
|
) -> Box<dyn ItemHandle> {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_tab_disambiguation(cx: &mut TestAppContext) {
|
async fn test_tab_disambiguation(cx: &mut TestAppContext) {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
@ -2870,7 +2955,8 @@ mod tests {
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
let project = Project::test(fs, [], cx).await;
|
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.
|
// Adding an item with no ambiguity renders the tab without detail.
|
||||||
let item1 = cx.add_view(&workspace, |_| {
|
let item1 = cx.add_view(&workspace, |_| {
|
||||||
|
@ -2934,7 +3020,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(fs, ["root1".as_ref()], cx).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| {
|
let worktree_id = project.read_with(cx, |project, cx| {
|
||||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||||
});
|
});
|
||||||
|
@ -3030,7 +3117,8 @@ mod tests {
|
||||||
fs.insert_tree("/root", json!({ "one": "" })).await;
|
fs.insert_tree("/root", json!({ "one": "" })).await;
|
||||||
|
|
||||||
let project = Project::test(fs, ["root".as_ref()], cx).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.
|
// When there are no dirty items, there's nothing to do.
|
||||||
let item1 = cx.add_view(&workspace, |_| TestItem::new());
|
let item1 = cx.add_view(&workspace, |_| TestItem::new());
|
||||||
|
@ -3070,7 +3158,8 @@ mod tests {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
let project = Project::test(fs, None, cx).await;
|
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 item1 = cx.add_view(&workspace, |_| {
|
||||||
let mut item = TestItem::new();
|
let mut item = TestItem::new();
|
||||||
|
@ -3165,7 +3254,8 @@ mod tests {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
let project = Project::test(fs, [], cx).await;
|
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
|
// Create several workspace items with single project entries, and two
|
||||||
// workspace items with multiple project entries.
|
// workspace items with multiple project entries.
|
||||||
|
@ -3266,7 +3356,8 @@ mod tests {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
let project = Project::test(fs, [], cx).await;
|
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 item = cx.add_view(&workspace, |_| {
|
||||||
let mut item = TestItem::new();
|
let mut item = TestItem::new();
|
||||||
|
@ -3383,7 +3474,7 @@ mod tests {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
let project = Project::test(fs, [], cx).await;
|
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 item = cx.add_view(&workspace, |_| {
|
||||||
let mut item = TestItem::new();
|
let mut item = TestItem::new();
|
||||||
|
@ -3635,4 +3726,6 @@ mod tests {
|
||||||
vec![ItemEvent::UpdateTab, ItemEvent::Edit]
|
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 {
|
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 = &cx.global::<Settings>().theme;
|
||||||
let theme = &theme.workspace.status_bar.feedback;
|
let theme = &theme.workspace.status_bar.feedback;
|
||||||
Text::new(
|
Text::new(
|
||||||
|
|
|
@ -19,20 +19,21 @@ use futures::{
|
||||||
channel::{mpsc, oneshot},
|
channel::{mpsc, oneshot},
|
||||||
FutureExt, SinkExt, StreamExt,
|
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 isahc::{config::Configurable, AsyncBody, Request};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{Fs, ProjectStore};
|
use project::{Fs, ProjectStore};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::{self, KeymapFileContent, Settings, SettingsFileContent};
|
use settings::{self, KeymapFileContent, Settings, SettingsFileContent, WorkingDirectory};
|
||||||
use smol::process::Command;
|
use smol::process::Command;
|
||||||
use std::{env, ffi::OsStr, fs, panic, path::PathBuf, sync::Arc, thread, time::Duration};
|
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 theme::ThemeRegistry;
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
use workspace::{self, AppState, NewFile, OpenPaths};
|
use workspace::{self, AppState, ItemHandle, NewFile, OpenPaths, Workspace};
|
||||||
use zed::{
|
use zed::{
|
||||||
self, build_window_options,
|
self, build_window_options,
|
||||||
fs::RealFs,
|
fs::RealFs,
|
||||||
|
@ -148,6 +149,7 @@ fn main() {
|
||||||
fs,
|
fs,
|
||||||
build_window_options,
|
build_window_options,
|
||||||
initialize_workspace,
|
initialize_workspace,
|
||||||
|
default_item_factory,
|
||||||
});
|
});
|
||||||
auto_update::init(db, http, client::ZED_SERVER_URL.clone(), cx);
|
auto_update::init(db, http, client::ZED_SERVER_URL.clone(), cx);
|
||||||
workspace::init(app_state.clone(), cx);
|
workspace::init(app_state.clone(), cx);
|
||||||
|
@ -591,3 +593,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 std::{env, path::Path, str, sync::Arc};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
pub use workspace;
|
pub use workspace;
|
||||||
use workspace::{sidebar::Side, AppState, Workspace};
|
use workspace::{sidebar::SidebarSide, AppState, Workspace};
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, PartialEq)]
|
#[derive(Deserialize, Clone, PartialEq)]
|
||||||
struct OpenBrowser {
|
struct OpenBrowser {
|
||||||
|
@ -204,14 +204,14 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
|workspace: &mut Workspace,
|
|workspace: &mut Workspace,
|
||||||
_: &project_panel::ToggleFocus,
|
_: &project_panel::ToggleFocus,
|
||||||
cx: &mut ViewContext<Workspace>| {
|
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(
|
cx.add_action(
|
||||||
|workspace: &mut Workspace,
|
|workspace: &mut Workspace,
|
||||||
_: &contacts_panel::ToggleFocus,
|
_: &contacts_panel::ToggleFocus,
|
||||||
cx: &mut ViewContext<Workspace>| {
|
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();
|
.detach();
|
||||||
|
|
||||||
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
|
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
|
||||||
|
cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
|
||||||
|
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
|
|
||||||
|
@ -723,7 +724,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).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 entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||||
let file1 = entries[0].clone();
|
let file1 = entries[0].clone();
|
||||||
|
@ -842,7 +844,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).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.
|
// Open a file within an existing worktree.
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -1001,7 +1004,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).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.
|
// Open a file within an existing worktree.
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -1043,7 +1047,8 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
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());
|
let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
|
||||||
|
|
||||||
// Create a new untitled buffer
|
// Create a new untitled buffer
|
||||||
|
@ -1132,7 +1137,8 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
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
|
// Create a new untitled buffer
|
||||||
cx.dispatch_action(window_id, NewFile);
|
cx.dispatch_action(window_id, NewFile);
|
||||||
|
@ -1185,7 +1191,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).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 entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||||
let file1 = entries[0].clone();
|
let file1 = entries[0].clone();
|
||||||
|
@ -1221,7 +1228,7 @@ mod tests {
|
||||||
|
|
||||||
cx.foreground().run_until_parked();
|
cx.foreground().run_until_parked();
|
||||||
workspace.read_with(cx, |workspace, _| {
|
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);
|
assert_eq!(workspace.active_pane(), &pane_1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1231,6 +1238,7 @@ mod tests {
|
||||||
cx.foreground().run_until_parked();
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
workspace.read_with(cx, |workspace, cx| {
|
workspace.read_with(cx, |workspace, cx| {
|
||||||
|
assert_eq!(workspace.panes().len(), 2);
|
||||||
assert!(workspace.active_item(cx).is_none());
|
assert!(workspace.active_item(cx).is_none());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1258,7 +1266,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).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 entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||||
let file1 = entries[0].clone();
|
let file1 = entries[0].clone();
|
||||||
|
@ -1522,7 +1531,8 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).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 pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
|
||||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
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"),
|
background: backgroundColor(theme, "on500", "active"),
|
||||||
border: border(theme, "muted"),
|
border: border(theme, "muted"),
|
||||||
},
|
},
|
||||||
|
clicked: {
|
||||||
|
...text(theme, "mono", "active"),
|
||||||
|
background: backgroundColor(theme, "on300", "active"),
|
||||||
|
border: border(theme, "secondary"),
|
||||||
|
},
|
||||||
hover: {
|
hover: {
|
||||||
...text(theme, "mono", "active"),
|
...text(theme, "mono", "active"),
|
||||||
background: backgroundColor(theme, "on500", "hovered"),
|
background: backgroundColor(theme, "on500", "hovered"),
|
||||||
|
|
|
@ -96,7 +96,6 @@ export default function tabBar(theme: Theme) {
|
||||||
buttonWidth: activePaneActiveTab.height,
|
buttonWidth: activePaneActiveTab.height,
|
||||||
hover: {
|
hover: {
|
||||||
color: iconColor(theme, "active"),
|
color: iconColor(theme, "active"),
|
||||||
background: backgroundColor(theme, 300),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,11 +37,14 @@ export default function workspace(theme: Theme) {
|
||||||
},
|
},
|
||||||
cursor: "Arrow",
|
cursor: "Arrow",
|
||||||
},
|
},
|
||||||
sidebarResizeHandle: {
|
sidebar: {
|
||||||
background: border(theme, "primary").color,
|
initialSize: 240,
|
||||||
padding: {
|
border: {
|
||||||
left: 1,
|
color: border(theme, "primary").color,
|
||||||
},
|
width: 1,
|
||||||
|
left: true,
|
||||||
|
right: true,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
paneDivider: {
|
paneDivider: {
|
||||||
color: border(theme, "secondary").color,
|
color: border(theme, "secondary").color,
|
||||||
|
@ -156,5 +159,19 @@ export default function workspace(theme: Theme) {
|
||||||
width: 400,
|
width: 400,
|
||||||
margin: { right: 10, bottom: 10 },
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue