OXYGENHUB
# fb420bbdb058 oxygenhub-android / rust-core / src / scenes.rs
5.8 KiB · Rust · last changed The hub's living backgrounds in the app: the mark at sign-in, Your sky on the dashboard, live runs pulse (refs #23) 8 days ago source blame history raw
1
//! The website's living backgrounds on the phone (E1): the hub's own
2
//! scenes (backdrop/, copied by scripts/sync-backdrop.sh) stepped here, and
3
//! drawn by the app from the floats each frame gives (ui/backdrop), as the
4
//! hub's WebGL draws them in a browser.
5
6
use crate::backdrop::adrift::Adrift;
7
use crate::backdrop::mark::Mark;
8
use crate::backdrop::scene::Scene;
9
use crate::backdrop::sky::{Colours, Event, Kind, Repo, Sky};
10
use std::sync::Mutex;
11
12
/// One event of the viewer's own activity, for Your sky: an index into the
13
/// repositories, its kind (0 a push's change, 1 a proposal event, 2 an
14
/// issue event, 3 a release, 4 a Flow run), how old it is and how many
15
/// marks it makes.
16
#[derive(uniffi::Record)]
17
pub struct SkyEvent {
18
    pub repo: u32,
19
    pub kind: u32,
20
    pub age_days: f32,
21
    pub n: u32,
22
}
23
24
/// A frame to draw: marks, 8 floats each (x, y, size, shape, then a
25
/// premultiplied colour), and links, 12 floats each (two ends, each x, y
26
/// and a premultiplied colour), as little-endian bytes: a byte array
27
/// crosses to Kotlin as one copy, where a list of floats would be boxed.
28
#[derive(uniffi::Record)]
29
pub struct BackdropFrame {
30
    pub marks: Vec<u8>,
31
    pub links: Vec<u8>,
32
}
33
34
fn bytes(v: &[f32]) -> Vec<u8> {
35
    v.iter().flat_map(|f| f.to_le_bytes()).collect()
36
}
37
38
/// The name a resting finger shows, and where.
39
#[derive(uniffi::Record)]
40
pub struct BackdropLabel {
41
    pub text: String,
42
    pub x: f32,
43
    pub y: f32,
44
}
45
46
/// A scene and the colours it is drawn in.
47
#[derive(uniffi::Object)]
48
pub struct Backdrop {
49
    scene: Mutex<Box<dyn Scene + Send>>,
50
    colours: Mutex<Colours>,
51
}
52
53
fn rgba(c: &[f32], i: usize) -> [f32; 4] {
54
    [c[i * 4], c[i * 4 + 1], c[i * 4 + 2], c[i * 4 + 3]]
55
}
56
57
impl Backdrop {
58
    fn of(scene: Box<dyn Scene + Send>) -> std::sync::Arc<Backdrop> {
59
        let grey = [0.5, 0.55, 0.65, 0.5];
60
        let colours = Colours { mark: grey, lit: grey, link: grey, ok: grey, warn: grey, bad: grey };
61
        std::sync::Arc::new(Backdrop { scene: Mutex::new(scene), colours: Mutex::new(colours) })
62
    }
63
}
64
65
#[uniffi::export]
66
impl Backdrop {
67
    /// The OxygenHub mark, as behind the hub's sign-in page.
68
    #[uniffi::constructor]
69
    pub fn mark(still: bool) -> std::sync::Arc<Backdrop> {
70
        Backdrop::of(Box::new(Mark::new(still)))
71
    }
72
73
    /// The quiet field behind the hub's error pages, for an empty screen.
74
    #[uniffi::constructor]
75
    pub fn adrift(still: bool) -> std::sync::Arc<Backdrop> {
76
        Backdrop::of(Box::new(Adrift::new(still)))
77
    }
78
79
    /// Your sky, as on the hub's dashboard: the viewer's own repositories
80
    /// and their activity, nothing inferred.
81
    #[uniffi::constructor]
82
    pub fn sky(repos: Vec<String>, events: Vec<SkyEvent>, w: f32, h: f32, still: bool) -> std::sync::Arc<Backdrop> {
83
        let repos: Vec<Repo> = repos.into_iter().map(|name| Repo { name }).collect();
84
        let events: Vec<Event> = events
85
            .into_iter()
86
            .filter(|e| (e.repo as usize) < repos.len())
87
            .map(|e| Event { repo: e.repo as usize, kind: Kind::from_code(e.kind), age_days: e.age_days, n: e.n.max(1) })
88
            .collect();
89
        Backdrop::of(Box::new(Sky::new(&repos, &events, w, h, still)))
90
    }
91
92
    /// The colours to draw in, from the app's theme: mark, lit, link, ok,
93
    /// warn and bad, 4 floats each (red, green, blue, alpha, 0 to 1).
94
    pub fn colours(&self, rgba_x6: Vec<f32>) {
95
        if rgba_x6.len() < 24 {
96
            return;
97
        }
98
        let c = &rgba_x6;
99
        *self.colours.lock().unwrap() =
100
            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) };
101
    }
102
103
    /// The view is now `w` by `h` (density-independent pixels).
104
    pub fn resize(&self, w: f32, h: f32) {
105
        self.scene.lock().unwrap().resize(w, h, None);
106
    }
107
108
    /// A finger at `(x, y)`, or lifted.
109
    pub fn point(&self, x: f32, y: f32, down: bool) {
110
        self.scene.lock().unwrap().point(down.then_some((x, y)));
111
    }
112
113
    /// Move on by `dt` seconds; whether anything but the drift is moving.
114
    pub fn step(&self, dt: f32) -> bool {
115
        let mut s = self.scene.lock().unwrap();
116
        s.step(dt.clamp(0.0, 0.1));
117
        s.busy()
118
    }
119
120
    /// What to draw now.
121
    pub fn frame(&self) -> BackdropFrame {
122
        let (s, c) = (self.scene.lock().unwrap(), self.colours.lock().unwrap());
123
        let (mut marks, mut links) = (Vec::new(), Vec::new());
124
        s.instances(&c, &mut marks);
125
        s.links(&c, &mut links);
126
        BackdropFrame { marks: bytes(&marks), links: bytes(&links) }
127
    }
128
129
    /// The name a resting finger shows, if any.
130
    pub fn label(&self) -> Option<BackdropLabel> {
131
        self.scene.lock().unwrap().label().map(|(t, x, y)| BackdropLabel { text: t.to_string(), x, y })
132
    }
133
}
134
135
#[cfg(test)]
136
mod tests {
137
    use super::*;
138
139
    #[test]
140
    fn a_sky_of_real_repositories_draws_marks_and_moves() {
141
        let b = Backdrop::sky(
142
            vec!["andy/oxygen".into(), "andy/hub".into()],
143
            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 }],
144
            400.0,
145
            800.0,
146
            false,
147
        );
148
        b.colours(vec![0.6; 24]);
149
        b.step(0.016);
150
        let f = b.frame();
151
        assert!(!f.marks.is_empty() && f.marks.len() % 32 == 0, "{} bytes", f.marks.len());
152
        assert_eq!(f.links.len() % 48, 0);
153
        let first = f.marks.clone();
154
        for _ in 0..30 {
155
            b.step(0.033);
156
        }
157
        assert_ne!(b.frame().marks, first, "it drifts");
158
    }
159
160
    #[test]
161
    fn the_mark_and_the_field_draw_too() {
162
        for b in [Backdrop::mark(false), Backdrop::adrift(true)] {
163
            b.resize(360.0, 640.0);
164
            b.step(0.016);
165
            assert!(!b.frame().marks.is_empty());
166
        }
167
    }
168
}