Skip to repository content356 lines · 12.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:32:51.464Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
update_button.rs
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