use std::{cell::RefCell, rc::Rc, time::Duration};
use gpui::{
Bounds, Context, IntoElement, ParentElement, Pixels, Render, Styled, Task, Window, canvas, div,
px,
};
use web_time::Instant;
use crate::orbs::{
engine::{Frame, draw_mode_into, draw_mode_into_resolved},
paint::paint_frame,
presets::{Resolved, resolve_preset},
types::{OrbSize, OrbState, OrbTheme},
};
pub const DEFAULT_TARGET_FPS: f32 = 30.0;
pub struct Orb {
state: OrbState,
size: OrbSize,
theme: OrbTheme,
speed: f32,
paused: bool,
reduced_motion: bool,
target_fps: f32,
pause_when_inactive: bool,
visible: bool,
started: Instant,
paused_total: Duration,
paused_at: Option<Instant>,
cache_key: (OrbState, OrbSize),
resolved: Resolved,
frame: Rc<RefCell<Frame>>,
geometry_dirty: bool,
tick: Option<Task<()>>,
activation: Option<gpui::Subscription>,
}
impl Default for Orb {
fn default() -> Self {
Self::new()
}
}
impl Orb {
pub fn new() -> Self {
let state = OrbState::Working;
let size = OrbSize::Avatar;
Self {
state,
size,
theme: OrbTheme::Auto,
speed: 1.0,
paused: false,
reduced_motion: false,
target_fps: DEFAULT_TARGET_FPS,
pause_when_inactive: true,
visible: true,
started: Instant::now(),
paused_total: Duration::ZERO,
paused_at: None,
cache_key: (state, size),
resolved: resolve_preset(state, size),
frame: Rc::new(RefCell::new(Frame::new())),
geometry_dirty: true,
tick: None,
activation: None,
}
}
pub fn state(mut self, state: OrbState) -> Self {
self.state = state;
self
}
pub fn size(mut self, size: OrbSize) -> Self {
self.size = size;
self
}
pub fn theme(mut self, theme: OrbTheme) -> Self {
self.theme = theme;
self
}
pub fn speed(mut self, speed: f32) -> Self {
self.speed = sanitize_speed(speed);
self
}
pub fn paused(mut self, paused: bool) -> Self {
apply_pause_clock(
&mut self.paused,
&mut self.paused_at,
&mut self.paused_total,
paused,
);
self
}
pub fn reduced_motion(mut self, reduced: bool) -> Self {
self.reduced_motion = reduced;
self
}
pub fn target_fps(mut self, fps: f32) -> Self {
self.target_fps = sanitize_fps(fps);
self
}
pub fn pause_when_inactive(mut self, pause: bool) -> Self {
self.pause_when_inactive = pause;
self
}
pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn set_state(&mut self, state: OrbState, cx: &mut Context<Self>) {
if self.state != state {
self.state = state;
self.geometry_dirty = true;
cx.notify();
}
}
pub fn set_size(&mut self, size: OrbSize, cx: &mut Context<Self>) {
if self.size != size {
self.size = size;
self.geometry_dirty = true;
cx.notify();
}
}
pub fn set_theme(&mut self, theme: OrbTheme, cx: &mut Context<Self>) {
if self.theme != theme {
self.theme = theme;
cx.notify();
}
}
pub fn set_speed(&mut self, speed: f32, cx: &mut Context<Self>) {
let speed = sanitize_speed(speed);
if self.speed != speed {
self.speed = speed;
self.geometry_dirty = true;
cx.notify();
}
}
pub fn set_paused(&mut self, paused: bool, cx: &mut Context<Self>) {
if self.paused == paused {
return;
}
apply_pause_clock(
&mut self.paused,
&mut self.paused_at,
&mut self.paused_total,
paused,
);
self.geometry_dirty = true;
cx.notify();
}
pub fn set_reduced_motion(&mut self, reduced: bool, cx: &mut Context<Self>) {
if self.reduced_motion != reduced {
self.reduced_motion = reduced;
self.geometry_dirty = true;
cx.notify();
}
}
pub fn set_target_fps(&mut self, fps: f32, cx: &mut Context<Self>) {
self.target_fps = sanitize_fps(fps);
cx.notify();
}
pub fn set_pause_when_inactive(&mut self, pause: bool, cx: &mut Context<Self>) {
if self.pause_when_inactive != pause {
self.pause_when_inactive = pause;
cx.notify();
}
}
pub fn set_visible(&mut self, visible: bool, cx: &mut Context<Self>) {
if self.visible != visible {
self.visible = visible;
self.geometry_dirty = true;
cx.notify();
}
}
pub fn state_value(&self) -> OrbState {
self.state
}
pub fn size_value(&self) -> OrbSize {
self.size
}
pub fn theme_value(&self) -> OrbTheme {
self.theme
}
pub fn speed_value(&self) -> f32 {
self.speed
}
pub fn is_paused(&self) -> bool {
self.paused
}
pub fn reduced_motion_value(&self) -> bool {
self.reduced_motion
}
pub fn target_fps_value(&self) -> f32 {
self.target_fps
}
pub fn pause_when_inactive_value(&self) -> bool {
self.pause_when_inactive
}
pub fn is_visible(&self) -> bool {
self.visible
}
fn dark(&self) -> bool {
match self.theme {
OrbTheme::Dark => true,
OrbTheme::Light => false,
OrbTheme::Auto => theme::current_appearance().is_dark(),
}
}
fn time_seconds(&self, reduced: bool) -> f32 {
if reduced {
return 0.6;
}
let paused = match self.paused_at {
Some(at) => self.paused_total + at.elapsed(),
None => self.paused_total,
};
let live = self.started.elapsed().saturating_sub(paused);
(live.as_secs_f64() * self.resolved.speed as f64 * self.speed as f64) as f32
}
fn schedule_tick(&mut self, cx: &mut Context<Self>) {
if self.tick.is_some() {
return;
}
let period = Duration::from_secs_f32(1.0 / self.target_fps);
self.tick = Some(cx.spawn(async move |this, cx| {
cx.background_executor().timer(period).await;
let _ = this.update(cx, |orb, cx| {
orb.tick = None;
orb.geometry_dirty = true;
cx.notify();
});
}));
}
}
impl Render for Orb {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.activation.is_none() {
self.activation = Some(cx.observe_window_activation(window, |orb, _, cx| {
orb.geometry_dirty = true;
cx.notify();
}));
}
let key = (self.state, self.size);
if self.cache_key != key {
self.cache_key = key;
self.resolved = resolve_preset(self.state, self.size);
}
let size_px = self.size.pixels();
let dark = self.dark();
let reduced = self.reduced_motion || cx.reduce_motion();
let t = self.time_seconds(reduced);
let r_min = self.resolved.opts.r_min.unwrap_or(0.3);
if self.geometry_dirty {
draw_mode_into_resolved(
self.resolved.mode,
size_px,
t,
&self.resolved.opts,
&mut self.frame.borrow_mut(),
);
self.geometry_dirty = false;
}
let animating = self.visible
&& !self.paused
&& !reduced
&& (!self.pause_when_inactive || window.is_window_active());
if animating {
self.schedule_tick(cx);
} else {
self.tick = None;
}
let frame = self.frame.clone();
div()
.size(px(size_px))
.flex_shrink_0()
.overflow_hidden()
.child(
canvas(
move |_bounds: Bounds<Pixels>, _window, _cx| (),
move |bounds, (), window, _cx| {
paint_frame(window, bounds, &frame.borrow(), dark, r_min);
},
)
.size_full(),
)
}
}
pub fn orb_element(
state: OrbState,
size: OrbSize,
t: f32,
frame: &Rc<RefCell<Frame>>,
) -> impl IntoElement {
let resolved = resolve_preset(state, size);
let size_px = size.pixels();
let frame = frame.clone();
draw_mode_into(
resolved.mode,
size_px,
t,
&resolved.opts,
&mut frame.borrow_mut(),
);
let dark = theme::current_appearance().is_dark();
let r_min = resolved.opts.r_min.unwrap_or(0.3);
div()
.size(px(size_px))
.flex_shrink_0()
.overflow_hidden()
.child(
canvas(
move |_bounds: Bounds<Pixels>, _window, _cx| (),
move |bounds, (), window, _cx| {
paint_frame(window, bounds, &frame.borrow(), dark, r_min);
},
)
.size_full(),
)
}
fn apply_pause_clock(
paused: &mut bool,
paused_at: &mut Option<Instant>,
paused_total: &mut Duration,
want: bool,
) {
if *paused == want {
if !want && let Some(at) = paused_at.take() {
*paused_total += at.elapsed();
}
return;
}
*paused = want;
if want {
if paused_at.is_none() {
*paused_at = Some(Instant::now());
}
} else if let Some(at) = paused_at.take() {
*paused_total += at.elapsed();
}
}
fn sanitize_speed(speed: f32) -> f32 {
if speed.is_finite() {
speed.clamp(0.0, 100.0)
} else {
1.0
}
}
fn sanitize_fps(fps: f32) -> f32 {
if fps.is_finite() {
fps.clamp(1.0, 240.0)
} else {
DEFAULT_TARGET_FPS
}
}