1use std::{cell::RefCell, rc::Rc, time::Duration};
10
11use gpui::{
12 Bounds, Context, IntoElement, ParentElement, Pixels, Render, Styled, Task, Window, canvas, div,
13 px,
14};
15use web_time::Instant;
16
17use crate::orbs::{
18 engine::{Frame, draw_mode_into, draw_mode_into_resolved},
19 paint::paint_frame,
20 presets::{Resolved, resolve_preset},
21 types::{OrbSize, OrbState, OrbTheme},
22};
23
24pub const DEFAULT_TARGET_FPS: f32 = 30.0;
31
32pub struct Orb {
38 state: OrbState,
39 size: OrbSize,
40 theme: OrbTheme,
41 speed: f32,
43 paused: bool,
44 reduced_motion: bool,
48 target_fps: f32,
50 pause_when_inactive: bool,
52 visible: bool,
56
57 started: Instant,
59 paused_total: Duration,
62 paused_at: Option<Instant>,
64
65 cache_key: (OrbState, OrbSize),
68 resolved: Resolved,
69 frame: Rc<RefCell<Frame>>,
72 geometry_dirty: bool,
75
76 tick: Option<Task<()>>,
79 activation: Option<gpui::Subscription>,
82}
83
84impl Default for Orb {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl Orb {
91 pub fn new() -> Self {
92 let state = OrbState::Working;
93 let size = OrbSize::Avatar;
94 Self {
95 state,
96 size,
97 theme: OrbTheme::Auto,
98 speed: 1.0,
99 paused: false,
100 reduced_motion: false,
101 target_fps: DEFAULT_TARGET_FPS,
102 pause_when_inactive: true,
103 visible: true,
104 started: Instant::now(),
105 paused_total: Duration::ZERO,
106 paused_at: None,
107 cache_key: (state, size),
108 resolved: resolve_preset(state, size),
109 frame: Rc::new(RefCell::new(Frame::new())),
110 geometry_dirty: true,
111 tick: None,
112 activation: None,
113 }
114 }
115
116 pub fn state(mut self, state: OrbState) -> Self {
117 self.state = state;
118 self
119 }
120
121 pub fn size(mut self, size: OrbSize) -> Self {
122 self.size = size;
123 self
124 }
125
126 pub fn theme(mut self, theme: OrbTheme) -> Self {
127 self.theme = theme;
128 self
129 }
130
131 pub fn speed(mut self, speed: f32) -> Self {
132 self.speed = sanitize_speed(speed);
133 self
134 }
135
136 pub fn paused(mut self, paused: bool) -> Self {
139 apply_pause_clock(
140 &mut self.paused,
141 &mut self.paused_at,
142 &mut self.paused_total,
143 paused,
144 );
145 self
146 }
147
148 pub fn reduced_motion(mut self, reduced: bool) -> Self {
149 self.reduced_motion = reduced;
150 self
151 }
152
153 pub fn target_fps(mut self, fps: f32) -> Self {
157 self.target_fps = sanitize_fps(fps);
158 self
159 }
160
161 pub fn pause_when_inactive(mut self, pause: bool) -> Self {
167 self.pause_when_inactive = pause;
168 self
169 }
170
171 pub fn visible(mut self, visible: bool) -> Self {
178 self.visible = visible;
179 self
180 }
181
182 pub fn set_state(&mut self, state: OrbState, cx: &mut Context<Self>) {
184 if self.state != state {
185 self.state = state;
186 self.geometry_dirty = true;
187 cx.notify();
188 }
189 }
190
191 pub fn set_size(&mut self, size: OrbSize, cx: &mut Context<Self>) {
192 if self.size != size {
193 self.size = size;
194 self.geometry_dirty = true;
195 cx.notify();
196 }
197 }
198
199 pub fn set_theme(&mut self, theme: OrbTheme, cx: &mut Context<Self>) {
200 if self.theme != theme {
201 self.theme = theme;
202 cx.notify();
203 }
204 }
205
206 pub fn set_speed(&mut self, speed: f32, cx: &mut Context<Self>) {
207 let speed = sanitize_speed(speed);
208 if self.speed != speed {
209 self.speed = speed;
210 self.geometry_dirty = true;
211 cx.notify();
212 }
213 }
214
215 pub fn set_paused(&mut self, paused: bool, cx: &mut Context<Self>) {
216 if self.paused == paused {
217 return;
218 }
219 apply_pause_clock(
220 &mut self.paused,
221 &mut self.paused_at,
222 &mut self.paused_total,
223 paused,
224 );
225 self.geometry_dirty = true;
226 cx.notify();
227 }
228
229 pub fn set_reduced_motion(&mut self, reduced: bool, cx: &mut Context<Self>) {
230 if self.reduced_motion != reduced {
231 self.reduced_motion = reduced;
232 self.geometry_dirty = true;
233 cx.notify();
234 }
235 }
236
237 pub fn set_target_fps(&mut self, fps: f32, cx: &mut Context<Self>) {
238 self.target_fps = sanitize_fps(fps);
239 cx.notify();
240 }
241
242 pub fn set_pause_when_inactive(&mut self, pause: bool, cx: &mut Context<Self>) {
243 if self.pause_when_inactive != pause {
244 self.pause_when_inactive = pause;
245 cx.notify();
246 }
247 }
248
249 pub fn set_visible(&mut self, visible: bool, cx: &mut Context<Self>) {
251 if self.visible != visible {
252 self.visible = visible;
253 self.geometry_dirty = true;
254 cx.notify();
255 }
256 }
257
258 pub fn state_value(&self) -> OrbState {
259 self.state
260 }
261
262 pub fn size_value(&self) -> OrbSize {
263 self.size
264 }
265
266 pub fn theme_value(&self) -> OrbTheme {
267 self.theme
268 }
269
270 pub fn speed_value(&self) -> f32 {
271 self.speed
272 }
273
274 pub fn is_paused(&self) -> bool {
275 self.paused
276 }
277
278 pub fn reduced_motion_value(&self) -> bool {
279 self.reduced_motion
280 }
281
282 pub fn target_fps_value(&self) -> f32 {
283 self.target_fps
284 }
285
286 pub fn pause_when_inactive_value(&self) -> bool {
287 self.pause_when_inactive
288 }
289
290 pub fn is_visible(&self) -> bool {
291 self.visible
292 }
293
294 fn dark(&self) -> bool {
298 match self.theme {
299 OrbTheme::Dark => true,
300 OrbTheme::Light => false,
301 OrbTheme::Auto => theme::current_appearance().is_dark(),
302 }
303 }
304
305 fn time_seconds(&self, reduced: bool) -> f32 {
317 if reduced {
318 return 0.6;
319 }
320 let paused = match self.paused_at {
321 Some(at) => self.paused_total + at.elapsed(),
322 None => self.paused_total,
323 };
324 let live = self.started.elapsed().saturating_sub(paused);
325 (live.as_secs_f64() * self.resolved.speed as f64 * self.speed as f64) as f32
326 }
327
328 fn schedule_tick(&mut self, cx: &mut Context<Self>) {
330 if self.tick.is_some() {
334 return;
335 }
336 let period = Duration::from_secs_f32(1.0 / self.target_fps);
337 self.tick = Some(cx.spawn(async move |this, cx| {
338 cx.background_executor().timer(period).await;
339 let _ = this.update(cx, |orb, cx| {
340 orb.tick = None;
341 orb.geometry_dirty = true;
342 cx.notify();
343 });
344 }));
345 }
346}
347
348impl Render for Orb {
349 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
350 if self.activation.is_none() {
354 self.activation = Some(cx.observe_window_activation(window, |orb, _, cx| {
355 orb.geometry_dirty = true;
356 cx.notify();
357 }));
358 }
359
360 let key = (self.state, self.size);
363 if self.cache_key != key {
364 self.cache_key = key;
365 self.resolved = resolve_preset(self.state, self.size);
366 }
367
368 let size_px = self.size.pixels();
369 let dark = self.dark();
370 let reduced = self.reduced_motion || cx.reduce_motion();
371 let t = self.time_seconds(reduced);
372 let r_min = self.resolved.opts.r_min.unwrap_or(0.3);
373
374 if self.geometry_dirty {
378 draw_mode_into_resolved(
379 self.resolved.mode,
380 size_px,
381 t,
382 &self.resolved.opts,
383 &mut self.frame.borrow_mut(),
384 );
385 self.geometry_dirty = false;
386 }
387
388 let animating = self.visible
389 && !self.paused
390 && !reduced
391 && (!self.pause_when_inactive || window.is_window_active());
392 if animating {
393 self.schedule_tick(cx);
394 } else {
395 self.tick = None;
398 }
399
400 let frame = self.frame.clone();
401 div()
402 .size(px(size_px))
403 .flex_shrink_0()
404 .overflow_hidden()
405 .child(
406 canvas(
407 move |_bounds: Bounds<Pixels>, _window, _cx| (),
408 move |bounds, (), window, _cx| {
409 paint_frame(window, bounds, &frame.borrow(), dark, r_min);
410 },
411 )
412 .size_full(),
413 )
414 }
415}
416
417pub fn orb_element(
426 state: OrbState,
427 size: OrbSize,
428 t: f32,
429 frame: &Rc<RefCell<Frame>>,
430) -> impl IntoElement {
431 let resolved = resolve_preset(state, size);
432 let size_px = size.pixels();
433 let frame = frame.clone();
434 draw_mode_into(
435 resolved.mode,
436 size_px,
437 t,
438 &resolved.opts,
439 &mut frame.borrow_mut(),
440 );
441 let dark = theme::current_appearance().is_dark();
442 let r_min = resolved.opts.r_min.unwrap_or(0.3);
443 div()
444 .size(px(size_px))
445 .flex_shrink_0()
446 .overflow_hidden()
447 .child(
448 canvas(
449 move |_bounds: Bounds<Pixels>, _window, _cx| (),
450 move |bounds, (), window, _cx| {
451 paint_frame(window, bounds, &frame.borrow(), dark, r_min);
452 },
453 )
454 .size_full(),
455 )
456}
457
458fn apply_pause_clock(
460 paused: &mut bool,
461 paused_at: &mut Option<Instant>,
462 paused_total: &mut Duration,
463 want: bool,
464) {
465 if *paused == want {
466 if !want && let Some(at) = paused_at.take() {
468 *paused_total += at.elapsed();
469 }
470 return;
471 }
472 *paused = want;
473 if want {
474 if paused_at.is_none() {
475 *paused_at = Some(Instant::now());
476 }
477 } else if let Some(at) = paused_at.take() {
478 *paused_total += at.elapsed();
479 }
480}
481
482fn sanitize_speed(speed: f32) -> f32 {
483 if speed.is_finite() {
484 speed.clamp(0.0, 100.0)
485 } else {
486 1.0
487 }
488}
489
490fn sanitize_fps(fps: f32) -> f32 {
491 if fps.is_finite() {
492 fps.clamp(1.0, 240.0)
493 } else {
494 DEFAULT_TARGET_FPS
495 }
496}