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;
6use maycoon_core::vg::Scene;
7use maycoon_core::widget::Widget;
8use maycoon_theme::id::WidgetId;
9use maycoon_theme::theme::Theme;
10use std::future::Future;
11use std::sync::{Arc, Mutex};
12
13/// Widget builder to fetch data from an asynchronous task. The [TaskRunner](tasks::runner::TaskRunner) needs to be initialized.
14/// This is similar to the [FutureBuilder](https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html) from Flutter.
15///
16/// ### Async + UI
17/// Normally, a UI Application is not able to asynchronously spawn tasks and will block the UI thread instead of running tasks in the background.
18/// The [WidgetFetcher] is able to spawn asynchronous tasks on a global runner and construct a widget based on the result of the task.
19/// You can use it to fetch asynchronous data and either return something like a loading screen or the actual data.
20///
21/// ### Workflow of a [WidgetFetcher].
22/// 1. Run the task in the background using the [TaskRunner](tasks::runner::TaskRunner).
23/// 2. Construct the widget with [None] as the result (task is still loading).
24/// 3. Once the task is done, update the UI with the new result and trigger an [Update].
25///
26/// ### Theming
27/// The widget itself only draws the underlying widget, so theming is useless.
28pub struct WidgetFetcher<T: Send + 'static, W: Widget, F: Fn(Option<T>) -> W> {
29    result: Arc<Mutex<Option<T>>>,
30    render: F,
31    widget: Option<W>,
32    update: Update,
33}
34
35impl<T: Send + 'static, W: Widget, F: Fn(Option<T>) -> W> WidgetFetcher<T, W, F> {
36    /// Creates a new [WidgetFetcher] with parameters:
37    /// - `future`: The future to execute.
38    /// - `update`: The update to trigger when the data is updated (from loading to done).
39    /// - `render`: The function to render the widget. The first parameter is the result of the future and the second parameter is the mutable app state.
40    pub fn new<Fut>(future: Fut, update: Update, render: F) -> Self
41    where
42        Fut: Future<Output = T> + Send + 'static,
43    {
44        let result = Arc::new(Mutex::new(None));
45
46        let result_clone = result.clone();
47        tasks::spawn(async move {
48            let out = future.await;
49            *result_clone.lock().expect("failed to lock result") = Some(out);
50        })
51        .forget();
52
53        Self {
54            result,
55            render,
56            widget: None,
57            update,
58        }
59    }
60}
61
62impl<T: Send + 'static, W: Widget, F: Fn(Option<T>) -> W> Widget for WidgetFetcher<T, W, F> {
63    fn render(
64        &mut self,
65        scene: &mut Scene,
66        theme: &mut dyn Theme,
67        layout_node: &LayoutNode,
68        info: &AppInfo,
69        context: AppContext,
70    ) {
71        if let Some(widget) = &mut self.widget {
72            widget.render(scene, theme, layout_node, info, context.clone())
73        }
74    }
75
76    fn layout_style(&self) -> StyleNode {
77        if let Some(widget) = &self.widget {
78            widget.layout_style()
79        } else {
80            StyleNode {
81                style: LayoutStyle::default(),
82                children: Vec::new(),
83            }
84        }
85    }
86
87    fn update(&mut self, layout: &LayoutNode, context: AppContext, info: &AppInfo) -> Update {
88        let mut update = Update::empty();
89
90        if let Some(result) = self.result.lock().expect("failed to lock result").take() {
91            self.widget = Some((self.render)(Some(result)));
92            update = self.update;
93        } else if self.widget.is_none() {
94            self.widget = Some((self.render)(None));
95            update = self.update;
96        }
97
98        // Widget is guaranteed to be some at this point
99        self.widget.as_mut().unwrap().update(layout, context, info) | update
100    }
101
102    fn widget_id(&self) -> WidgetId {
103        if let Some(widget) = &self.widget {
104            widget.widget_id()
105        } else {
106            WidgetId::new("maycoon-widgets", "WidgetFetcher")
107        }
108    }
109}