Cargo.lock 🔗
@@ -980,6 +980,7 @@ version = "0.1.0"
dependencies = [
"gpui",
"settings",
+ "smallvec",
"theme",
]
Antonio Scandurra created
Cargo.lock | 1
crates/context_menu/Cargo.toml | 1
crates/context_menu/src/context_menu.rs | 108 ++++++++++++++--------
crates/gpui/src/app.rs | 11 +-
crates/gpui/src/elements.rs | 5
crates/gpui/src/elements/keystroke_label.rs | 92 +++++++++++++++++++
crates/gpui/src/keymap.rs | 30 ++++++
crates/gpui/src/presenter.rs | 12 ++
crates/project_panel/src/project_panel.rs | 20 ---
crates/theme/src/theme.rs | 1
styles/src/styleTree/contextMenu.ts | 3
11 files changed, 219 insertions(+), 65 deletions(-)
@@ -980,6 +980,7 @@ version = "0.1.0"
dependencies = [
"gpui",
"settings",
+ "smallvec",
"theme",
]
@@ -11,3 +11,4 @@ doctest = false
gpui = { path = "../gpui" }
settings = { path = "../settings" }
theme = { path = "../theme" }
+smallvec = "1.6"
@@ -12,10 +12,22 @@ pub enum ContextMenuItem {
Separator,
}
+impl ContextMenuItem {
+ pub fn item(label: String, action: impl 'static + Action) -> Self {
+ Self::Item {
+ label,
+ action: Box::new(action),
+ }
+ }
+
+ pub fn separator() -> Self {
+ Self::Separator
+ }
+}
+
pub struct ContextMenu {
position: Vector2F,
items: Vec<ContextMenuItem>,
- widest_item_index: usize,
selected_index: Option<usize>,
visible: bool,
}
@@ -36,28 +48,22 @@ impl View for ContextMenu {
return Empty::new().boxed();
}
- let style = cx.global::<Settings>().theme.context_menu.clone();
-
- let mut widest_item = self.render_menu_item::<()>(self.widest_item_index, cx, &style);
-
- Overlay::new(
- Flex::column()
- .with_children(
- (0..self.items.len()).map(|ix| self.render_menu_item::<Tag>(ix, cx, &style)),
+ // Render the menu once at minimum width.
+ let mut collapsed_menu = self.render_menu::<()>(false, cx).boxed();
+ let expanded_menu = self
+ .render_menu::<Tag>(true, cx)
+ .constrained()
+ .dynamically(move |constraint, cx| {
+ SizeConstraint::strict_along(
+ Axis::Horizontal,
+ collapsed_menu.layout(constraint, cx).x(),
)
- .constrained()
- .dynamically(move |constraint, cx| {
- SizeConstraint::strict_along(
- Axis::Horizontal,
- widest_item.layout(constraint, cx).x(),
- )
- })
- .contained()
- .with_style(style.container)
- .boxed(),
- )
- .with_abs_position(self.position)
- .boxed()
+ })
+ .boxed();
+
+ Overlay::new(expanded_menu)
+ .with_abs_position(self.position)
+ .boxed()
}
fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
@@ -72,7 +78,6 @@ impl ContextMenu {
position: Default::default(),
items: Default::default(),
selected_index: Default::default(),
- widest_item_index: Default::default(),
visible: false,
}
}
@@ -86,25 +91,31 @@ impl ContextMenu {
let mut items = items.into_iter().peekable();
assert!(items.peek().is_some(), "must have at least one item");
self.items = items.collect();
- self.widest_item_index = self
- .items
- .iter()
- .enumerate()
- .max_by_key(|(_, item)| match item {
- ContextMenuItem::Item { label, .. } => label.chars().count(),
- ContextMenuItem::Separator => 0,
- })
- .unwrap()
- .0;
self.position = position;
self.visible = true;
cx.focus_self();
cx.notify();
}
+ fn render_menu<Tag: 'static>(
+ &mut self,
+ expanded: bool,
+ cx: &mut RenderContext<Self>,
+ ) -> impl Element {
+ let style = cx.global::<Settings>().theme.context_menu.clone();
+ Flex::column()
+ .with_children(
+ (0..self.items.len())
+ .map(|ix| self.render_menu_item::<Tag>(ix, expanded, cx, &style)),
+ )
+ .contained()
+ .with_style(style.container)
+ }
+
fn render_menu_item<T: 'static>(
&self,
ix: usize,
+ expanded: bool,
cx: &mut RenderContext<ContextMenu>,
style: &theme::ContextMenu,
) -> ElementBox {
@@ -115,18 +126,35 @@ impl ContextMenu {
let style = style.item.style_for(state, Some(ix) == self.selected_index);
Flex::row()
.with_child(Label::new(label.to_string(), style.label.clone()).boxed())
+ .with_child({
+ let label = KeystrokeLabel::new(
+ action.boxed_clone(),
+ style.keystroke.container,
+ style.keystroke.text.clone(),
+ );
+ if expanded {
+ label.flex_float().boxed()
+ } else {
+ label.boxed()
+ }
+ })
.boxed()
})
.on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()))
.boxed()
}
- ContextMenuItem::Separator => Empty::new()
- .contained()
- .with_style(style.separator)
- .constrained()
- .with_height(1.)
- .flex(1., false)
- .boxed(),
+ ContextMenuItem::Separator => {
+ let mut separator = Empty::new();
+ if !expanded {
+ separator = separator.collapsed();
+ }
+ separator
+ .contained()
+ .with_style(style.separator)
+ .constrained()
+ .with_height(1.)
+ .boxed()
+ }
}
}
}
@@ -1414,11 +1414,12 @@ impl MutableAppContext {
}
/// Return keystrokes that would dispatch the given action closest to the focused view, if there are any.
- pub fn keystrokes_for_action(&self, action: &dyn Action) -> Option<SmallVec<[Keystroke; 2]>> {
- let window_id = self.cx.platform.key_window_id()?;
- let (presenter, _) = self.presenters_and_platform_windows.get(&window_id)?;
- let dispatch_path = presenter.borrow().dispatch_path(&self.cx);
-
+ pub(crate) fn keystrokes_for_action(
+ &self,
+ window_id: usize,
+ dispatch_path: &[usize],
+ action: &dyn Action,
+ ) -> Option<SmallVec<[Keystroke; 2]>> {
for view_id in dispatch_path.iter().rev() {
let view = self
.cx
@@ -8,6 +8,7 @@ mod expanded;
mod flex;
mod hook;
mod image;
+mod keystroke_label;
mod label;
mod list;
mod mouse_event_handler;
@@ -20,8 +21,8 @@ mod uniform_list;
use self::expanded::Expanded;
pub use self::{
align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
- hook::*, image::*, label::*, list::*, mouse_event_handler::*, overlay::*, stack::*, svg::*,
- text::*, uniform_list::*,
+ hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
+ stack::*, svg::*, text::*, uniform_list::*,
};
pub use crate::presenter::ChildView;
use crate::{
@@ -0,0 +1,92 @@
+use crate::{
+ elements::*,
+ fonts::TextStyle,
+ geometry::{rect::RectF, vector::Vector2F},
+ Action, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
+};
+use serde_json::json;
+
+use super::ContainerStyle;
+
+pub struct KeystrokeLabel {
+ action: Box<dyn Action>,
+ container_style: ContainerStyle,
+ text_style: TextStyle,
+}
+
+impl KeystrokeLabel {
+ pub fn new(
+ action: Box<dyn Action>,
+ container_style: ContainerStyle,
+ text_style: TextStyle,
+ ) -> Self {
+ Self {
+ action,
+ container_style,
+ text_style,
+ }
+ }
+}
+
+impl Element for KeystrokeLabel {
+ type LayoutState = ElementBox;
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: SizeConstraint,
+ cx: &mut LayoutContext,
+ ) -> (Vector2F, ElementBox) {
+ let mut element = if let Some(keystrokes) = cx.keystrokes_for_action(self.action.as_ref()) {
+ Flex::row()
+ .with_children(keystrokes.iter().map(|keystroke| {
+ Label::new(keystroke.to_string(), self.text_style.clone())
+ .contained()
+ .with_style(self.container_style)
+ .boxed()
+ }))
+ .boxed()
+ } else {
+ Empty::new().collapsed().boxed()
+ };
+
+ let size = element.layout(constraint, cx);
+ (size, element)
+ }
+
+ fn paint(
+ &mut self,
+ bounds: RectF,
+ visible_bounds: RectF,
+ element: &mut ElementBox,
+ cx: &mut PaintContext,
+ ) {
+ element.paint(bounds.origin(), visible_bounds, cx);
+ }
+
+ fn dispatch_event(
+ &mut self,
+ event: &Event,
+ _: RectF,
+ _: RectF,
+ element: &mut ElementBox,
+ _: &mut (),
+ cx: &mut EventContext,
+ ) -> bool {
+ element.dispatch_event(event, cx)
+ }
+
+ fn debug(
+ &self,
+ _: RectF,
+ element: &ElementBox,
+ _: &(),
+ cx: &crate::DebugContext,
+ ) -> serde_json::Value {
+ json!({
+ "type": "KeystrokeLabel",
+ "action": self.action.name(),
+ "child": element.debug(cx)
+ })
+ }
+}
@@ -185,7 +185,7 @@ impl Matcher {
return Some(binding.keystrokes.clone());
}
}
- todo!()
+ None
}
}
@@ -311,6 +311,34 @@ impl Keystroke {
}
}
+impl std::fmt::Display for Keystroke {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ if self.ctrl {
+ write!(f, "{}", "^")?;
+ }
+ if self.alt {
+ write!(f, "{}", "⎇")?;
+ }
+ if self.cmd {
+ write!(f, "{}", "⌘")?;
+ }
+ if self.shift {
+ write!(f, "{}", "⇧")?;
+ }
+ let key = match self.key.as_str() {
+ "backspace" => "⌫",
+ "up" => "↑",
+ "down" => "↓",
+ "left" => "←",
+ "right" => "→",
+ "tab" => "⇥",
+ "escape" => "⎋",
+ key => key,
+ };
+ write!(f, "{}", key)
+ }
+}
+
impl Context {
pub fn extend(&mut self, other: &Context) {
for v in &other.set {
@@ -4,6 +4,7 @@ use crate::{
font_cache::FontCache,
geometry::rect::RectF,
json::{self, ToJson},
+ keymap::Keystroke,
platform::{CursorStyle, Event},
text_layout::TextLayoutCache,
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
@@ -12,6 +13,7 @@ use crate::{
};
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json;
+use smallvec::SmallVec;
use std::{
collections::{HashMap, HashSet},
ops::{Deref, DerefMut},
@@ -148,6 +150,7 @@ impl Presenter {
cx: &'a mut MutableAppContext,
) -> LayoutContext<'a> {
LayoutContext {
+ window_id: self.window_id,
rendered_views: &mut self.rendered_views,
parents: &mut self.parents,
refreshing,
@@ -257,6 +260,7 @@ pub struct DispatchDirective {
}
pub struct LayoutContext<'a> {
+ window_id: usize,
rendered_views: &'a mut HashMap<usize, ElementBox>,
parents: &'a mut HashMap<usize, usize>,
view_stack: Vec<usize>,
@@ -281,6 +285,14 @@ impl<'a> LayoutContext<'a> {
self.view_stack.pop();
size
}
+
+ pub(crate) fn keystrokes_for_action(
+ &self,
+ action: &dyn Action,
+ ) -> Option<SmallVec<[Keystroke; 2]>> {
+ self.app
+ .keystrokes_for_action(self.window_id, &self.view_stack, action)
+ }
}
impl<'a> Deref for LayoutContext<'a> {
@@ -220,23 +220,11 @@ impl ProjectPanel {
menu.show(
action.position,
[
- ContextMenuItem::Item {
- label: "New File".to_string(),
- action: Box::new(AddFile),
- },
- ContextMenuItem::Item {
- label: "New Directory".to_string(),
- action: Box::new(AddDirectory),
- },
+ ContextMenuItem::item("New File".to_string(), AddFile),
+ ContextMenuItem::item("New Directory".to_string(), AddDirectory),
ContextMenuItem::Separator,
- ContextMenuItem::Item {
- label: "Rename".to_string(),
- action: Box::new(Rename),
- },
- ContextMenuItem::Item {
- label: "Delete".to_string(),
- action: Box::new(Delete),
- },
+ ContextMenuItem::item("Rename".to_string(), Rename),
+ ContextMenuItem::item("Delete".to_string(), Delete),
],
cx,
);
@@ -253,6 +253,7 @@ pub struct ContextMenuItem {
#[serde(flatten)]
pub container: ContainerStyle,
pub label: TextStyle,
+ pub keystroke: ContainedText,
}
#[derive(Debug, Deserialize, Default)]
@@ -15,9 +15,10 @@ export default function contextMenu(theme: Theme) {
shadow: shadow(theme),
item: {
label: text(theme, "sans", "secondary", { size: "sm" }),
+ keystroke: text(theme, "sans", "muted", { size: "sm", weight: "bold" }),
},
separator: {
background: "#00ff00"
- }
+ },
}
}