1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
//! Experimental watchOS host runtime.
//!
//! watchOS gives an app no Metal of its own, so the host draws with
//! SpriteKit: each frame the runtime hands it the scene as sprites
//! ([`Application::sprites`]), and SpriteKit puts them on the GPU. The same
//! scene rasterizes on the CPU as well ([`Application::pixels`]), which is
//! what snapshots and tests read.
//!
//! The native host owns presentation and feeds physical surface sizes, touch
//! positions in points, crown deltas, and lifecycle changes into this runtime.
//! Application content remains an ordinary Rust composable. No wgpu or winit
//! dependency is required. The host installs the application's folders, taps,
//! the microphone permission and, with the `wearable` feature, the link to the
//! iPhone app. Audio playback, purchases, and accessibility are not yet
//! implemented by this experimental host.
pub use cranpose_render_pixels::sprites::{SPRITE_FLOATS, SpriteFrame, TextureUpload, field};
use cranpose_services::SystemTheme;
use cranpose_ui::EdgeInsets;
pub use crate::framebuffer::SurfaceError;
use crate::framebuffer::{Framebuffer, PixelRect, TouchPhase, frame_bytes};
/// One watch UI composition, drawn as sprites or into a retained RGBA
/// framebuffer on request.
///
/// Create and use this object on the native host's main thread.
pub struct Application {
framebuffer: Framebuffer,
pixels: Vec<u8>,
areas: Vec<PixelRect>,
sprites: SpriteFrame,
sprites_picture: Option<u64>,
}
impl Application {
/// Composes shared application content at the display's physical resolution.
/// `density` is physical pixels per point, as reported by the watch.
/// The host calls [`Self::set_active`] when the scene enters the foreground.
pub fn new(
width: u32,
height: u32,
density: f32,
content: impl FnMut() + 'static,
) -> Result<Self, SurfaceError> {
let mut pixels = Vec::new();
pixels
.try_reserve_exact(frame_bytes(width, height, density)?)
.map_err(|_| SurfaceError)?;
#[cfg(target_os = "watchos")]
crate::watchos_services::register();
// A watch shows its apps on black.
let framebuffer =
Framebuffer::new(width, height, density, SystemTheme::Dark, &[], content)?;
pixels.resize(pixels.capacity(), 0);
Ok(Self {
framebuffer,
pixels,
areas: Vec::new(),
sprites: SpriteFrame::default(),
sprites_picture: None,
})
}
/// Applies the screen's safe area in points: the edges the system draws
/// on, such as the clock. Content reads it from `local_safe_area_insets`.
/// Negative and non-finite edges count as none.
pub fn set_safe_area(&mut self, left: f32, top: f32, right: f32, bottom: f32) {
let edge = |value: f32| {
if value.is_finite() {
value.max(0.0)
} else {
0.0
}
};
self.framebuffer.set_safe_area(EdgeInsets::from_components(
edge(left),
edge(top),
edge(right),
edge(bottom),
));
}
/// Applies a new physical surface size without replacing the composition.
pub fn resize(&mut self, width: u32, height: u32, density: f32) -> Result<(), SurfaceError> {
let len = frame_bytes(width, height, density)?;
if len > self.pixels.len() {
self.pixels
.try_reserve_exact(len - self.pixels.len())
.map_err(|_| SurfaceError)?;
}
self.framebuffer.resize(width, height, density)?;
self.pixels.resize(len, 0);
Ok(())
}
/// Advances the shared frame clock. Returns whether the picture changed,
/// so a new frame should be presented.
pub fn tick(&mut self, elapsed_nanos: u64) -> bool {
self.framebuffer.tick(elapsed_nanos)
}
/// The current picture as a row-major, straight-alpha RGBA8 image,
/// rasterized on the CPU when it changed.
pub fn pixels(&mut self) -> &[u8] {
self.framebuffer.changed_areas(&mut self.areas);
self.areas.sort_unstable_by_key(|area| area.y);
let width = self.framebuffer.width();
let row_bytes = width as usize * 4;
let mut spans = self.areas.iter().map(|area| (area.y, area.y + area.height));
let mut current = spans.next();
while let Some((top, bottom)) = current {
let mut bottom = bottom;
current = None;
for (next_top, next_bottom) in spans.by_ref() {
if next_top > bottom {
current = Some((next_top, next_bottom));
break;
}
bottom = bottom.max(next_bottom);
}
let rows = top as usize * row_bytes..bottom as usize * row_bytes;
self.framebuffer.draw_area(
PixelRect {
x: 0,
y: top,
width,
height: bottom - top,
},
&mut self.pixels[rows],
);
}
&self.pixels
}
/// The sprites [`Self::sprites`] composed last, without composing.
pub fn last_sprites(&self) -> &SpriteFrame {
&self.sprites
}
/// The current picture as sprites for SpriteKit, with the textures it
/// first uses and those it no longer does; see
/// [`cranpose_render_pixels::sprites`]. Composed when the picture
/// changed; until it changes again, the same frame, uploads included.
pub fn sprites(&mut self) -> &SpriteFrame {
let picture = self.framebuffer.picture();
if self.sprites_picture != Some(picture) {
let sprites = &mut self.sprites;
self.framebuffer
.with_renderer(|renderer, width, height, density| {
renderer.compose_sprites(sprites, width, height, density);
});
self.sprites_picture = Some(picture);
}
&self.sprites
}
/// Returns the physical image width.
pub fn width(&self) -> u32 {
self.framebuffer.width()
}
/// Returns the physical image height.
pub fn height(&self) -> u32 {
self.framebuffer.height()
}
/// Routes one touch sample in display points: down=0, move=1, up=2, cancel=3.
/// Invalid phases and non-finite coordinates are ignored.
pub fn touch(&mut self, phase: u8, x: f32, y: f32) {
let phase = match phase {
0 => TouchPhase::Down,
1 => TouchPhase::Move,
2 => TouchPhase::Up,
3 => TouchPhase::Cancel,
_ => return,
};
self.framebuffer.touch(phase, x, y);
}
/// Routes crown units through Cranpose's existing detent-to-scroll conversion.
/// The native host supplies the timestamp in milliseconds since launch.
pub fn crown(&mut self, delta: f32, elapsed_millis: u64) -> bool {
self.framebuffer.rotary(delta, elapsed_millis)
}
/// Suspends frame production and cancels held touches when leaving the foreground.
pub fn set_active(&mut self, active: bool) {
self.framebuffer.set_active(active);
}
}