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>) {
333 if self.tick.is_some() {
337 return;
338 }
339 let period = Duration::from_secs_f32(1.0 / self.target_fps);
340 self.tick = Some(cx.spawn(async move |this, cx| {
341 cx.background_executor().timer(period).await;
342 let _ = this.update(cx, |orb, cx| {
343 orb.tick = None;
344 orb.geometry_dirty = true;
345 cx.notify();
346 });
347 }));
348 }
349}
350
351impl Render for Orb {
352 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
353 if self.activation.is_none() {
357 self.activation = Some(cx.observe_window_activation(window, |orb, _, cx| {
358 orb.geometry_dirty = true;
359 cx.notify();
360 }));
361 }
362
363 let key = (self.state, self.size);
366 if self.cache_key != key {
367 self.cache_key = key;
368 self.resolved = resolve_preset(self.state, self.size);
369 }
370
371 let size_px = self.size.pixels();
372 let dark = self.dark();
373 let reduced = self.reduced_motion || cx.reduce_motion();
374 let t = self.time_seconds(reduced);
375 let r_min = self.resolved.opts.r_min.unwrap_or(0.3);
376
377 if self.geometry_dirty {
381 draw_mode_into_resolved(
382 self.resolved.mode,
383 size_px,
384 t,
385 &self.resolved.opts,
386 &mut self.frame.borrow_mut(),
387 );
388 self.geometry_dirty = false;
389 }
390
391 let animating = self.visible
392 && !self.paused
393 && !reduced
394 && (!self.pause_when_inactive || window.is_window_active());
395 if animating {
396 self.schedule_tick(cx);
397 } else {
398 self.tick = None;
401 }
402
403 let frame = self.frame.clone();
404 div()
405 .size(px(size_px))
406 .flex_shrink_0()
407 .overflow_hidden()
408 .child(
409 canvas(
410 move |_bounds: Bounds<Pixels>, _window, _cx| (),
411 move |bounds, (), window, _cx| {
412 paint_frame(window, bounds, &frame.borrow(), dark, r_min);
413 },
414 )
415 .size_full(),
416 )
417 }
418}
419
420pub fn orb_element(
429 state: OrbState,
430 size: OrbSize,
431 t: f32,
432 frame: &Rc<RefCell<Frame>>,
433) -> impl IntoElement {
434 let resolved = resolve_preset(state, size);
435 let size_px = size.pixels();
436 let frame = frame.clone();
437 draw_mode_into(
438 resolved.mode,
439 size_px,
440 t,
441 &resolved.opts,
442 &mut frame.borrow_mut(),
443 );
444 let dark = theme::current_appearance().is_dark();
445 let r_min = resolved.opts.r_min.unwrap_or(0.3);
446 div()
447 .size(px(size_px))
448 .flex_shrink_0()
449 .overflow_hidden()
450 .child(
451 canvas(
452 move |_bounds: Bounds<Pixels>, _window, _cx| (),
453 move |bounds, (), window, _cx| {
454 paint_frame(window, bounds, &frame.borrow(), dark, r_min);
455 },
456 )
457 .size_full(),
458 )
459}
460
461fn apply_pause_clock(
463 paused: &mut bool,
464 paused_at: &mut Option<Instant>,
465 paused_total: &mut Duration,
466 want: bool,
467) {
468 if *paused == want {
469 if !want {
471 if let Some(at) = paused_at.take() {
472 *paused_total += at.elapsed();
473 }
474 }
475 return;
476 }
477 *paused = want;
478 if want {
479 if paused_at.is_none() {
480 *paused_at = Some(Instant::now());
481 }
482 } else if let Some(at) = paused_at.take() {
483 *paused_total += at.elapsed();
484 }
485}
486
487fn sanitize_speed(speed: f32) -> f32 {
488 if speed.is_finite() {
489 speed.clamp(0.0, 100.0)
490 } else {
491 1.0
492 }
493}
494
495fn sanitize_fps(fps: f32) -> f32 {
496 if fps.is_finite() {
497 fps.clamp(1.0, 240.0)
498 } else {
499 DEFAULT_TARGET_FPS
500 }
501}