Skip to main content

liora_components/
loading.rs

1//! Loading module.
2//!
3//! This public module implements the Liora loading indicator and overlay components. It keeps the reusable
4//! component logic inside `liora-components` rather than Gallery or Docs so
5//! downstream GPUI applications can compose the same behavior with their own
6//! app state, assets, and release policy.
7//!
8//! ## Usage model
9//!
10//! Components in this module render native GPUI element trees. Stateless builder
11//! values can be constructed inline, while controls with focus, selection,
12//! popup, drag, or editing state should be stored as `gpui::Entity<T>` fields in
13//! the parent view so state survives GPUI render passes.
14//!
15//! ## Design contract
16//!
17//! The implementation should use Liora theme tokens from `liora-core` and
18//! `liora-theme`, keep accessibility-oriented keyboard/pointer behavior close to
19//! the component, and avoid app-specific Gallery/Docs resources in this SDK
20//! crate.
21
22use crate::motion::{fade_in, spin_icon};
23use gpui::{App, Component, IntoElement, RenderOnce, SharedString, Window, div, prelude::*, px};
24use liora_core::Config;
25use liora_icons::Icon;
26use liora_icons_lucide::IconName;
27
28/// Fluent native GPUI component for rendering Liora loading.
29pub struct Loading {
30    text: Option<SharedString>,
31    full_screen: bool,
32}
33
34impl Loading {
35    /// Creates `Loading` with default theme-driven styling and no optional callbacks attached.
36    pub fn new() -> Self {
37        Self {
38            text: None,
39            full_screen: false,
40        }
41    }
42
43    /// Applies the text-only visual variant.
44    pub fn text(mut self, text: impl Into<SharedString>) -> Self {
45        self.text = Some(text.into());
46        self
47    }
48
49    /// Sets the full screen value used by the component.
50    pub fn full_screen(mut self) -> Self {
51        self.full_screen = true;
52        self
53    }
54}
55
56impl RenderOnce for Loading {
57    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
58        let theme = cx.global::<Config>().theme.clone();
59
60        let spinner_icon = spin_icon(
61            "liora-loading-spinner-motion",
62            Icon::new(IconName::LoaderCircle)
63                .size(px(32.0))
64                .color(theme.primary.base),
65        );
66
67        let spinner = div()
68            .flex()
69            .flex_col()
70            .items_center()
71            .gap_2()
72            .child(spinner_icon)
73            .when_some(self.text, |s, t| {
74                s.child(div().text_sm().text_color(theme.primary.base).child(t))
75            });
76
77        if self.full_screen {
78            fade_in(
79                "liora-loading-fullscreen-motion",
80                div()
81                    .absolute()
82                    .size_full()
83                    .bg(theme.neutral.mask)
84                    .flex()
85                    .items_center()
86                    .justify_center()
87                    .child(spinner),
88            )
89        } else {
90            fade_in("liora-loading-inline-motion", spinner)
91        }
92    }
93}
94
95impl IntoElement for Loading {
96    type Element = Component<Self>;
97    fn into_element(self) -> Self::Element {
98        Component::new(self)
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    #[test]
105    fn loading_uses_spin_and_fade_motion() {
106        let source = include_str!("loading.rs")
107            .split("#[cfg(test)]")
108            .next()
109            .unwrap();
110
111        assert!(source.contains("spin_icon("));
112        assert!(source.contains("fade_in("));
113        assert!(source.contains("liora-loading-spinner-motion"));
114    }
115}