Skip to main content

rdi_core/
scene.rs

1use crate::{AnimationCurve, DesktopError, IconAnimationSpec, IconFrame, IconRenderPlan, OverlayRenderOptions, Point};
2
3#[derive(Clone, Debug)]
4pub struct CapturedFrame {
5    pub width: u32,
6    pub height: u32,
7    pub seconds: f64,
8    pub pixels: Vec<u8>,
9}
10
11pub trait SceneRenderer {
12    fn render(&mut self, frame: &[IconFrame], seconds: f64) -> Result<CapturedFrame, DesktopError>;
13}
14
15pub struct RenderSession {
16    pub(crate) tx: crossbeam_channel::Sender<RenderRequest>,
17    pub(crate) worker: std::thread::ThreadId,
18    pub duration: f64,
19}
20
21pub(crate) enum RenderRequest {
22    Frame(f64, crossbeam_channel::Sender<Result<CapturedFrame, DesktopError>>),
23    Close(crossbeam_channel::Sender<()>),
24}
25
26impl RenderSession {
27    pub fn render_at(&self, seconds: f64) -> Result<CapturedFrame, DesktopError> {
28        if std::thread::current().id() == self.worker {
29            return Err(DesktopError::BackendUnavailable("render calls cannot run on the worker".into()));
30        }
31        let (reply, response) = crossbeam_channel::bounded(1);
32        self.tx.send(RenderRequest::Frame(seconds, reply)).map_err(|_| DesktopError::BackendUnavailable("render session closed".into()))?;
33        response.recv().map_err(|_| DesktopError::WorkerCrashed("render worker stopped".into()))?
34    }
35
36    pub fn close(&self) -> Result<(), DesktopError> {
37        if std::thread::current().id() == self.worker {
38            return Err(DesktopError::BackendUnavailable("close cannot run on the worker".into()));
39        }
40        let (reply, response) = crossbeam_channel::bounded(1);
41        if self.tx.send(RenderRequest::Close(reply)).is_ok() { let _ = response.recv(); }
42        Ok(())
43    }
44}
45
46impl Drop for RenderSession {
47    fn drop(&mut self) {
48        let (reply, _) = crossbeam_channel::bounded(1);
49        let _ = self.tx.send(RenderRequest::Close(reply));
50    }
51}
52
53#[derive(Clone, Copy, Debug)]
54pub struct Canvas {
55    pub width: u32,
56    pub height: u32,
57    pub dpi_scale: f32,
58    pub icon_size: u32,
59}
60
61impl Canvas {
62    pub fn validate(&self) -> Result<(), DesktopError> {
63        if self.width == 0 || self.height == 0 || self.width > 8192 || self.height > 8192
64            || u64::from(self.width) * u64::from(self.height) > 33_554_432
65            || !self.dpi_scale.is_finite() || !(0.5..=4.0).contains(&self.dpi_scale)
66            || !(16..=256).contains(&self.icon_size)
67        {
68            return Err(DesktopError::InvalidEffect("canvas requires dimensions 1..8192, at most 33554432 pixels, DPI scale 0.5..4 and icon size 16..256 DIP".into()));
69        }
70        Ok(())
71    }
72}
73
74#[derive(Clone, Debug)]
75pub struct SceneIcon {
76    pub origin: Point,
77    pub animation: IconAnimationSpec,
78}
79
80#[derive(Clone, Debug)]
81pub struct Scene {
82    pub canvas: Canvas,
83    pub icons: Vec<SceneIcon>,
84    pub render_options: OverlayRenderOptions,
85}
86
87pub(crate) fn position_at(origin: Point, target: Point, horizontal: &crate::Curve, vertical: &crate::Curve, progress: f32) -> Point {
88    Point::new(
89        (origin.x as f32 + horizontal.eval(progress) * (target.x as f32 - origin.x as f32)).round() as i32,
90        (origin.y as f32 + vertical.eval(progress) * (target.y as f32 - origin.y as f32)).round() as i32,
91    )
92}
93
94pub(crate) fn progress_at(duration: f64, position: f64, elapsed: f64) -> f32 {
95    if duration == 0.0 { return if position > 0.0 { 1.0 } else { 0.0 }; }
96    (elapsed / duration).clamp(0.0, 1.0) as f32
97}
98
99pub(crate) struct EvaluatedScene {
100    pub scene: Scene,
101    pub duration: f64,
102    durations: Vec<f64>,
103}
104
105impl EvaluatedScene {
106    pub fn new(scene: Scene) -> Result<Self, DesktopError> {
107        scene.canvas.validate()?;
108        let mut ids = std::collections::HashSet::new();
109        let mut durations = Vec::with_capacity(scene.icons.len());
110        for icon in &scene.icons {
111            if !ids.insert(icon.animation.id.clone()) {
112                return Err(DesktopError::InvalidEffect("duplicate scene icon id".into()));
113            }
114            if let Some(effect) = &icon.animation.effect { effect.validate()?; }
115            durations.push(icon.animation.duration.resolve(icon.origin, icon.animation.target)?.as_secs_f64());
116        }
117        let duration = durations.iter().copied().fold(0.0, f64::max);
118        Ok(Self { scene, duration, durations })
119    }
120
121    pub fn plans(&self) -> Vec<IconRenderPlan> {
122        self.scene.icons.iter().map(|icon| {
123            let mut plan = IconRenderPlan::placeholder(icon.animation.id.clone(), icon.origin, icon.animation.target);
124            plan.effect = icon.animation.effect.clone();
125            plan
126        }).collect()
127    }
128
129    pub fn sample(&self, seconds: f64) -> Result<Vec<IconFrame>, DesktopError> {
130        if !seconds.is_finite() || seconds < 0.0 || seconds > self.duration {
131            return Err(DesktopError::InvalidDuration("scene time must be finite and within its duration".into()));
132        }
133        Ok(self.scene.icons.iter().zip(&self.durations).map(|(icon, duration)| {
134            let progress = progress_at(*duration, seconds, seconds);
135            let spec = &icon.animation;
136            let position = if progress <= 0.0 { icon.origin } else if progress >= 1.0 { spec.target }
137                else { position_at(icon.origin, spec.target, &spec.curve_x, &spec.curve_y, progress) };
138            IconFrame { id: spec.id.clone(), position, progress, elapsed_seconds: seconds as f32 }
139        }).collect())
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    struct ReadOnlyBackend;
148    struct TestRenderer;
149
150    impl SceneRenderer for TestRenderer {
151        fn render(&mut self, frame: &[IconFrame], seconds: f64) -> Result<CapturedFrame, DesktopError> {
152            Ok(CapturedFrame { width: 1, height: 1, seconds, pixels: vec![frame[0].position.x as u8, 0, 0, 255] })
153        }
154    }
155
156    impl crate::DesktopBackend for ReadOnlyBackend {
157        fn list_icons(&mut self) -> Result<Vec<crate::IconSnapshot>, DesktopError> {
158            Ok(vec![crate::IconSnapshot::new("icon".into(), "Icon", None, false, Point::new(999, 999))])
159        }
160        fn get_flags(&mut self) -> Result<u32, DesktopError> { Ok(42) }
161        fn list_monitors(&mut self) -> Result<Vec<crate::MonitorInfo>, DesktopError> { Ok(vec![]) }
162        fn apply_flags(&mut self, _: u32, _: u32) -> Result<(), DesktopError> { panic!("scene wrote flags") }
163        fn set_positions(&mut self, _: &[(crate::IconId, Point)]) -> Result<Vec<crate::IconId>, DesktopError> { panic!("scene moved icons") }
164        fn begin_overlay_session(&mut self, _: &[IconRenderPlan], _: OverlayRenderOptions) -> Result<(), DesktopError> { panic!("scene opened overlay") }
165        fn commit_overlay_frame(&mut self, _: &[(crate::IconId, Point)]) -> Result<(), DesktopError> { panic!("scene committed overlay") }
166        fn finalize_overlay_session(&mut self, _: &[(crate::IconId, Point)]) -> Result<crate::FinalCommitOutcome, DesktopError> { panic!("scene finalized overlay") }
167        fn prepare_scene_renderer(&mut self, _: Canvas, _: &[IconRenderPlan], _: OverlayRenderOptions) -> Result<Box<dyn SceneRenderer>, DesktopError> { Ok(Box::new(TestRenderer)) }
168    }
169
170    fn sample_scene() -> Scene {
171        Scene { canvas: Canvas { width: 640, height: 480, dpi_scale: 1.0, icon_size: 48 },
172            icons: vec![SceneIcon { origin: Point::new(20, 30), animation: IconAnimationSpec::new("icon".into(), Point::new(220, 30),
173                crate::Duration::fixed(std::time::Duration::from_secs(2)), crate::Curve::linear()) }],
174            render_options: OverlayRenderOptions::default() }
175    }
176
177    #[test]
178    fn scene_session_is_read_only_and_releases_on_close_drop_shutdown() {
179        let controller = crate::DesktopController::new(ReadOnlyBackend).unwrap();
180        let scene = sample_scene();
181        let session = controller.prepare_scene(scene.clone()).unwrap();
182        assert_eq!(session.render_at(1.0).unwrap().pixels[0], 120);
183        assert_eq!(controller.get_flags().unwrap(), 42);
184        assert!(matches!(controller.prepare_scene(scene.clone()), Err(DesktopError::AnimationBusy)));
185        assert!(matches!(controller.set_positions(vec![]), Err(DesktopError::AnimationBusy)));
186        assert!(session.render_at(f64::NAN).is_err());
187        assert!(session.render_at(3.0).is_err());
188        session.close().unwrap();
189        session.close().unwrap();
190        assert!(session.render_at(0.0).is_err());
191        drop(controller.prepare_scene(scene.clone()).unwrap());
192        let reopened = controller.prepare_scene(scene).unwrap();
193        assert_eq!(reopened.render_at(0.0).unwrap().pixels[0], 20);
194        controller.shutdown();
195        assert!(reopened.render_at(0.0).is_err());
196    }
197
198    #[test]
199    fn canvas_accepts_8k_with_bounded_allocations() {
200        let mut canvas = Canvas { width: 7680, height: 4320, dpi_scale: 1.0, icon_size: 96 };
201        assert!(canvas.validate().is_ok());
202        canvas.width = 8192;
203        canvas.height = 8192;
204        assert!(canvas.validate().is_err());
205    }
206
207    #[test]
208    fn scene_rejects_duplicate_ids_and_invalid_canvas() {
209        let mut scene = sample_scene();
210        scene.icons.push(scene.icons[0].clone());
211        assert!(EvaluatedScene::new(scene).is_err());
212        let mut scene = sample_scene();
213        scene.canvas.width = u32::MAX;
214        assert!(EvaluatedScene::new(scene).is_err());
215    }
216
217    #[test]
218    fn scene_samples_explicit_origins_and_rewinds() {
219        let icons = [1.0, 2.0].into_iter().enumerate().map(|(index, seconds)| SceneIcon {
220            origin: Point::new(20, 30),
221            animation: IconAnimationSpec::new(format!("icon{index}").into(), Point::new(220, 30),
222                crate::Duration::fixed(std::time::Duration::from_secs_f64(seconds)), crate::Curve::linear()),
223        }).collect();
224        let scene = EvaluatedScene::new(Scene {
225            canvas: Canvas { width: 640, height: 480, dpi_scale: 1.0, icon_size: 48 },
226            icons, render_options: OverlayRenderOptions::default(),
227        }).unwrap();
228        assert_eq!(scene.duration, 2.0);
229        let first = scene.sample(0.5).unwrap();
230        assert_eq!(first[0].position, Point::new(120, 30));
231        assert_eq!(first[1].position, Point::new(70, 30));
232        scene.sample(2.0).unwrap();
233        for (before, after) in first.iter().zip(scene.sample(0.5).unwrap()) {
234            assert_eq!(before.position, after.position);
235            assert_eq!(before.progress, after.progress);
236            assert_eq!(before.elapsed_seconds, after.elapsed_seconds);
237        }
238        assert!(scene.sample(f64::NAN).is_err());
239        assert!(scene.sample(2.1).is_err());
240    }
241}