relm4/component/async/
builder.rs

1// Copyright 2021-2022 Aaron Erhardt <aaron.erhardt@t-online.de>
2// Copyright 2022 System76 <info@system76.com>
3// SPDX-License-Identifier: MIT or Apache-2.0
4
5use super::super::MessageBroker;
6use super::{AsyncComponent, AsyncComponentParts, AsyncConnector};
7use crate::channel::AsyncComponentSender;
8use crate::{
9    GuardedReceiver, Receiver, RelmContainerExt, RelmWidgetExt, RuntimeSenders, Sender,
10    late_initialization,
11};
12use gtk::glib;
13use gtk::prelude::{GtkWindowExt, NativeDialogExt};
14use std::any;
15use std::marker::PhantomData;
16use tracing::info_span;
17
18/// A component that is ready for docking and launch.
19#[derive(Debug)]
20pub struct AsyncComponentBuilder<C: AsyncComponent> {
21    /// The root widget of the component.
22    pub root: C::Root,
23    priority: glib::Priority,
24
25    pub(super) component: PhantomData<C>,
26}
27
28impl<C: AsyncComponent> Default for AsyncComponentBuilder<C> {
29    /// Prepares a component for initialization.
30    fn default() -> Self {
31        Self {
32            root: C::init_root(),
33            priority: glib::Priority::default(),
34            component: PhantomData,
35        }
36    }
37}
38
39impl<C: AsyncComponent> AsyncComponentBuilder<C> {
40    /// Configure the root widget before launching.
41    #[must_use]
42    pub fn update_root<F: FnOnce(&mut C::Root)>(mut self, func: F) -> Self {
43        func(&mut self.root);
44        self
45    }
46
47    /// Access the root widget before the component is initialized.
48    pub const fn widget(&self) -> &C::Root {
49        &self.root
50    }
51
52    /// Change the priority at which the messages of this component
53    /// are handled.
54    ///
55    /// + Use [`glib::Priority::HIGH`] for high priority event sources.
56    /// + Use [`glib::Priority::LOW`] for very low priority background tasks.
57    /// + Use [`glib::Priority::DEFAULT_IDLE`] for default priority idle functions.
58    /// + Use [`glib::Priority::HIGH_IDLE`] for high priority idle functions.
59    pub fn priority(mut self, priority: glib::Priority) -> Self {
60        self.priority = priority;
61        self
62    }
63}
64
65impl<C: AsyncComponent> AsyncComponentBuilder<C>
66where
67    C::Root: AsRef<gtk::Widget>,
68{
69    /// Attach the component's root widget to a given container.
70    #[must_use]
71    pub fn attach_to<T: RelmContainerExt<Child = gtk::Widget>>(self, container: &T) -> Self {
72        container.container_add(self.root.as_ref());
73
74        self
75    }
76}
77
78impl<C: AsyncComponent> AsyncComponentBuilder<C>
79where
80    C::Root: AsRef<gtk::Window> + Clone,
81{
82    /// Set the component's root widget transient for a given window.
83    /// This function doesn't require a [`gtk::Window`] as parameter,
84    /// but instead uses [`RelmWidgetExt::toplevel_window()`] to retrieve the toplevel
85    /// window of any [`gtk::Widget`].
86    /// Therefore, you don't have to pass a window to every component.
87    ///
88    /// If the root widget is a native dialog, such as [`gtk::FileChooserNative`],
89    /// you should use [`transient_for_native`][AsyncComponentBuilder::transient_for_native] instead.
90    #[must_use]
91    pub fn transient_for(self, widget: impl AsRef<gtk::Widget>) -> Self {
92        let widget = widget.as_ref().clone();
93        let root = self.root.clone();
94        late_initialization::register_callback(Box::new(move || {
95            if let Some(window) = widget.toplevel_window() {
96                root.as_ref().set_transient_for(Some(&window));
97            } else {
98                tracing::error!("Couldn't find root of transient widget");
99            }
100        }));
101
102        self
103    }
104}
105
106impl<C: AsyncComponent> AsyncComponentBuilder<C>
107where
108    C::Root: AsRef<gtk::NativeDialog> + Clone,
109{
110    /// Set the component's root widget transient for a given window.
111    /// This function doesn't require a [`gtk::Window`] as parameter,
112    /// but instead uses [`RelmWidgetExt::toplevel_window()`] to retrieve the toplevel
113    /// window of any [`gtk::Widget`].
114    /// Therefore, you don't have to pass a window to every component.
115    ///
116    /// Applicable to native dialogs only, such as [`gtk::FileChooserNative`].
117    /// If the root widget is a non-native dialog,
118    /// you should use [`transient_for`][AsyncComponentBuilder::transient_for] instead.
119    #[must_use]
120    pub fn transient_for_native(self, widget: impl AsRef<gtk::Widget>) -> Self {
121        let widget = widget.as_ref().clone();
122        let root = self.root.clone();
123        late_initialization::register_callback(Box::new(move || {
124            if let Some(window) = widget.toplevel_window() {
125                root.as_ref().set_transient_for(Some(&window));
126            } else {
127                tracing::error!("Couldn't find root of transient widget");
128            }
129        }));
130
131        self
132    }
133}
134
135impl<C: AsyncComponent> AsyncComponentBuilder<C> {
136    /// Starts the component, passing ownership to a future attached to a [gtk::glib::MainContext].
137    pub fn launch(self, payload: C::Init) -> AsyncConnector<C> {
138        // Used for all events to be processed by this component's internal service.
139        let (input_sender, input_receiver) = crate::channel::<C::Input>();
140
141        self.launch_with_input_channel(payload, input_sender, input_receiver)
142    }
143
144    /// Similar to [`launch()`](AsyncComponentBuilder::launch) but also initializes a [`MessageBroker`].
145    ///
146    /// # Panics
147    ///
148    /// This method panics if the message broker was already initialized in another launch.
149    pub fn launch_with_broker(
150        self,
151        payload: C::Init,
152        broker: &MessageBroker<C::Input>,
153    ) -> AsyncConnector<C> {
154        let (input_sender, input_receiver) = broker.get_channel();
155        self.launch_with_input_channel(
156            payload,
157            input_sender,
158            input_receiver.expect("Message broker launched multiple times"),
159        )
160    }
161
162    fn launch_with_input_channel(
163        self,
164        payload: C::Init,
165        input_sender: Sender<C::Input>,
166        input_receiver: Receiver<C::Input>,
167    ) -> AsyncConnector<C> {
168        let Self { root, priority, .. } = self;
169        let temp_widgets = C::init_loading_widgets(root.clone());
170
171        let RuntimeSenders {
172            output_sender,
173            output_receiver,
174            cmd_sender,
175            cmd_receiver,
176            shutdown_notifier,
177            shutdown_recipient,
178            shutdown_on_drop: destroy_on_drop,
179            mut shutdown_event,
180        } = RuntimeSenders::<C::Output, C::CommandOutput>::new();
181
182        // Encapsulates the senders used by component methods.
183        let component_sender = AsyncComponentSender::new(
184            input_sender.clone(),
185            output_sender.clone(),
186            cmd_sender,
187            shutdown_recipient,
188        );
189
190        let rt_root = root.clone();
191
192        // Spawns the component's service. It will receive both `Self::Input` and
193        // `Self::CommandOutput` messages. It will spawn commands as requested by
194        // updates, and send `Self::Output` messages externally.
195        crate::spawn_local_with_priority(priority, async move {
196            let mut state = C::init(payload, rt_root.clone(), component_sender.clone()).await;
197            drop(temp_widgets);
198
199            let mut cmd = GuardedReceiver::new(cmd_receiver);
200            let mut input = GuardedReceiver::new(input_receiver);
201
202            loop {
203                futures::select!(
204                    // Performs the model update, checking if the update requested a command.
205                    // Runs that command asynchronously in the background using tokio.
206                    message = input => {
207                        let AsyncComponentParts {
208                            model,
209                            widgets,
210                        } = &mut state;
211
212                        let span = info_span!(
213                            "update_with_view",
214                            input=?message,
215                            component=any::type_name::<C>(),
216                            id=model.id(),
217                        );
218                        let _enter = span.enter();
219
220                        model.update_with_view(widgets, message, component_sender.clone(), &rt_root).await;
221                    }
222
223                    // Handles responses from a command.
224                    message = cmd => {
225                        let AsyncComponentParts {
226                            model,
227                            widgets,
228                        } = &mut state;
229
230                        let span = info_span!(
231                            "update_cmd_with_view",
232                            cmd_output=?message,
233                            component=any::type_name::<C>(),
234                            id=model.id(),
235                        );
236                        let _enter = span.enter();
237
238                        model.update_cmd_with_view(widgets, message, component_sender.clone(), &rt_root).await;
239                    }
240
241                    // Triggered when the component is destroyed
242                    _ = shutdown_event => {
243                        let AsyncComponentParts {
244                            model,
245                            widgets,
246                        } = &mut state;
247
248                        model.shutdown(widgets, output_sender);
249
250                        shutdown_notifier.shutdown();
251
252                        return;
253                    }
254                );
255            }
256        });
257
258        // Give back a type for controlling the component service.
259        AsyncConnector {
260            widget: root,
261            sender: input_sender,
262            receiver: output_receiver,
263            shutdown_on_drop: destroy_on_drop,
264        }
265    }
266}