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 {
41 state: OrbState,
42 size: OrbSize,
43 theme: OrbTheme,
44 speed: f32,
46 paused: bool,
47 reduced_motion: bool,
51 target_fps: f32,
53 pause_when_inactive: bool,
55 visible: bool,
59
60 started: Instant,
62 paused_total: Duration,
65 paused_at: Option<Instant>,
67
68 cache_key: (OrbState, OrbSize),
71 resolved: Resolved,
72 frame: Rc<RefCell<Frame>>,
75 geometry_dirty: bool,
78
79 tick: Option<Task<()>>,
82 activation: Option<gpui::Subscription>,
85}
86
87impl Default for Orb {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl Orb {
94 pub fn new() -> Self {
95 let state = OrbState::Working;
96 let size = OrbSize::Avatar;
97 Self {
98 state,
99 size,
100 theme: OrbTheme::Auto,
101 speed: 1.0,
102 paused: false,
103 reduced_motion: false,
104 target_fps: DEFAULT_TARGET_FPS,
105 pause_when_inactive: true,
106 visible: true,
107 started: Instant::now(),
108 paused_total: Duration::ZERO,
109 paused_at: None,
110 cache_key: (state, size),
111 resolved: resolve_preset(state, size),
112 frame: Rc::new(RefCell::new(Frame::new())),
113 geometry_dirty: true,
114 tick: None,
115 activation: None,
116 }
117 }
118
119 pub fn state(mut self, state: OrbState) -> Self {
120 self.state = state;
121 self
122 }
123
124 pub fn size(mut self, size: OrbSize) -> Self {
125 self.size = size;
126 self
127 }
128
129 pub fn theme(mut self, theme: OrbTheme) -> Self {
130 self.theme = theme;
131 self
132 }
133
134 pub fn speed(mut self, speed: f32) -> Self {
135 self.speed = sanitize_speed(speed);
136 self
137 }
138
139 pub fn paused(mut self, paused: bool) -> Self {
142 apply_pause_clock(
143 &mut self.paused,
144 &mut self.paused_at,
145 &mut self.paused_total,
146 paused,
147 );
148 self
149 }
150
151 pub fn reduced_motion(mut self, reduced: bool) -> Self {
152 self.reduced_motion = reduced;
153 self
154 }
155
156 pub fn target_fps(mut self, fps: f32) -> Self {
160 self.target_fps = sanitize_fps(fps);
161 self
162 }
163
164 pub fn pause_when_inactive(mut self, pause: bool) -> Self {
170 self.pause_when_inactive = pause;
171 self
172 }
173
174 pub fn visible(mut self, visible: bool) -> Self {
181 self.visible = visible;
182 self
183 }
184
185 pub fn set_state(&mut self, state: OrbState, cx: &mut Context<Self>) {
187 if self.state != state {
188 self.state = state;
189 self.geometry_dirty = true;
190 cx.notify();
191 }
192 }
193
194 pub fn set_size(&mut self, size: OrbSize, cx: &mut Context<Self>) {
195 if self.size != size {
196 self.size = size;
197 self.geometry_dirty = true;
198 cx.notify();
199 }
200 }
201
202 pub fn set_theme(&mut self, theme: OrbTheme, cx: &mut Context<Self>) {
203 if self.theme != theme {
204 self.theme = theme;
205 cx.notify();
206 }
207 }
208
209 pub fn set_speed(&mut self, speed: f32, cx: &mut Context<Self>) {
210 let speed = sanitize_speed(speed);
211 if self.speed != speed {
212 self.speed = speed;
213 self.geometry_dirty = true;
214 cx.notify();
215 }
216 }
217
218 pub fn set_paused(&mut self, paused: bool, cx: &mut Context<Self>) {
219 if self.paused == paused {
220 return;
221 }
222 apply_pause_clock(
223 &mut self.paused,
224 &mut self.paused_at,
225 &mut self.paused_total,
226 paused,
227 );
228 self.geometry_dirty = true;
229 cx.notify();
230 }
231
232 pub fn set_reduced_motion(&mut self, reduced: bool, cx: &mut Context<Self>) {
233 if self.reduced_motion != reduced {
234 self.reduced_motion = reduced;
235 self.geometry_dirty = true;
236 cx.notify();
237 }
238 }
239
240 pub fn set_target_fps(&mut self, fps: f32, cx: &mut Context<Self>) {
241 self.target_fps = sanitize_fps(fps);
242 cx.notify();
243 }
244
245 pub fn set_pause_when_inactive(&mut self, pause: bool, cx: &mut Context<Self>) {
246 if self.pause_when_inactive != pause {
247 self.pause_when_inactive = pause;
248 cx.notify();
249 }
250 }
251
252 pub fn set_visible(&mut self, visible: bool, cx: &mut Context<Self>) {
254 if self.visible != visible {
255 self.visible = visible;
256 self.geometry_dirty = true;
257 cx.notify();
258 }
259 }
260
261 pub fn state_value(&self) -> OrbState {
262 self.state
263 }
264
265 pub fn size_value(&self) -> OrbSize {
266 self.size
267 }
268
269 pub fn theme_value(&self) -> OrbTheme {
270 self.theme
271 }
272
273 pub fn speed_value(&self) -> f32 {
274 self.speed
275 }
276
277 pub fn is_paused(&self) -> bool {
278 self.paused
279 }
280
281 pub fn reduced_motion_value(&self) -> bool {
282 self.reduced_motion
283 }
284
285 pub fn target_fps_value(&self) -> f32 {
286 self.target_fps
287 }
288
289 pub fn pause_when_inactive_value(&self) -> bool {
290 self.pause_when_inactive
291 }
292
293 pub fn is_visible(&self) -> bool {
294 self.visible
295 }
296
297 fn dark(&self) -> bool {
301 match self.theme {
302 OrbTheme::Dark => true,
303 OrbTheme::Light => false,
304 OrbTheme::Auto => theme::current_appearance().is_dark(),
305 }
306 }
307
308 fn time_seconds(&self, reduced: bool) -> f32 {
320 if reduced {
321 return 0.6;
322 }
323 let paused = match self.paused_at {
324 Some(at) => self.paused_total + at.elapsed(),
325 None => self.paused_total,
326 };
327 let live = self.started.elapsed().saturating_sub(paused);
328 (live.as_secs_f64() * self.resolved.speed as f64 * self.speed as f64) as f32
329 }
330
331 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 && let Some(at) = paused_at.take() {
471 *paused_total += at.elapsed();
472 }
473 return;
474 }
475 *paused = want;
476 if want {
477 if paused_at.is_none() {
478 *paused_at = Some(Instant::now());
479 }
480 } else if let Some(at) = paused_at.take() {
481 *paused_total += at.elapsed();
482 }
483}
484
485fn sanitize_speed(speed: f32) -> f32 {
486 if speed.is_finite() {
487 speed.clamp(0.0, 100.0)
488 } else {
489 1.0
490 }
491}
492
493fn sanitize_fps(fps: f32) -> f32 {
494 if fps.is_finite() {
495 fps.clamp(1.0, 240.0)
496 } else {
497 DEFAULT_TARGET_FPS
498 }
499}