maycoon_widgets/
fetcher.rs

1use maycoon_core::app::context::AppContext;
2use maycoon_core::app::info::AppInfo;
3use maycoon_core::app::update::Update;
4use maycoon_core::layout::{LayoutNode, LayoutStyle, StyleNode};
5use maycoon_core::tasks::fetcher::Fetcher;
6use maycoon_core::vgi::Scene;
7use maycoon_core::widget::Widget;
8use maycoon_theme::id::WidgetId;
9use maycoon_theme::theme::Theme;
10use std::future::Future;
11
12/// A widget to build an inner widget from an asynchronous task.
13/// This is a [Widget] version of [Fetcher].
14///
15/// It is similar to the [FutureBuilder](https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html) from Flutter.
16///
17/// ### Async + UI
18/// When working with a future, you usually need to block on it to produce a value.
19/// This would however block the main thread and freeze the whole application temporarily until the task is done.
20/// Blocking is less than ideal for any UI application,
21/// so the [Fetcher] and [WidgetFetcher] structures exist to run a task in the background,
22/// while still providing the user with a smooth interface experience.
23///
24/// The [WidgetFetcher] will spawn the given task in the background.
25/// While the task is still running/loading, the fetcher will still be able to produce a widget via a given factory function.
26/// This factory function takes `Option<T>` as an argument, where `T` is the output of the spawned task.
27/// If the task is not ready yet, the factory will be called with `None`, otherwise it will be called with `Some(T)`.
28///
29/// ### Spawning the task
30/// By default, the task is spawned inside the [TaskRunner](maycoon_core::tasks::runner::TaskRunner) in a non-blocking manner.
31/// If you want to spawn a blocking task (which is however, only supported on native platforms), use [WidgetFetcher::new_blocking]
32///
33/// **NOTE:** If the future is spawned on a thread pool or not,
34/// is up to the [TaskRunner](maycoon_core::tasks::runner::TaskRunner) implementation.
35/// Blocking tasks will always be spawned on a thread pool.
36///
37/// ### Note for the Web
38/// On the web, blocking a task is not possible and will freeze the browser,
39/// so only futures can be spawned.
40/// Furthermore, the futures will be executed on the local thread,
41/// since WebAssembly doesn't support threading out-of-the-box yet.
42///
43/// ### Workflow of a [WidgetFetcher].
44/// 1. Run the task in the background using the [TaskRunner](tasks::runner::TaskRunner).
45/// 2. Construct the widget with [None] passed into the factory function (while task is still loading).
46/// 3. Once the task is done, the UI is updated with the new result and an [Update] is triggered.
47///
48/// ### Theming
49/// The widget itself only draws the underlying widget, so theming is useless.
50///
51/// The [WidgetId] is equal to `maycoon-widgets:WidgetFetcher`.
52pub struct WidgetFetcher<T: Send + 'static, W: Widget> {
53    fetcher: Fetcher<T, W>,
54    update: Update,
55}
56
57impl<T: Send + 'static, W: Widget> WidgetFetcher<T, W> {
58    /// Creates a new [WidgetFetcher] with parameters:
59    /// - `future`: The future to execute.
60    /// - `update`: The update to trigger when the data is updated (when loading is done).
61    /// - `render`: The function to render the widget. It takes a possible task result as the only parameter.
62    ///
63    /// Unlike [WidgetFetcher::new_blocking]. this will spawn a future in the background.
64    /// It's up to the task runner implementation, if the task is spawned on a thread pool or not.
65    #[inline(always)]
66    pub fn new<Fut>(future: Fut, render: impl Fn(Option<T>) -> W + 'static, update: Update) -> Self
67    where
68        Fut: Future<Output = T> + Send + 'static,
69    {
70        Self {
71            fetcher: Fetcher::spawn(future, render),
72            update,
73        }
74    }
75
76    /// Creates a new [WidgetFetcher] with parameters:
77    /// - `func`: The blocking task to execute.
78    /// - `update`: The update to trigger when the data is updated (when loading is done).
79    /// - `render`: The function to render the widget. It takes a possible task result as the only parameter.
80    ///
81    /// Unlike [WidgetFetcher::new], this takes a function which will be run on a separate thread pool.
82    /// This is only supported on native platforms.
83    #[inline(always)]
84    #[cfg(native)]
85    pub fn new_blocking<F>(
86        func: F,
87        render: impl Fn(Option<T>) -> W + 'static,
88        update: Update,
89    ) -> Self
90    where
91        F: Fn() -> T + Send + 'static,
92    {
93        Self {
94            fetcher: Fetcher::spawn_blocking(func, render),
95            update,
96        }
97    }
98}
99
100impl<T: Send + 'static, W: Widget> Widget for WidgetFetcher<T, W> {
101    #[inline(always)]
102    fn render(
103        &mut self,
104        scene: &mut dyn Scene,
105        theme: &mut dyn Theme,
106        layout_node: &LayoutNode,
107        info: &AppInfo,
108        context: AppContext,
109    ) {
110        if let Some(widget) = self.fetcher.value_mut() {
111            widget.render(scene, theme, layout_node, info, context.clone())
112        }
113    }
114
115    #[inline(always)]
116    fn layout_style(&self) -> StyleNode {
117        if let Some(widget) = self.fetcher.value_ref() {
118            widget.layout_style()
119        } else {
120            StyleNode {
121                style: LayoutStyle::default(),
122                children: Vec::new(),
123            }
124        }
125    }
126
127    #[inline(always)]
128    fn update(&mut self, layout: &LayoutNode, context: AppContext, info: &AppInfo) -> Update {
129        let mut update = Update::empty();
130
131        // Make sure to update the widget if the task is about to be polled
132        if self.fetcher.is_ready() {
133            update.insert(self.update);
134        }
135
136        let widget = self.fetcher.fetch();
137
138        widget.update(layout, context, info) | update
139    }
140
141    #[inline(always)]
142    fn widget_id(&self) -> WidgetId {
143        WidgetId::new("maycoon-widgets", "WidgetFetcher")
144    }
145}