Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T07:36:47.954Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

update_button.rs

356 lines · 12.1 KB · rust
1use gpui::{AnyElement, AnyView, ClickEvent, prelude::*};
2
3use crate::{ButtonLike, CircularProgress, CommonAnimationExt, Tooltip, prelude::*};
4
5const LOAD_CIRCLE_GLYPH_VIEWBOX: f32 = 16.0;
6const LOAD_CIRCLE_GLYPH_STROKE_WIDTH: f32 = 1.2;
7const LOAD_CIRCLE_GLYPH_RADIUS: f32 = 5.0;
8
9/// A button component displayed in the title bar to show auto-update status.
10#[derive(IntoElement, RegisterComponent)]
11pub struct UpdateButton {
12    icon: IconName,
13    icon_animate: bool,
14    icon_color: Option<Color>,
15    message: SharedString,
16    tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
17    disabled: bool,
18    show_dismiss: bool,
19    progress: Option<f32>,
20    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
21    on_dismiss: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
22}
23
24impl UpdateButton {
25    pub fn new(icon: IconName, message: impl Into<SharedString>) -> Self {
26        Self {
27            icon,
28            icon_animate: false,
29            icon_color: None,
30            message: message.into(),
31            tooltip: None,
32            disabled: false,
33            show_dismiss: false,
34            progress: None,
35            on_click: None,
36            on_dismiss: None,
37        }
38    }
39
40    /// Sets whether the icon should have a rotation animation (for progress states).
41    pub fn icon_animate(mut self, animate: bool) -> Self {
42        self.icon_animate = animate;
43        self
44    }
45
46    /// Sets the icon color (e.g., for warning/error states).
47    pub fn icon_color(mut self, color: impl Into<Option<Color>>) -> Self {
48        self.icon_color = color.into();
49        self
50    }
51
52    /// Sets the tooltip text shown on hover.
53    pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
54        self.tooltip = Some(Box::new(Tooltip::text(tooltip.into())));
55        self
56    }
57
58    /// Sets a tooltip builder invoked on every render, so the tooltip can
59    /// display content that changes while it stays visible.
60    pub fn tooltip_fn(
61        mut self,
62        tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
63    ) -> Self {
64        self.tooltip = Some(Box::new(tooltip));
65        self
66    }
67
68    /// Shows a dismiss button on the right side.
69    pub fn with_dismiss(mut self) -> Self {
70        self.show_dismiss = true;
71        self
72    }
73
74    /// Sets the click handler for the main button area.
75    pub fn on_click(
76        mut self,
77        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
78    ) -> Self {
79        self.on_click = Some(Box::new(handler));
80        self
81    }
82
83    /// Sets the click handler for the dismiss button.
84    pub fn on_dismiss(
85        mut self,
86        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
87    ) -> Self {
88        self.on_dismiss = Some(Box::new(handler));
89        self
90    }
91
92    pub fn disabled(mut self, disabled: bool) -> Self {
93        self.disabled = disabled;
94        self
95    }
96
97    pub fn progress(mut self, progress: impl Into<Option<f32>>) -> Self {
98        self.progress = progress.into();
99        self
100    }
101
102    pub fn checking() -> Self {
103        Self::new(IconName::LoadCircle, "Checking for Omega Updates…")
104            .icon_animate(true)
105            .disabled(true)
106    }
107
108    pub fn downloading(progress: Option<f32>) -> Self {
109        Self::new(IconName::Download, "Downloading Omega Update…")
110            .progress(progress)
111            .disabled(true)
112    }
113
114    pub fn installing(version: impl Into<SharedString>) -> Self {
115        Self::new(IconName::LoadCircle, "Installing Omega Update…")
116            .icon_animate(true)
117            .tooltip(version)
118            .disabled(true)
119    }
120
121    pub fn updated(version: impl Into<SharedString>) -> Self {
122        Self::new(IconName::Download, "Restart to Update")
123            .tooltip(version)
124            .with_dismiss()
125    }
126
127    pub fn errored(error: impl Into<SharedString>) -> Self {
128        Self::new(IconName::Warning, "Failed to Update")
129            .icon_color(Color::Warning)
130            .tooltip(error)
131            .with_dismiss()
132    }
133
134    pub fn version_tooltip_message(version: impl std::fmt::Display) -> String {
135        format!("Update to Version: {version}")
136    }
137
138    pub fn downloading_tooltip_message(
139        version: impl std::fmt::Display,
140        progress: Option<f32>,
141    ) -> String {
142        let message = Self::version_tooltip_message(version);
143        match progress {
144            Some(progress) => format!(
145                "{message} ({:.0}% downloaded)",
146                progress.clamp(0.0, 1.0) * 100.0
147            ),
148            None => message,
149        }
150    }
151}
152
153impl RenderOnce for UpdateButton {
154    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
155        let border_color = if self.disabled {
156            cx.theme().colors().border
157        } else {
158            cx.theme().colors().text.opacity(0.15)
159        };
160
161        let icon_element = if let Some(progress) = self.progress {
162            let progress = progress.clamp(0.0, 1.0);
163            let icon_box = IconSize::XSmall.rems().to_pixels(window.rem_size());
164            let progress_color = Color::Default.color(cx);
165            CircularProgress::new(progress, 1.0, icon_box, cx)
166                .stroke_width(
167                    icon_box * (LOAD_CIRCLE_GLYPH_STROKE_WIDTH / LOAD_CIRCLE_GLYPH_VIEWBOX),
168                )
169                .radius(icon_box * (LOAD_CIRCLE_GLYPH_RADIUS / LOAD_CIRCLE_GLYPH_VIEWBOX))
170                .bg_color(progress_color.opacity(0.2))
171                .progress_color(progress_color)
172                .into_any_element()
173        } else {
174            let icon = Icon::new(self.icon)
175                .size(IconSize::XSmall)
176                .when_some(self.icon_color, |this, color| this.color(color));
177            if self.icon_animate {
178                icon.with_rotate_animation(2).into_any_element()
179            } else {
180                icon.into_any_element()
181            }
182        };
183
184        let tooltip = self.tooltip;
185
186        let button_id = ElementId::Name(self.message.clone());
187        let dismiss_button_id = ElementId::Name(format!("dismiss-{}", self.message).into());
188
189        let label_row = h_flex()
190            .h_full()
191            .gap_1()
192            .child(icon_element)
193            .child(Label::new(self.message).size(LabelSize::Small));
194
195        h_flex()
196            .mr_2()
197            .rounded_sm()
198            .border_1()
199            .border_color(border_color)
200            .child(
201                ButtonLike::new(button_id)
202                    .child(label_row)
203                    .when_some(tooltip, |this, tooltip| this.tooltip(tooltip))
204                    .disabled(self.disabled)
205                    .when_some(self.on_click, |this, handler| this.on_click(handler)),
206            )
207            .when(self.show_dismiss, |this| {
208                this.child(
209                    div().border_l_1().border_color(border_color).child(
210                        IconButton::new(dismiss_button_id, IconName::Close)
211                            .icon_size(IconSize::Indicator)
212                            .when_some(self.on_dismiss, |this, handler| this.on_click(handler))
213                            .tooltip(Tooltip::text("Dismiss")),
214                    ),
215                )
216            })
217    }
218}
219
220impl Component for UpdateButton {
221    fn scope() -> ComponentScope {
222        ComponentScope::Collaboration
223    }
224
225    fn name() -> &'static str {
226        "UpdateButton"
227    }
228
229    fn description() -> &'static str {
230        "A button component displayed in the title bar to \
231        show auto-update status and allow users to restart Omega."
232    }
233
234    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
235        let version = "1.3.0+stable.2025051";
236
237        v_flex()
238            .gap_6()
239            .children(vec![
240                example_group_with_title(
241                    "Progress States",
242                    vec![
243                        single_example("Checking", UpdateButton::checking().into_any_element()),
244                        single_example(
245                            "Downloading",
246                            UpdateButton::downloading(Some(0.45))
247                                .tooltip(UpdateButton::downloading_tooltip_message(
248                                    version,
249                                    Some(0.45),
250                                ))
251                                .into_any_element(),
252                        ),
253                        single_example(
254                            "Installing",
255                            UpdateButton::installing(version).into_any_element(),
256                        ),
257                    ],
258                ),
259                example_group_with_title(
260                    "Actionable States",
261                    vec![
262                        single_example(
263                            "Ready to Update",
264                            UpdateButton::updated(version).into_any_element(),
265                        ),
266                        single_example(
267                            "Error",
268                            UpdateButton::errored("Network timeout").into_any_element(),
269                        ),
270                    ],
271                ),
272            ])
273            .into_any_element()
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use gpui::{Render, TestAppContext, point, px};
281    use std::cell::Cell;
282    use std::rc::Rc;
283    use std::time::Duration;
284
285    struct TestTooltip {
286        rendered: Rc<Cell<u32>>,
287    }
288
289    impl Render for TestTooltip {
290        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
291            self.rendered.set(self.rendered.get() + 1);
292            div().child("tooltip")
293        }
294    }
295
296    struct PreviewLikeButtons {
297        tooltip_built: Rc<Cell<bool>>,
298        tooltip_rendered: Rc<Cell<u32>>,
299    }
300
301    impl Render for PreviewLikeButtons {
302        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
303            let tooltip_built = self.tooltip_built.clone();
304            let tooltip_rendered = self.tooltip_rendered.clone();
305            crate::v_flex()
306                .size_full()
307                .child(UpdateButton::checking())
308                .child(
309                    UpdateButton::downloading(Some(0.5)).tooltip_fn(move |_, cx| {
310                        tooltip_built.set(true);
311                        let rendered = tooltip_rendered.clone();
312                        cx.new(|_| TestTooltip { rendered }).into()
313                    }),
314                )
315                .child(UpdateButton::updated("Update to Version: 1.0.0"))
316        }
317    }
318
319    #[gpui::test]
320    async fn test_downloading_tooltip_shows_in_preview_like_layout(cx: &mut TestAppContext) {
321        cx.update(|cx| {
322            let settings_store = settings::SettingsStore::test(cx);
323            cx.set_global(settings_store);
324            theme_settings::init(theme::LoadThemes::JustBase, cx);
325        });
326        let tooltip_built = Rc::new(Cell::new(false));
327        let tooltip_rendered = Rc::new(Cell::new(0));
328        let (_view, cx) = cx.add_window_view({
329            let tooltip_built = tooltip_built.clone();
330            let tooltip_rendered = tooltip_rendered.clone();
331            |_, _| PreviewLikeButtons {
332                tooltip_built,
333                tooltip_rendered,
334            }
335        });
336
337        cx.simulate_mouse_move(point(px(30.), px(30.)), None, gpui::Modifiers::default());
338        cx.run_until_parked();
339        cx.simulate_mouse_move(point(px(31.), px(30.)), None, gpui::Modifiers::default());
340        cx.run_until_parked();
341
342        cx.executor().advance_clock(Duration::from_millis(600));
343        cx.run_until_parked();
344
345        assert!(tooltip_built.get(), "tooltip should have been built");
346
347        tooltip_rendered.set(0);
348        cx.update(|window, _| window.refresh());
349        cx.run_until_parked();
350        assert!(
351            tooltip_rendered.get() > 0,
352            "tooltip should still be rendered after another frame"
353        );
354    }
355}
356
Served at tenant.openagents/omega Member data and write actions are omitted.