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}