//! The website's living backgrounds on the phone (E1): the hub's own //! scenes (backdrop/, copied by scripts/sync-backdrop.sh) stepped here, and //! drawn by the app from the floats each frame gives (ui/backdrop), as the //! hub's WebGL draws them in a browser. use crate::backdrop::adrift::Adrift; use crate::backdrop::mark::Mark; use crate::backdrop::scene::Scene; use crate::backdrop::sky::{Colours, Event, Kind, Repo, Sky}; use std::sync::Mutex; /// One event of the viewer's own activity, for Your sky: an index into the /// repositories, its kind (0 a push's change, 1 a proposal event, 2 an /// issue event, 3 a release, 4 a Flow run), how old it is and how many /// marks it makes. #[derive(uniffi::Record)] pub struct SkyEvent { pub repo: u32, pub kind: u32, pub age_days: f32, pub n: u32, } /// A frame to draw: marks, 8 floats each (x, y, size, shape, then a /// premultiplied colour), and links, 12 floats each (two ends, each x, y /// and a premultiplied colour), as little-endian bytes: a byte array /// crosses to Kotlin as one copy, where a list of floats would be boxed. #[derive(uniffi::Record)] pub struct BackdropFrame { pub marks: Vec, pub links: Vec, } fn bytes(v: &[f32]) -> Vec { v.iter().flat_map(|f| f.to_le_bytes()).collect() } /// The name a resting finger shows, and where. #[derive(uniffi::Record)] pub struct BackdropLabel { pub text: String, pub x: f32, pub y: f32, } /// A scene and the colours it is drawn in. #[derive(uniffi::Object)] pub struct Backdrop { scene: Mutex>, colours: Mutex, } fn rgba(c: &[f32], i: usize) -> [f32; 4] { [c[i * 4], c[i * 4 + 1], c[i * 4 + 2], c[i * 4 + 3]] } impl Backdrop { fn of(scene: Box) -> std::sync::Arc { let grey = [0.5, 0.55, 0.65, 0.5]; let colours = Colours { mark: grey, lit: grey, link: grey, ok: grey, warn: grey, bad: grey }; std::sync::Arc::new(Backdrop { scene: Mutex::new(scene), colours: Mutex::new(colours) }) } } #[uniffi::export] impl Backdrop { /// The OxygenHub mark, as behind the hub's sign-in page. #[uniffi::constructor] pub fn mark(still: bool) -> std::sync::Arc { Backdrop::of(Box::new(Mark::new(still))) } /// The quiet field behind the hub's error pages, for an empty screen. #[uniffi::constructor] pub fn adrift(still: bool) -> std::sync::Arc { Backdrop::of(Box::new(Adrift::new(still))) } /// Your sky, as on the hub's dashboard: the viewer's own repositories /// and their activity, nothing inferred. #[uniffi::constructor] pub fn sky(repos: Vec, events: Vec, w: f32, h: f32, still: bool) -> std::sync::Arc { let repos: Vec = repos.into_iter().map(|name| Repo { name }).collect(); let events: Vec = events .into_iter() .filter(|e| (e.repo as usize) < repos.len()) .map(|e| Event { repo: e.repo as usize, kind: Kind::from_code(e.kind), age_days: e.age_days, n: e.n.max(1) }) .collect(); Backdrop::of(Box::new(Sky::new(&repos, &events, w, h, still))) } /// The colours to draw in, from the app's theme: mark, lit, link, ok, /// warn and bad, 4 floats each (red, green, blue, alpha, 0 to 1). pub fn colours(&self, rgba_x6: Vec) { if rgba_x6.len() < 24 { return; } let c = &rgba_x6; *self.colours.lock().unwrap() = Colours { mark: rgba(c, 0), lit: rgba(c, 1), link: rgba(c, 2), ok: rgba(c, 3), warn: rgba(c, 4), bad: rgba(c, 5) }; } /// The view is now `w` by `h` (density-independent pixels). pub fn resize(&self, w: f32, h: f32) { self.scene.lock().unwrap().resize(w, h, None); } /// A finger at `(x, y)`, or lifted. pub fn point(&self, x: f32, y: f32, down: bool) { self.scene.lock().unwrap().point(down.then_some((x, y))); } /// Move on by `dt` seconds; whether anything but the drift is moving. pub fn step(&self, dt: f32) -> bool { let mut s = self.scene.lock().unwrap(); s.step(dt.clamp(0.0, 0.1)); s.busy() } /// What to draw now. pub fn frame(&self) -> BackdropFrame { let (s, c) = (self.scene.lock().unwrap(), self.colours.lock().unwrap()); let (mut marks, mut links) = (Vec::new(), Vec::new()); s.instances(&c, &mut marks); s.links(&c, &mut links); BackdropFrame { marks: bytes(&marks), links: bytes(&links) } } /// The name a resting finger shows, if any. pub fn label(&self) -> Option { self.scene.lock().unwrap().label().map(|(t, x, y)| BackdropLabel { text: t.to_string(), x, y }) } } #[cfg(test)] mod tests { use super::*; #[test] fn a_sky_of_real_repositories_draws_marks_and_moves() { let b = Backdrop::sky( vec!["andy/oxygen".into(), "andy/hub".into()], vec![SkyEvent { repo: 0, kind: 0, age_days: 1.0, n: 5 }, SkyEvent { repo: 1, kind: 4, age_days: 2.0, n: 1 }, SkyEvent { repo: 9, kind: 0, age_days: 1.0, n: 1 }], 400.0, 800.0, false, ); b.colours(vec![0.6; 24]); b.step(0.016); let f = b.frame(); assert!(!f.marks.is_empty() && f.marks.len() % 32 == 0, "{} bytes", f.marks.len()); assert_eq!(f.links.len() % 48, 0); let first = f.marks.clone(); for _ in 0..30 { b.step(0.033); } assert_ne!(b.frame().marks, first, "it drifts"); } #[test] fn the_mark_and_the_field_draw_too() { for b in [Backdrop::mark(false), Backdrop::adrift(true)] { b.resize(360.0, 640.0); b.step(0.016); assert!(!b.frame().marks.is_empty()); } } }