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 = [
"gpui",
"settings",
"smallvec",
"theme",
]

View File

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

View File

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

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.
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

View File

@ -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::{

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());
}
}
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 {

View File

@ -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> {

View File

@ -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,
);

View File

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

View File

@ -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"
}
},
}
}