Skip to main content

egui_async/egui/widgets/
async_view.rs

1//! A structured container that fully manages the four foundational states of data loading.
2
3use std::fmt::Debug;
4
5use crate::bind::{Bind, MaybeSend, StateWithData};
6
7/// Determines how the intermediate states (Loading, Error) are positioned within the UI.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum StateLayout {
10    /// Centers the content and greedily consumes all available space in the parent `Ui`.
11    ///
12    /// **Note:** This can cause the UI to snap or shrink abruptly when transitioning
13    /// to the `Finished` state if the successful content does not also fill the space.
14    FillAndCenter,
15    /// Centers the content horizontally, consuming only the vertical space required.
16    ///
17    /// This prevents the surrounding UI from aggressively shifting and is the recommended default.
18    #[default]
19    CenterHorizontal,
20    /// Lays out the state content inline, directly following the parent `Ui`'s standard flow.
21    Inline,
22}
23
24/// A standardized widget that exhaustively handles the `Idle`, `Pending`,
25/// `Failed`, and `Finished` states of a data fetch.
26///
27/// Use this to wrap the core visual components of your app that rely on external data.
28#[must_use = "You should call .show() on this widget to render it"]
29pub struct AsyncView<'a, T, E> {
30    bind: &'a mut Bind<T, E>,
31    loading_text: String,
32    error_retry_text: String,
33    state_layout: StateLayout,
34}
35
36impl<'a, T, E> AsyncView<'a, T, E> {
37    /// Constructs a new `AsyncView`.
38    pub fn new(bind: &'a mut Bind<T, E>) -> Self {
39        Self {
40            bind,
41            loading_text: "Loading...".to_string(),
42            error_retry_text: "Retry".to_string(),
43            state_layout: StateLayout::default(),
44        }
45    }
46
47    /// Sets the text to display below the spinner when the fetch is pending.
48    pub fn loading_text(mut self, text: impl Into<String>) -> Self {
49        self.loading_text = text.into();
50        self
51    }
52
53    /// Sets the text to display on the retry button when the fetch fails.
54    pub fn error_retry_text(mut self, text: impl Into<String>) -> Self {
55        self.error_retry_text = text.into();
56        self
57    }
58
59    /// Configures the layout strategy for the intermediate loading and error states.
60    pub const fn state_layout(mut self, layout: StateLayout) -> Self {
61        self.state_layout = layout;
62        self
63    }
64
65    /// Runs the state machine. If data is successfully loaded, it invokes `on_ok`
66    /// to let you render your successful data.
67    ///
68    /// # Returns
69    /// `Some(R)` if the data is available and successfully rendered, otherwise `None`.
70    pub fn show<Fut, R>(
71        self,
72        ui: &mut egui::Ui,
73        fetch: impl FnOnce() -> Fut,
74        on_ok: impl FnOnce(&mut egui::Ui, &T) -> R,
75    ) -> Option<R>
76    where
77        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
78        T: MaybeSend + 'static,
79        E: Debug + MaybeSend + 'static,
80    {
81        let mut should_clear = false;
82        let mut ret = None;
83
84        match self.bind.state_or_request(fetch) {
85            StateWithData::Idle | StateWithData::Pending => {
86                Self::apply_layout(self.state_layout, ui, |ui| {
87                    ui.spinner();
88                    ui.add_space(8.0);
89                    ui.label(&self.loading_text);
90                });
91            }
92            StateWithData::Finished(data) => {
93                ret = Some(Self::apply_layout(self.state_layout, ui, |ui| {
94                    on_ok(ui, data)
95                }));
96            }
97            StateWithData::Failed(err) => {
98                Self::apply_layout(self.state_layout, ui, |ui| {
99                    ui.label(
100                        egui::RichText::new("⚠ Request Failed")
101                            .color(ui.visuals().error_fg_color)
102                            .size(16.0)
103                            .strong(),
104                    );
105                    ui.add_space(8.0);
106                    ui.label(format!("{err:?}"));
107                    ui.add_space(12.0);
108
109                    if ui.button(&self.error_retry_text).clicked() {
110                        should_clear = true;
111                    }
112                });
113            }
114        }
115
116        // Apply mutation safely after the match block drops its borrow on `self.bind`
117        if should_clear {
118            self.bind.clear(); // Will trigger fetch on next frame
119        }
120
121        ret
122    }
123
124    /// Safely applies the configured `StateLayout` to the given closure without borrow conflicts.
125    fn apply_layout<R>(
126        layout: StateLayout,
127        ui: &mut egui::Ui,
128        add_contents: impl FnOnce(&mut egui::Ui) -> R,
129    ) -> R {
130        match layout {
131            StateLayout::FillAndCenter => {
132                ui.centered_and_justified(|ui| ui.vertical_centered(add_contents).inner)
133                    .inner
134            }
135            StateLayout::CenterHorizontal => ui.vertical_centered(add_contents).inner,
136            StateLayout::Inline => add_contents(ui),
137        }
138    }
139}