Skip to main content

ferroui_base/data/converters/
method_to_command_converter.rs

1use crate::data::core::plugins::InpcPropertyAccessor;
2use crate::data::core::{ValueType, ValueTypes};
3use crate::data::model::{Event, ICommand};
4use crate::metadata::{attributes, MarkupAttributeValue, MarkupDelegate, MarkupMethod, MarkupValue};
5use crate::reactive::{Disposable, IDisposable};
6use crate::threading::{Dispatcher, DispatcherPriority};
7use crate::BoxedValue;
8use std::cell::Cell;
9use std::rc::{Rc, Weak};
10
11/// A command made from the delegate of a method: what a string-path binding
12/// of a method to a command property produces.
13///
14/// Executing the command calls the method, with the command parameter
15/// converted to the type of its parameter if it has one. A method
16/// `Can<Name>` with one "any value" parameter, declared by the type that
17/// declares the method (or one of its base types), answers whether the
18/// command can execute; the properties named by its `DependsOn` attributes
19/// re-query it when the target of the delegate reports that they changed.
20///
21/// A failure of the method (its `Err`, or a command parameter that cannot be
22/// converted to a parameter that takes no null) panics: a command has no
23/// other way to report what the managed original throws from `Execute`.
24///
25/// The command has the target of the delegate as the delegate has it. The
26/// delegate a binding reads from its source
27/// ([`MarkupDelegate::for_method_of_source`]) does not keep the source
28/// alive (DEVIATIONS.md, Bindings): once the source is gone the command
29/// cannot execute and executing it does nothing, as the command of a
30/// compiled binding path
31/// ([`MethodCommand`](crate::data::core::expression_nodes::MethodCommand)).
32pub struct MethodToCommandConverter {
33    this: Weak<MethodToCommandConverter>,
34    action: MarkupDelegate,
35    /// The type of the parameter of the method; `None` without a parameter.
36    parameter_type: Option<ValueType>,
37    can_execute: Option<&'static MarkupMethod>,
38    dependency_properties: Vec<&'static str>,
39    token: Cell<Option<u64>>,
40    can_execute_changed: Event<()>,
41}
42
43impl MethodToCommandConverter {
44    /// Creates the command of `action`: the delegate of a declared method
45    /// ([`MarkupDelegate::for_method`]). For any other delegate the command
46    /// calls it with the command parameter and can always execute.
47    pub fn new(action: &MarkupDelegate) -> Rc<Self> {
48        let method = action.method();
49        let parameter_type = method.and_then(|method| method.parameters.first()).map(|parameter| parameter());
50
51        let can_execute = method.and_then(|method| {
52            let name = format!("Can{}", method.name);
53            let mut current = action.declaring_type();
54            let mut depth = 0;
55            while let Some(markup) = current {
56                let found = markup
57                    .find_methods(&name)
58                    .find(|m| m.parameters.len() == 1 && m.parameters[0]().is_object() && m.is_static == method.is_static);
59                if found.is_some() {
60                    return found;
61                }
62                depth += 1;
63                current = if depth < 64 { markup.base_type() } else { None };
64            }
65            None
66        });
67        let dependency_properties: Vec<&'static str> = can_execute
68            .map(|can_execute| {
69                can_execute
70                    .attributes
71                    .iter()
72                    .filter(|attribute| attribute.name == attributes::DEPENDS_ON)
73                    .filter_map(|attribute| match attribute.arguments.first() {
74                        Some(MarkupAttributeValue::Str(name)) => Some(*name),
75                        _ => None,
76                    })
77                    .collect()
78            })
79            .unwrap_or_default();
80
81        let this = Rc::new_cyclic(|this: &Weak<Self>| Self {
82            this: this.clone(),
83            action: action.clone(),
84            parameter_type,
85            can_execute,
86            dependency_properties,
87            token: Cell::new(None),
88            can_execute_changed: Event::new(),
89        });
90
91        if !this.dependency_properties.is_empty() {
92            let target = action.target();
93            if let Some(inpc) = target.as_ref().and_then(|target| InpcPropertyAccessor::find_notifier(&**target)) {
94                // The target does not keep the command alive.
95                let weak = this.this.clone();
96                let token = inpc.property_changed().add(Rc::new(move |name: &str| {
97                    if let Some(this) = weak.upgrade() {
98                        this.on_property_changed(name);
99                    }
100                }));
101                this.token.set(Some(token));
102            }
103        }
104        this
105    }
106
107    fn on_property_changed(&self, name: &str) {
108        if name.trim().is_empty() || self.dependency_properties.contains(&name) {
109            let weak = self.this.clone();
110            Dispatcher::current_dispatcher().post_local(
111                move || {
112                    if let Some(this) = weak.upgrade() {
113                        this.can_execute_changed.raise(&());
114                    }
115                },
116                DispatcherPriority::INPUT,
117            );
118        }
119    }
120
121    fn arguments(&self, parameter: Option<&BoxedValue>) -> Vec<MarkupValue> {
122        match self.parameter_type {
123            None => Vec::new(),
124            Some(type_) if type_.is_object() => vec![parameter.cloned()],
125            // A parameter that cannot be converted is passed as null.
126            Some(type_) => vec![ValueTypes::try_convert(parameter, type_).flatten()],
127        }
128    }
129}
130
131impl ICommand for MethodToCommandConverter {
132    fn can_execute(&self, parameter: Option<&BoxedValue>) -> bool {
133        if !self.action.is_target_alive() {
134            return false;
135        }
136        let Some(can_execute) = self.can_execute else { return true };
137        let result = match can_execute.is_static {
138            true => (can_execute.invoke)(&[parameter.cloned()]),
139            false => (can_execute.invoke)(&[self.action.target(), parameter.cloned()]),
140        };
141        match result {
142            Ok(value) => crate::metadata::from_markup_value::<bool>(&value).unwrap_or(false),
143            Err(error) => panic!("{}: {error}", can_execute.name),
144        }
145    }
146
147    fn execute(&self, parameter: Option<&BoxedValue>) {
148        let Some(method) = self.action.method() else {
149            self.action.invoke(&[parameter.cloned()]);
150            return;
151        };
152        if !self.action.is_target_alive() {
153            return;
154        }
155        if let Err(error) = self.action.try_invoke(&self.arguments(parameter)) {
156            panic!("{}: {error}", method.name);
157        }
158    }
159
160    fn can_execute_changed(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
161        let token = self.can_execute_changed.add(Rc::new(move |_: &()| handler()));
162        let weak = self.this.clone();
163        Disposable::create(move || {
164            if let Some(this) = weak.upgrade() {
165                this.can_execute_changed.remove(token);
166            }
167        })
168    }
169}
170
171impl Drop for MethodToCommandConverter {
172    fn drop(&mut self) {
173        if let Some(token) = self.token.take() {
174            let target = self.action.target();
175            if let Some(inpc) = target.as_ref().and_then(|target| InpcPropertyAccessor::find_notifier(&**target)) {
176                inpc.property_changed().remove(token);
177            }
178        }
179    }
180}
181
182/// The command of a delegate for a command-typed target: the conversion the
183/// converters of string-path bindings apply before any other. `None` if
184/// `target_type` is not the command contract or `value` is not the delegate
185/// of a declared method with at most one parameter.
186pub(crate) fn method_to_command(value: &BoxedValue, target_type: ValueType) -> Option<BoxedValue> {
187    let nullable = target_type.is::<Option<Rc<dyn ICommand>>>();
188    if !nullable && !target_type.is::<Rc<dyn ICommand>>() {
189        return None;
190    }
191    let delegate = value.downcast_ref::<MarkupDelegate>()?;
192    if delegate.method()?.parameters.len() > 1 {
193        return None;
194    }
195    let command: Rc<dyn ICommand> = MethodToCommandConverter::new(delegate);
196    Some(match nullable {
197        true => Rc::new(Some(command)),
198        false => Rc::new(command),
199    })
200}