Allow actions to be deserialized from JSON

Introduce separate macro for implementing 'internal' actions which
are not intended to be loaded from keymaps.
This commit is contained in:
Max Brunsfeld 2022-04-08 15:32:56 -07:00
parent 1778622960
commit fd4b81c8fc
26 changed files with 559 additions and 335 deletions

View file

@ -715,12 +715,14 @@ type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
type GlobalObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
pub struct MutableAppContext {
weak_self: Option<rc::Weak<RefCell<Self>>>,
foreground_platform: Rc<dyn platform::ForegroundPlatform>,
assets: Arc<AssetCache>,
cx: AppContext,
action_deserializers: HashMap<&'static str, DeserializeActionCallback>,
capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
@ -773,6 +775,7 @@ impl MutableAppContext {
font_cache,
platform,
},
action_deserializers: HashMap::new(),
capture_actions: HashMap::new(),
actions: HashMap::new(),
global_actions: HashMap::new(),
@ -857,6 +860,18 @@ impl MutableAppContext {
.and_then(|(presenter, _)| presenter.borrow().debug_elements(self))
}
pub fn deserialize_action(
&self,
name: &str,
argument: Option<&str>,
) -> Result<Box<dyn Action>> {
let callback = self
.action_deserializers
.get(name)
.ok_or_else(|| anyhow!("unknown action {}", name))?;
callback(argument.unwrap_or("{}"))
}
pub fn add_action<A, V, F>(&mut self, handler: F)
where
A: Action,
@ -899,6 +914,10 @@ impl MutableAppContext {
},
);
self.action_deserializers
.entry(A::qualified_name())
.or_insert(A::from_json_str);
let actions = if capture {
&mut self.capture_actions
} else {
@ -934,6 +953,10 @@ impl MutableAppContext {
handler(action, cx);
});
self.action_deserializers
.entry(A::qualified_name())
.or_insert(A::from_json_str);
if self
.global_actions
.insert(TypeId::of::<A>(), handler)
@ -4575,7 +4598,8 @@ impl RefCounts {
#[cfg(test)]
mod tests {
use super::*;
use crate::{elements::*, impl_actions};
use crate::{actions, elements::*, impl_actions};
use serde::Deserialize;
use smol::future::poll_once;
use std::{
cell::Cell,
@ -5683,6 +5707,42 @@ mod tests {
);
}
#[crate::test(self)]
fn test_deserialize_actions(cx: &mut MutableAppContext) {
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct ComplexAction {
arg: String,
count: usize,
}
actions!(test::something, [SimpleAction]);
impl_actions!(test::something, [ComplexAction]);
cx.add_global_action(move |_: &SimpleAction, _: &mut MutableAppContext| {});
cx.add_global_action(move |_: &ComplexAction, _: &mut MutableAppContext| {});
let action1 = cx
.deserialize_action(
"test::something::ComplexAction",
Some(r#"{"arg": "a", "count": 5}"#),
)
.unwrap();
let action2 = cx
.deserialize_action("test::something::SimpleAction", None)
.unwrap();
assert_eq!(
action1.as_any().downcast_ref::<ComplexAction>().unwrap(),
&ComplexAction {
arg: "a".to_string(),
count: 5,
}
);
assert_eq!(
action2.as_any().downcast_ref::<SimpleAction>().unwrap(),
&SimpleAction
);
}
#[crate::test(self)]
fn test_dispatch_action(cx: &mut MutableAppContext) {
struct ViewA {
@ -5721,32 +5781,32 @@ mod tests {
}
}
#[derive(Clone)]
pub struct Action(pub &'static str);
#[derive(Clone, Deserialize)]
pub struct Action(pub String);
impl_actions!(test, [Action]);
let actions = Rc::new(RefCell::new(Vec::new()));
{
cx.add_global_action({
let actions = actions.clone();
cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| {
move |_: &Action, _: &mut MutableAppContext| {
actions.borrow_mut().push("global".to_string());
});
}
}
});
{
cx.add_action({
let actions = actions.clone();
cx.add_action(move |view: &mut ViewA, action: &Action, cx| {
move |view: &mut ViewA, action: &Action, cx| {
assert_eq!(action.0, "bar");
cx.propagate_action();
actions.borrow_mut().push(format!("{} a", view.id));
});
}
}
});
{
cx.add_action({
let actions = actions.clone();
cx.add_action(move |view: &mut ViewA, _: &Action, cx| {
move |view: &mut ViewA, _: &Action, cx| {
if view.id != 1 {
cx.add_view(|cx| {
cx.propagate_action(); // Still works on a nested ViewContext
@ -5754,32 +5814,32 @@ mod tests {
});
}
actions.borrow_mut().push(format!("{} b", view.id));
});
}
}
});
{
cx.add_action({
let actions = actions.clone();
cx.add_action(move |view: &mut ViewB, _: &Action, cx| {
move |view: &mut ViewB, _: &Action, cx| {
cx.propagate_action();
actions.borrow_mut().push(format!("{} c", view.id));
});
}
}
});
{
cx.add_action({
let actions = actions.clone();
cx.add_action(move |view: &mut ViewB, _: &Action, cx| {
move |view: &mut ViewB, _: &Action, cx| {
cx.propagate_action();
actions.borrow_mut().push(format!("{} d", view.id));
});
}
}
});
{
cx.capture_action({
let actions = actions.clone();
cx.capture_action(move |view: &mut ViewA, _: &Action, cx| {
move |view: &mut ViewA, _: &Action, cx| {
cx.propagate_action();
actions.borrow_mut().push(format!("{} capture", view.id));
});
}
}
});
let (window_id, view_1) = cx.add_window(Default::default(), |_| ViewA { id: 1 });
let view_2 = cx.add_view(window_id, |_| ViewB { id: 2 });
@ -5789,7 +5849,7 @@ mod tests {
cx.dispatch_action(
window_id,
vec![view_1.id(), view_2.id(), view_3.id(), view_4.id()],
&Action("bar"),
&Action("bar".to_string()),
);
assert_eq!(
@ -5812,7 +5872,7 @@ mod tests {
cx.dispatch_action(
window_id,
vec![view_2.id(), view_3.id(), view_4.id()],
&Action("bar"),
&Action("bar".to_string()),
);
assert_eq!(
@ -5832,8 +5892,8 @@ mod tests {
#[crate::test(self)]
fn test_dispatch_keystroke(cx: &mut MutableAppContext) {
#[derive(Clone)]
pub struct Action(pub &'static str);
#[derive(Clone, Deserialize)]
pub struct Action(String);
impl_actions!(test, [Action]);
@ -5887,16 +5947,20 @@ mod tests {
// "a" and "b" in its context, but not "c".
cx.add_bindings(vec![keymap::Binding::new(
"a",
Action("a"),
Action("a".to_string()),
Some("a && b && !c"),
)]);
cx.add_bindings(vec![keymap::Binding::new("b", Action("b"), None)]);
cx.add_bindings(vec![keymap::Binding::new(
"b",
Action("b".to_string()),
None,
)]);
let actions = Rc::new(RefCell::new(Vec::new()));
{
cx.add_action({
let actions = actions.clone();
cx.add_action(move |view: &mut View, action: &Action, cx| {
move |view: &mut View, action: &Action, cx| {
if action.0 == "a" {
actions.borrow_mut().push(format!("{} a", view.id));
} else {
@ -5905,14 +5969,15 @@ mod tests {
.push(format!("{} {}", view.id, action.0));
cx.propagate_action();
}
});
}
{
}
});
cx.add_global_action({
let actions = actions.clone();
cx.add_global_action(move |action: &Action, _| {
move |action: &Action, _| {
actions.borrow_mut().push(format!("global {}", action.0));
});
}
}
});
cx.dispatch_keystroke(
window_id,

View file

@ -2,55 +2,108 @@ use std::any::{Any, TypeId};
pub trait Action: 'static {
fn id(&self) -> TypeId;
fn namespace(&self) -> &'static str;
fn name(&self) -> &'static str;
fn as_any(&self) -> &dyn Any;
fn boxed_clone(&self) -> Box<dyn Action>;
fn boxed_clone_as_any(&self) -> Box<dyn Any>;
fn qualified_name() -> &'static str
where
Self: Sized;
fn from_json_str(json: &str) -> anyhow::Result<Box<dyn Action>>
where
Self: Sized;
}
/// Define a set of unit struct types that all implement the `Action` trait.
///
/// The first argument is a namespace that will be associated with each of
/// the given action types, to ensure that they have globally unique
/// qualified names for use in keymap files.
#[macro_export]
macro_rules! impl_actions {
macro_rules! actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
impl $crate::action::Action for $name {
fn id(&self) -> std::any::TypeId {
std::any::TypeId::of::<$name>()
}
fn namespace(&self) -> &'static str {
stringify!($namespace)
}
fn name(&self) -> &'static str {
stringify!($name)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn boxed_clone(&self) -> Box<dyn $crate::action::Action> {
Box::new(self.clone())
}
fn boxed_clone_as_any(&self) -> Box<dyn std::any::Any> {
Box::new(self.clone())
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct $name;
$crate::__impl_action! {
$namespace,
$name,
fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
Ok(Box::new(Self))
}
}
)*
};
}
/// Implement the `Action` trait for a set of existing types.
///
/// The first argument is a namespace that will be associated with each of
/// the given action types, to ensure that they have globally unique
/// qualified names for use in keymap files.
#[macro_export]
macro_rules! actions {
macro_rules! impl_actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct $name;
$crate::__impl_action! {
$namespace,
$name,
fn from_json_str(json: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
Ok(Box::new($crate::serde_json::from_str::<Self>(json)?))
}
}
)*
$crate::impl_actions!($namespace, [ $($name),* ]);
};
}
/// Implement the `Action` trait for a set of existing types that are
/// not intended to be constructed via a keymap file, but only dispatched
/// internally.
#[macro_export]
macro_rules! impl_internal_actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
$crate::__impl_action! {
$namespace,
$name,
fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
Err($crate::anyhow::anyhow!("internal action"))
}
}
)*
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __impl_action {
($namespace:path, $name:ident, $from_json_fn:item) => {
impl $crate::action::Action for $name {
fn name(&self) -> &'static str {
stringify!($name)
}
fn qualified_name() -> &'static str {
concat!(
stringify!($namespace),
"::",
stringify!($name),
)
}
fn id(&self) -> std::any::TypeId {
std::any::TypeId::of::<$name>()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn boxed_clone(&self) -> Box<dyn $crate::Action> {
Box::new(self.clone())
}
$from_json_fn
}
};
}

View file

@ -33,3 +33,6 @@ pub use platform::{Event, NavigationDirection, PathPromptOptions, Platform, Prom
pub use presenter::{
Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
};
pub use anyhow;
pub use serde_json;

View file

@ -328,6 +328,8 @@ impl ContextPredicate {
#[cfg(test)]
mod tests {
use serde::Deserialize;
use crate::{actions, impl_actions};
use super::*;
@ -419,30 +421,18 @@ mod tests {
#[test]
fn test_matcher() -> anyhow::Result<()> {
#[derive(Clone)]
pub struct A(pub &'static str);
#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
pub struct A(pub String);
impl_actions!(test, [A]);
actions!(test, [B, Ab]);
impl PartialEq for A {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for A {}
impl Debug for A {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "A({:?})", &self.0)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ActionArg {
a: &'static str,
}
let keymap = Keymap(vec![
Binding::new("a", A("x"), Some("a")),
Binding::new("a", A("x".to_string()), Some("a")),
Binding::new("b", B, Some("a")),
Binding::new("a b", Ab, Some("a || b")),
]);
@ -456,40 +446,54 @@ mod tests {
let mut matcher = Matcher::new(keymap);
// Basic match
assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
assert_eq!(
downcast(&matcher.test_keystroke("a", 1, &ctx_a)),
Some(&A("x".to_string()))
);
// Multi-keystroke match
assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
assert!(matcher.test_keystroke("a", 1, &ctx_b).is_none());
assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_b)), Some(&Ab));
// Failed matches don't interfere with matching subsequent keys
assert_eq!(matcher.test_keystroke::<A>("x", 1, &ctx_a), None);
assert_eq!(matcher.test_keystroke("a", 1, &ctx_a), Some(A("x")));
assert!(matcher.test_keystroke("x", 1, &ctx_a).is_none());
assert_eq!(
downcast(&matcher.test_keystroke("a", 1, &ctx_a)),
Some(&A("x".to_string()))
);
// Pending keystrokes are cleared when the context changes
assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
assert_eq!(matcher.test_keystroke("b", 1, &ctx_a), Some(B));
assert!(&matcher.test_keystroke("a", 1, &ctx_b).is_none());
assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_a)), Some(&B));
let mut ctx_c = Context::default();
ctx_c.set.insert("c".into());
// Pending keystrokes are maintained per-view
assert_eq!(matcher.test_keystroke::<A>("a", 1, &ctx_b), None);
assert_eq!(matcher.test_keystroke::<A>("a", 2, &ctx_c), None);
assert_eq!(matcher.test_keystroke("b", 1, &ctx_b), Some(Ab));
assert!(matcher.test_keystroke("a", 1, &ctx_b).is_none());
assert!(matcher.test_keystroke("a", 2, &ctx_c).is_none());
assert_eq!(downcast(&matcher.test_keystroke("b", 1, &ctx_b)), Some(&Ab));
Ok(())
}
fn downcast<'a, A: Action>(action: &'a Option<Box<dyn Action>>) -> Option<&'a A> {
action
.as_ref()
.and_then(|action| action.as_any().downcast_ref())
}
impl Matcher {
fn test_keystroke<A>(&mut self, keystroke: &str, view_id: usize, cx: &Context) -> Option<A>
where
A: Action + Debug + Eq,
{
fn test_keystroke(
&mut self,
keystroke: &str,
view_id: usize,
cx: &Context,
) -> Option<Box<dyn Action>> {
if let MatchResult::Action(action) =
self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, cx)
{
Some(*action.boxed_clone_as_any().downcast().unwrap())
Some(action.boxed_clone())
} else {
None
}

View file

@ -1,3 +1,5 @@
use serde::Deserialize;
use crate::{
actions, elements::*, impl_actions, AppContext, Entity, MutableAppContext, RenderContext, View,
ViewContext, WeakViewHandle,
@ -25,7 +27,7 @@ pub enum ItemType {
Unselected,
}
#[derive(Clone)]
#[derive(Clone, Deserialize)]
pub struct SelectItem(pub usize);
actions!(select, [ToggleSelect]);