This commit is contained in:
Antonio Scandurra 2022-05-25 15:24:44 +02:00
parent 85ed7b41f1
commit a8483ba458
11 changed files with 219 additions and 65 deletions

1
Cargo.lock generated
View file

@ -980,6 +980,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"gpui", "gpui",
"settings", "settings",
"smallvec",
"theme", "theme",
] ]

View file

@ -11,3 +11,4 @@ doctest = false
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
smallvec = "1.6"

View file

@ -12,10 +12,22 @@ pub enum ContextMenuItem {
Separator, 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 { pub struct ContextMenu {
position: Vector2F, position: Vector2F,
items: Vec<ContextMenuItem>, items: Vec<ContextMenuItem>,
widest_item_index: usize,
selected_index: Option<usize>, selected_index: Option<usize>,
visible: bool, visible: bool,
} }
@ -36,28 +48,22 @@ impl View for ContextMenu {
return Empty::new().boxed(); return Empty::new().boxed();
} }
let style = cx.global::<Settings>().theme.context_menu.clone(); // Render the menu once at minimum width.
let mut collapsed_menu = self.render_menu::<()>(false, cx).boxed();
let mut widest_item = self.render_menu_item::<()>(self.widest_item_index, cx, &style); let expanded_menu = self
.render_menu::<Tag>(true, cx)
Overlay::new( .constrained()
Flex::column() .dynamically(move |constraint, cx| {
.with_children( SizeConstraint::strict_along(
(0..self.items.len()).map(|ix| self.render_menu_item::<Tag>(ix, cx, &style)), Axis::Horizontal,
collapsed_menu.layout(constraint, cx).x(),
) )
.constrained() })
.dynamically(move |constraint, cx| { .boxed();
SizeConstraint::strict_along(
Axis::Horizontal, Overlay::new(expanded_menu)
widest_item.layout(constraint, cx).x(), .with_abs_position(self.position)
) .boxed()
})
.contained()
.with_style(style.container)
.boxed(),
)
.with_abs_position(self.position)
.boxed()
} }
fn on_blur(&mut self, cx: &mut ViewContext<Self>) { fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
@ -72,7 +78,6 @@ impl ContextMenu {
position: Default::default(), position: Default::default(),
items: Default::default(), items: Default::default(),
selected_index: Default::default(), selected_index: Default::default(),
widest_item_index: Default::default(),
visible: false, visible: false,
} }
} }
@ -86,25 +91,31 @@ impl ContextMenu {
let mut items = items.into_iter().peekable(); let mut items = items.into_iter().peekable();
assert!(items.peek().is_some(), "must have at least one item"); assert!(items.peek().is_some(), "must have at least one item");
self.items = items.collect(); 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.position = position;
self.visible = true; self.visible = true;
cx.focus_self(); cx.focus_self();
cx.notify(); 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>( fn render_menu_item<T: 'static>(
&self, &self,
ix: usize, ix: usize,
expanded: bool,
cx: &mut RenderContext<ContextMenu>, cx: &mut RenderContext<ContextMenu>,
style: &theme::ContextMenu, style: &theme::ContextMenu,
) -> ElementBox { ) -> ElementBox {
@ -115,18 +126,35 @@ impl ContextMenu {
let style = style.item.style_for(state, Some(ix) == self.selected_index); let style = style.item.style_for(state, Some(ix) == self.selected_index);
Flex::row() Flex::row()
.with_child(Label::new(label.to_string(), style.label.clone()).boxed()) .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() .boxed()
}) })
.on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())) .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()))
.boxed() .boxed()
} }
ContextMenuItem::Separator => Empty::new() ContextMenuItem::Separator => {
.contained() let mut separator = Empty::new();
.with_style(style.separator) if !expanded {
.constrained() separator = separator.collapsed();
.with_height(1.) }
.flex(1., false) separator
.boxed(), .contained()
.with_style(style.separator)
.constrained()
.with_height(1.)
.boxed()
}
} }
} }
} }

View file

