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}