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