@ -1414,11 +1414,12 @@ impl MutableAppContext {
} }
/// Return keystrokes that would dispatch the given action closest to the focused view, if there are any. /// 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]>> { pub(crate) fn keystrokes_for_action(
let window_id = self.cx.platform.key_window_id()?; &self,
let (presenter, _) = self.presenters_and_platform_windows.get(&window_id)?; window_id: usize,
let dispatch_path = presenter.borrow().dispatch_path(&self.cx); dispatch_path: &[usize],
action: &dyn Action,
) -> Option<SmallVec<[Keystroke; 2]>> {
for view_id in dispatch_path.iter().rev() { for view_id in dispatch_path.iter().rev() {
let view = self let view = self
.cx .cx

View file

@ -8,6 +8,7 @@ mod expanded;
mod flex; mod flex;
mod hook; mod hook;
mod image; mod image;
mod keystroke_label;
mod label; mod label;
mod list; mod list;
mod mouse_event_handler; mod mouse_event_handler;
@ -20,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::*, event_handler::*, flex::*,
hook::*, image::*, label::*, list::*, mouse_event_handler::*, overlay::*, stack::*, svg::*, hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
text::*, uniform_list::*, stack::*, svg::*, text::*, uniform_list::*,
}; };
pub use crate::presenter::ChildView; pub use crate::presenter::ChildView;
use crate::{ use crate::{

View file

@ -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)
})
}
}

View file

@ -185,7 +185,7 @@ impl Matcher {
return Some(binding.keystrokes.clone()); 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 { impl Context {
pub fn extend(&mut self, other: &Context) { pub fn extend(&mut self, other: &Context) {
for v in &other.set { for v in &other.set {

View file

@ -4,6 +4,7 @@ use crate::{
font_cache::FontCache, font_cache::FontCache,
geometry::rect::RectF, geometry::rect::RectF,
json::{self, ToJson}, json::{self, ToJson},
keymap::Keystroke,
platform::{CursorStyle, Event}, platform::{CursorStyle, Event},
text_layout::TextLayoutCache, text_layout::TextLayoutCache,
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox,
@ -12,6 +13,7 @@ use crate::{
}; };
use pathfinder_geometry::vector::{vec2f, Vector2F}; use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json; use serde_json::json;
use smallvec::SmallVec;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
@ -148,6 +150,7 @@ impl Presenter {
cx: &'a mut MutableAppContext, cx: &'a mut MutableAppContext,
) -> LayoutContext<'a> { ) -> LayoutContext<'a> {
LayoutContext { LayoutContext {
window_id: self.window_id,
rendered_views: &mut self.rendered_views, rendered_views: &mut self.rendered_views,
parents: &mut self.parents, parents: &mut self.parents,
refreshing, refreshing,
@ -257,6 +260,7 @@ pub struct DispatchDirective {
} }
pub struct LayoutContext<'a> { pub struct LayoutContext<'a> {
window_id: usize,
rendered_views: &'a mut HashMap<usize, ElementBox>, rendered_views: &'a mut HashMap<usize, ElementBox>,
parents: &'a mut HashMap<usize, usize>, parents: &'a mut HashMap<usize, usize>,
view_stack: Vec<usize>, view_stack: Vec<usize>,
@ -281,6 +285,14 @@ impl<'a> LayoutContext<'a> {
self.view_stack.pop(); self.view_stack.pop();
size 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> { impl<'a> Deref for LayoutContext<'a> {

View file

@ -220,23 +220,11 @@ impl ProjectPanel {
menu.show( menu.show(
action.position, action.position,
[ [
ContextMenuItem::Item { ContextMenuItem::item("New File".to_string(), AddFile),
label: "New File".to_string(), ContextMenuItem::item("New Directory".to_string(), AddDirectory),
action: Box::new(AddFile),
},
ContextMenuItem::Item {
label: "New Directory".to_string(),
action: Box::new(AddDirectory),
},
ContextMenuItem::Separator, ContextMenuItem::Separator,
ContextMenuItem::Item { ContextMenuItem::item("Rename".to_string(), Rename),
label: "Rename".to_string(), ContextMenuItem::item("Delete".to_string(), Delete),
action: Box::new(Rename),
},
ContextMenuItem::Item {
label: "Delete".to_string(),
action: Box::new(Delete),
},
], ],
cx, cx,
); );

View file

@ -253,6 +253,7 @@ pub struct ContextMenuItem {
#[serde(flatten)] #[serde(flatten)]
pub container: ContainerStyle, pub container: ContainerStyle,
pub label: TextStyle, pub label: TextStyle,
pub keystroke: ContainedText,
} }
#[derive(Debug, Deserialize, Default)] #[derive(Debug, Deserialize, Default)]

View file

@ -15,9 +15,10 @@ export default function contextMenu(theme: Theme) {
shadow: shadow(theme), shadow: shadow(theme),
item: { item: {
label: text(theme, "sans", "secondary", { size: "sm" }), label: text(theme, "sans", "secondary", { size: "sm" }),
keystroke: text(theme, "sans", "muted", { size: "sm", weight: "bold" }),
}, },
separator: { separator: {
background: "#00ff00" background: "#00ff00"
} },
} }
} }