liora_components/
loading.rs1use 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
28pub struct Loading {
30 text: Option<SharedString>,
31 full_screen: bool,
32}
33
34impl Loading {
35 pub fn new() -> Self {
37 Self {
38 text: None,
39 full_screen: false,
40 }
41 }
42
43 pub fn text(mut self, text: impl Into<SharedString>) -> Self {
45 self.text = Some(text.into());
46 self
47 }
48
49 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}