Introduce keyboard navigation in context menus

This commit is contained in:
Antonio Scandurra 2022-05-26 16:36:30 +02:00
parent 991eb742b0
commit 5b2d6e41f3
20 changed files with 121 additions and 34 deletions

15
Cargo.lock generated
View File

@ -665,6 +665,7 @@ dependencies = [
"client", "client",
"editor", "editor",
"gpui", "gpui",
"menu",
"postage", "postage",
"settings", "settings",
"theme", "theme",
@ -964,6 +965,7 @@ dependencies = [
"gpui", "gpui",
"language", "language",
"log", "log",
"menu",
"picker", "picker",
"postage", "postage",
"project", "project",
@ -979,6 +981,7 @@ name = "context_menu"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"gpui", "gpui",
"menu",
"settings", "settings",
"smallvec", "smallvec",
"theme", "theme",
@ -1526,6 +1529,7 @@ dependencies = [
"env_logger", "env_logger",
"fuzzy", "fuzzy",
"gpui", "gpui",
"menu",
"picker", "picker",
"postage", "postage",
"project", "project",
@ -1904,6 +1908,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"editor", "editor",
"gpui", "gpui",
"menu",
"postage", "postage",
"settings", "settings",
"text", "text",
@ -2698,6 +2703,13 @@ dependencies = [
"autocfg 1.0.1", "autocfg 1.0.1",
] ]
[[package]]
name = "menu"
version = "0.1.0"
dependencies = [
"gpui",
]
[[package]] [[package]]
name = "metal" name = "metal"
version = "0.21.0" version = "0.21.0"
@ -3252,6 +3264,7 @@ dependencies = [
"editor", "editor",
"env_logger", "env_logger",
"gpui", "gpui",
"menu",
"serde_json", "serde_json",
"settings", "settings",
"theme", "theme",
@ -3463,6 +3476,7 @@ dependencies = [
"editor", "editor",
"futures", "futures",
"gpui", "gpui",
"menu",
"postage", "postage",
"project", "project",
"serde_json", "serde_json",
@ -4176,6 +4190,7 @@ dependencies = [
"gpui", "gpui",
"language", "language",
"log", "log",
"menu",
"postage", "postage",
"project", "project",
"serde", "serde",

View File

@ -11,6 +11,7 @@ doctest = false
client = { path = "../client" } client = { path = "../client" }
editor = { path = "../editor" } editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
menu = { path = "../menu" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }

View File

@ -11,12 +11,12 @@ use gpui::{
AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
ViewContext, ViewHandle, ViewContext, ViewHandle,
}; };
use menu::Confirm;
use postage::prelude::Stream; use postage::prelude::Stream;
use settings::{Settings, SoftWrap}; use settings::{Settings, SoftWrap};
use std::sync::Arc; use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
use workspace::menu::Confirm;
const MESSAGE_LOADING_THRESHOLD: usize = 50; const MESSAGE_LOADING_THRESHOLD: usize = 50;

View File

@ -12,6 +12,7 @@ client = { path = "../client" }
editor = { path = "../editor" } editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" } fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" } picker = { path = "../picker" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" } settings = { path = "../settings" }

View File

@ -16,15 +16,12 @@ use gpui::{
MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
}; };
use join_project_notification::JoinProjectNotification; use join_project_notification::JoinProjectNotification;
use menu::{Confirm, SelectNext, SelectPrev};
use serde::Deserialize; use serde::Deserialize;
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use theme::IconButton; use theme::IconButton;
use workspace::{ use workspace::{sidebar::SidebarItem, JoinProject, Workspace};
menu::{Confirm, SelectNext, SelectPrev},
sidebar::SidebarItem,
JoinProject, Workspace,
};
impl_actions!( impl_actions!(
contacts_panel, contacts_panel,

View File

@ -9,6 +9,7 @@ doctest = false
[dependencies] [dependencies]
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
menu = { path = "../menu" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
smallvec = "1.6" smallvec = "1.6"

View File

@ -1,18 +1,19 @@
use gpui::{ use gpui::{
elements::*, geometry::vector::Vector2F, impl_internal_actions, platform::CursorStyle, Action, elements::*, geometry::vector::Vector2F, keymap, platform::CursorStyle, Action, AppContext,
Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, View, ViewContext, Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, View, ViewContext,
}; };
use menu::*;
use settings::Settings; use settings::Settings;
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContextMenu::dismiss); cx.add_action(ContextMenu::select_first);
cx.add_action(ContextMenu::select_last);
cx.add_action(ContextMenu::select_next);
cx.add_action(ContextMenu::select_prev);
cx.add_action(ContextMenu::confirm);
cx.add_action(ContextMenu::cancel);
} }
#[derive(Clone)]
struct Dismiss;
impl_internal_actions!(context_menu, [Dismiss]);
pub enum ContextMenuItem { pub enum ContextMenuItem {
Item { Item {
label: String, label: String,
@ -32,6 +33,10 @@ impl ContextMenuItem {
pub fn separator() -> Self { pub fn separator() -> Self {
Self::Separator Self::Separator
} }
fn is_separator(&self) -> bool {
matches!(self, Self::Separator)
}
} }
#[derive(Default)] #[derive(Default)]
@ -52,6 +57,12 @@ impl View for ContextMenu {
"ContextMenu" "ContextMenu"
} }
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if !self.visible { if !self.visible {
return Empty::new().boxed(); return Empty::new().boxed();
@ -77,6 +88,7 @@ impl View for ContextMenu {
fn on_blur(&mut self, cx: &mut ViewContext<Self>) { fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
self.visible = false; self.visible = false;
self.selected_index.take();
cx.notify(); cx.notify();
} }
} }
@ -86,13 +98,66 @@ impl ContextMenu {
Default::default() Default::default()
} }
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
let window_id = cx.window_id();
let view_id = cx.view_id();
cx.dispatch_action_at(window_id, view_id, action.as_ref());
}
}
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
if cx.handle().is_focused(cx) { if cx.handle().is_focused(cx) {
let window_id = cx.window_id(); let window_id = cx.window_id();
(**cx).focus(window_id, self.previously_focused_view_id.take()); (**cx).focus(window_id, self.previously_focused_view_id.take());
} }
} }
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
self.selected_index = self.items.iter().position(|item| !item.is_separator());
cx.notify();
}
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
for (ix, item) in self.items.iter().enumerate().rev() {
if !item.is_separator() {
self.selected_index = Some(ix);
cx.notify();
break;
}
}
}
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
if !item.is_separator() {
self.selected_index = Some(ix);
cx.notify();
break;
}
}
} else {
self.select_first(&Default::default(), cx);
}
}
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
if !item.is_separator() {
self.selected_index = Some(ix);
cx.notify();
break;
}
}
} else {
self.select_last(&Default::default(), cx);
}
}
pub fn show( pub fn show(
&mut self, &mut self,
position: Vector2F, position: Vector2F,
@ -202,7 +267,7 @@ impl ContextMenu {
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(move |_, _, cx| { .on_click(move |_, _, cx| {
cx.dispatch_any_action(action.boxed_clone()); cx.dispatch_any_action(action.boxed_clone());
cx.dispatch_action(Dismiss); cx.dispatch_action(Cancel);
}) })
.boxed() .boxed()
} }

View File

@ -11,6 +11,7 @@ doctest = false
editor = { path = "../editor" } editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" } fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" } picker = { path = "../picker" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" } settings = { path = "../settings" }

View File

@ -257,11 +257,9 @@ impl PickerDelegate for FileFinder {
mod tests { mod tests {
use super::*; use super::*;
use editor::{Editor, Input}; use editor::{Editor, Input};
use menu::{Confirm, SelectNext};
use serde_json::json; use serde_json::json;
use workspace::{ use workspace::{AppState, Workspace};
menu::{Confirm, SelectNext},
AppState, Workspace,
};
#[ctor::ctor] #[ctor::ctor]
fn init_logger() { fn init_logger() {

View File

@ -8,9 +8,10 @@ path = "src/go_to_line.rs"
doctest = false doctest = false
[dependencies] [dependencies]
text = { path = "../text" }
editor = { path = "../editor" } editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
menu = { path = "../menu" }
settings = { path = "../settings" } settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
postage = { version = "0.4", features = ["futures-traits"] } postage = { version = "0.4", features = ["futures-traits"] }

View File

@ -3,12 +3,10 @@ use gpui::{
actions, elements::*, geometry::vector::Vector2F, Axis, Entity, MutableAppContext, actions, elements::*, geometry::vector::Vector2F, Axis, Entity, MutableAppContext,
RenderContext, View, ViewContext, ViewHandle, RenderContext, View, ViewContext, ViewHandle,
}; };
use menu::{Cancel, Confirm};
use settings::Settings; use settings::Settings;
use text::{Bias, Point}; use text::{Bias, Point};
use workspace::{ use workspace::Workspace;
menu::{Cancel, Confirm},
Workspace,
};
actions!(go_to_line, [Toggle]); actions!(go_to_line, [Toggle]);

11
crates/menu/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "menu"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/menu.rs"
doctest = false
[dependencies]
gpui = { path = "../gpui" }

View File

@ -10,6 +10,7 @@ doctest = false
[dependencies] [dependencies]
editor = { path = "../editor" } editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
menu = { path = "../menu" }
settings = { path = "../settings" } settings = { path = "../settings" }
util = { path = "../util" } util = { path = "../util" }
theme = { path = "../theme" } theme = { path = "../theme" }

View File

@ -10,11 +10,9 @@ use gpui::{
AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, View, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle, ViewContext, ViewHandle, WeakViewHandle,
}; };
use menu::{Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev};
use settings::Settings; use settings::Settings;
use std::cmp; use std::cmp;
use workspace::menu::{
Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev,
};
pub struct Picker<D: PickerDelegate> { pub struct Picker<D: PickerDelegate> {
delegate: WeakViewHandle<D>, delegate: WeakViewHandle<D>,

View File

@ -11,6 +11,7 @@ doctest = false
context_menu = { path = "../context_menu" } context_menu = { path = "../context_menu" }
editor = { path = "../editor" } editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
menu = { path = "../menu" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }

View File

@ -14,6 +14,7 @@ use gpui::{
AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task,
View, ViewContext, ViewHandle, WeakViewHandle, View, ViewContext, ViewHandle, WeakViewHandle,
}; };
use menu::{Confirm, SelectNext, SelectPrev};
use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use settings::Settings; use settings::Settings;
use std::{ use std::{
@ -23,10 +24,7 @@ use std::{
ops::Range, ops::Range,
}; };
use unicase::UniCase; use unicase::UniCase;
use workspace::{ use workspace::Workspace;
menu::{Confirm, SelectNext, SelectPrev},
Workspace,
};
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;

View File

@ -12,6 +12,7 @@ collections = { path = "../collections" }
editor = { path = "../editor" } editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
menu = { path = "../menu" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }

View File

@ -9,6 +9,7 @@ use gpui::{
ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext,
ViewHandle, WeakModelHandle, WeakViewHandle, ViewHandle, WeakModelHandle, WeakViewHandle,
}; };
use menu::Confirm;
use project::{search::SearchQuery, Project}; use project::{search::SearchQuery, Project};
use settings::Settings; use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
@ -19,8 +20,7 @@ use std::{
}; };
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{ use workspace::{
menu::Confirm, Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace,
Workspace,
}; };
actions!(project_search, [Deploy, SearchInNew, ToggleFocus]); actions!(project_search, [Deploy, SearchInNew, ToggleFocus]);

View File

@ -1,5 +1,4 @@
pub mod lsp_status; pub mod lsp_status;
pub mod menu;
pub mod pane; pub mod pane;
pub mod pane_group; pub mod pane_group;
pub mod sidebar; pub mod sidebar;