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:
parent
1778622960
commit
fd4b81c8fc
26 changed files with 559 additions and 335 deletions
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue