Skip to main content

ferroui_xaml_interactions/core/
async_action_group.rs

1//! Port of `Core/AsyncActionGroup.cs`.
2
3use super::{attach_actions_to_logical_tree, detach_actions_from_logical_tree};
4use crate::action_task::ActionTask;
5use ferroui_base::logical_tree::LogicalTreeAttachmentEventArgs;
6use ferroui_base::{
7    ferro_class, ferro_class_info, ferro_properties, instantiate, BoxedValue, FerroObject, FerroObjectImpl,
8    FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElementImpl, StyledElementImplExt,
9    StyledProperty,
10};
11use ferroui_xaml_interactivity::{ActionCollection, IAction, StyledElementAction, StyledElementActionImpl};
12use std::rc::Rc;
13
14/// Defines the mode of execution for [`AsyncActionGroup`].
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
16pub enum AsyncActionMode {
17    /// Actions are executed sequentially. If an action returns a task, execution waits for it to
18    /// complete before moving to the next action.
19    #[default]
20    Sequence,
21
22    /// Actions are executed in parallel.
23    Parallel,
24}
25
26impl std::fmt::Display for AsyncActionMode {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.write_str(match self {
29            Self::Sequence => "Sequence",
30            Self::Parallel => "Parallel",
31        })
32    }
33}
34
35/// An action that executes its child actions asynchronously, either in sequence or in parallel.
36///
37/// While the group is in the logical tree, its styled element actions are its logical children.
38#[repr(C)]
39pub struct AsyncActionGroup {
40    base: StyledElementAction,
41}
42
43ferro_class!(AsyncActionGroup: StyledElementAction);
44ferro_class_info!(AsyncActionGroup {
45    new: AsyncActionGroup::new,
46    markup: {
47        content: Actions,
48    },
49});
50
51ferro_properties! {
52    impl AsyncActionGroup {
53        /// Identifies the `Mode` property.
54        pub fn mode_property() -> StyledProperty<AsyncActionMode> {
55            FerroProperty::register::<AsyncActionGroup, _>("Mode", AsyncActionMode::Sequence)
56        }
57
58        /// Identifies the `Actions` property.
59        pub fn actions_property() -> StyledProperty<Option<ActionCollection>> {
60            FerroProperty::register::<AsyncActionGroup, _>("Actions", None)
61        }
62    }
63}
64
65ferroui_base::ferro_overrides! {
66    impl FerroObjectImpl for AsyncActionGroup {
67        /// The constructor body: the group starts with an empty collection of actions.
68        fn constructed(this: &Self) {
69            Self::parent_constructed(this);
70            this.set_current_value(Self::actions_property(), Some(ActionCollection::new()));
71        }
72
73        fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
74            Self::parent_on_property_changed(this, change);
75
76            if change.property() == Self::actions_property().as_property() {
77                let old_actions = change.get_old_value::<Option<ActionCollection>>().flatten();
78                let new_actions = change.get_new_value::<Option<ActionCollection>>();
79
80                if let Some(old_actions) = &old_actions {
81                    if this.is_attached_to_logical_tree() {
82                        detach_actions_from_logical_tree(Some(old_actions), this);
83                    }
84                }
85
86                if let Some(new_actions) = &new_actions {
87                    if this.is_attached_to_logical_tree() {
88                        attach_actions_to_logical_tree(Some(new_actions), this);
89                    }
90                }
91            }
92        }
93    }
94}
95
96ferroui_base::ferro_overrides! {
97    impl StyledElementImpl for AsyncActionGroup {
98        fn on_attached_to_logical_tree(this: &Self, e: &LogicalTreeAttachmentEventArgs) {
99            Self::parent_on_attached_to_logical_tree(this, e);
100            attach_actions_to_logical_tree(this.actions().as_ref(), this);
101        }
102
103        fn on_detached_from_logical_tree(this: &Self, e: &LogicalTreeAttachmentEventArgs) {
104            detach_actions_from_logical_tree(this.actions().as_ref(), this);
105            Self::parent_on_detached_from_logical_tree(this, e);
106        }
107    }
108}
109
110impl StyledElementActionImpl for AsyncActionGroup {
111    /// Executes the child actions and returns the task of their execution ([`ActionTask`]).
112    fn execute(this: &Self, sender: Option<&BoxedValue>, parameter: Option<&BoxedValue>) -> Option<BoxedValue> {
113        this.actions()?;
114
115        // The execution must return synchronously; the caller who wants to wait for it awaits the
116        // task it returns.
117        Some(Rc::new(this.execute_async(sender, parameter)))
118    }
119}
120
121/// The task a result of an action is, if it is one (`result is Task task`).
122fn task_of(result: Option<&BoxedValue>) -> Option<ActionTask> {
123    result.and_then(|result| result.downcast_ref::<ActionTask>()).cloned()
124}
125
126/// Executes the actions of `items` from `start` in sequence, awaiting the task an action returns
127/// before the next action.
128async fn execute_sequence(
129    items: Rc<Vec<Ref<FerroObject>>>,
130    start: usize,
131    sender: Option<BoxedValue>,
132    parameter: Option<BoxedValue>,
133) {
134    for item in items.iter().skip(start) {
135        let result = match <dyn IAction>::of(item) {
136            Some(action) => action.execute(sender.as_ref(), parameter.as_ref()),
137            None => continue,
138        };
139
140        if let Some(task) = task_of(result.as_ref()) {
141            let _ = task.await;
142        }
143    }
144}
145
146impl AsyncActionGroup {
147    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
148    pub fn construct() -> Self {
149        Self { base: StyledElementAction::construct() }
150    }
151
152    /// Creates an action.
153    pub fn new() -> Ref<Self> {
154        instantiate(Self::construct())
155    }
156
157    /// Gets or sets the execution mode. This is a property of the object model.
158    pub fn mode(&self) -> AsyncActionMode {
159        self.get_value(Self::mode_property())
160    }
161
162    pub fn set_mode(&self, value: AsyncActionMode) {
163        self.set_value(Self::mode_property(), value)
164    }
165
166    /// Gets or sets the collection of actions to execute. This is a property of the object model.
167    pub fn actions(&self) -> Option<ActionCollection> {
168        self.get_value(Self::actions_property())
169    }
170
171    pub fn set_actions(&self, value: Option<ActionCollection>) {
172        self.set_value(Self::actions_property(), value)
173    }
174
175    /// The asynchronous execution of the actions (`ExecuteAsync`): it runs synchronously up to the
176    /// first task to wait for, as an asynchronous method does, and the rest runs in jobs of the
177    /// dispatcher.
178    fn execute_async(&self, sender: Option<&BoxedValue>, parameter: Option<&BoxedValue>) -> ActionTask {
179        let Some(actions) = self.actions() else {
180            return ActionTask::completed();
181        };
182
183        let items = actions.snapshot();
184
185        if self.mode() == AsyncActionMode::Sequence {
186            for (index, item) in items.iter().enumerate() {
187                let Some(action) = <dyn IAction>::of(item) else {
188                    continue;
189                };
190
191                let result = action.execute(sender, parameter);
192                if let Some(task) = task_of(result.as_ref()) {
193                    let rest = items.clone();
194                    let sender = sender.cloned();
195                    let parameter = parameter.cloned();
196                    return ActionTask::spawn(async move {
197                        let _ = task.await;
198                        execute_sequence(rest, index + 1, sender, parameter).await;
199                    });
200                }
201            }
202
203            return ActionTask::completed();
204        }
205
206        // Parallel.
207        let mut tasks = Vec::new();
208        for item in items.iter() {
209            let Some(action) = <dyn IAction>::of(item) else {
210                continue;
211            };
212
213            let result = action.execute(sender, parameter);
214            if let Some(task) = task_of(result.as_ref()) {
215                tasks.push(task);
216            }
217        }
218
219        if tasks.is_empty() {
220            return ActionTask::completed();
221        }
222
223        ActionTask::spawn(async move {
224            for task in tasks {
225                let _ = task.await;
226            }
227        })
228    }
229}