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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
use std::sync::Arc;
use cranpose_app_shell::{AppShell, default_root_key};
use cranpose_render_wgpu::{WgpuRenderer, WgpuTextSystem};
use crate::{AppSettings, frame_readback::FrameTarget, offscreen_instance::OFFSCREEN_INSTANCE};
const FRAME_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
/// Failure to initialize, resize or draw an embedded component.
#[derive(Debug, thiserror::Error)]
pub enum EmbeddedViewError {
/// Dimensions must be nonzero, fit the GPU limit and fit the readback budget.
#[error("invalid embedded size {width} x {height} at density {density}")]
InvalidSize {
/// Requested pixel width.
width: u32,
/// Requested pixel height.
height: u32,
/// Requested pixels per logical point.
density: f32,
},
/// No offscreen GPU adapter is available.
#[error("no GPU adapter: {0}")]
Adapter(#[from] wgpu::RequestAdapterError),
/// The adapter could not create the device.
#[error("GPU device: {0}")]
Device(#[from] wgpu::RequestDeviceError),
/// Cranpose could not render the scene.
#[error("render: {0}")]
Render(String),
/// GPU readback failed.
#[error("readback: {0}")]
Readback(String),
}
/// A Cranpose composition owned and driven by another application's view.
///
/// This component starts no application, window or event loop. The host owns
/// lifecycle, input and scheduling through [Self::shell]. All methods run on
/// the creating thread. Each instance has its own composition and app context.
/// Offscreen components share GPU backend initialization across worker threads.
///
/// [Self::draw] renders to a reusable GPU target and reads pixels only when
/// changed. This portable adapter copies pixels; it is not a zero-copy native
/// surface. Hosts with a Rust GPU integration can instead use
/// `AppShell<WgpuRenderer>` to render directly into their texture.
pub struct EmbeddedView {
shell: AppShell<WgpuRenderer>,
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
target: FrameTarget,
dirty: bool,
visible: bool,
density: f32,
}
impl EmbeddedView {
/// Creates an independent component with a physical size and logical density.
///
/// Pixel buffers are limited to 64 MiB. Zero-size views should be suspended
/// with [Self::set_visible] until their native layout has a usable size.
pub fn new(
settings: &AppSettings,
width: u32,
height: u32,
density: f32,
content: impl FnMut() + 'static,
) -> Result<Self, EmbeddedViewError> {
validate_size(width, height, density, u32::MAX)?;
let adapter = pollster::block_on(OFFSCREEN_INSTANCE.request_adapter(
&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
..Default::default()
},
))?;
let (device, queue) =
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("Cranpose embedded view"),
required_features: cranpose_render_wgpu::optional_device_features(&adapter),
required_limits: crate::gpu_limits::mobile_device_limits(adapter.limits()),
memory_hints: crate::gpu_limits::mobile_memory_hints(),
..Default::default()
}))?;
validate_size(
width,
height,
density,
device.limits().max_texture_dimension_2d,
)?;
let device = Arc::new(device);
let queue = Arc::new(queue);
let mut renderer = WgpuRenderer::with_text_system(WgpuTextSystem::from_font_set(
settings.resolve_font_set(),
));
renderer.init_gpu(
Arc::clone(&device),
Arc::clone(&queue),
FRAME_FORMAT,
adapter.get_info().backend,
adapter.get_downlevel_capabilities().flags,
);
let shell = AppShell::new_with_size_and_density(
renderer,
default_root_key(),
content,
(width, height),
(width as f32 / density, height as f32 / density),
density,
);
let target = FrameTarget::new(&device, width, height, FRAME_FORMAT);
Ok(Self {
shell,
device,
queue,
target,
dirty: true,
visible: true,
density,
})
}
/// Accesses input, scheduling, semantics, text input and the app context.
///
/// Install a frame waker, route pointer events in logical coordinates, and
/// honor `frame_schedule()` after each draw. Native controls keep their own
/// touch, focus and accessibility handling.
pub fn shell(&mut self) -> &mut AppShell<WgpuRenderer> {
&mut self.shell
}
/// Updates physical dimensions and density after native layout changes.
///
/// Unchanged dimensions reuse GPU resources. Invalid dimensions leave the
/// previous configuration intact.
pub fn resize(
&mut self,
width: u32,
height: u32,
density: f32,
) -> Result<(), EmbeddedViewError> {
validate_size(
width,
height,
density,
self.device.limits().max_texture_dimension_2d,
)?;
let size_changed = self.target.size() != (width, height);
let density_changed = self.density != density;
if !size_changed && !density_changed {
return Ok(());
}
if size_changed {
self.target = FrameTarget::new(&self.device, width, height, FRAME_FORMAT);
}
self.shell
.primary()
.set_viewport(width as f32 / density, height as f32 / density);
self.shell.set_density(density);
self.density = density;
self.shell.set_buffer_size(width, height);
self.dirty = true;
Ok(())
}
/// Suspends drawing while detached or hidden and cancels any active gesture.
///
/// Becoming visible requests a fresh frame without destroying the composition.
pub fn set_visible(&mut self, visible: bool) {
if self.visible == visible {
return;
}
self.visible = visible;
if visible {
self.shell.notify_app_resumed();
self.dirty = true;
} else {
self.shell.cancel_gesture();
self.shell.notify_app_paused();
}
}
/// Advances the component and writes tightly packed premultiplied RGBA pixels.
///
/// Returns false without touching `pixels` when hidden or visually unchanged.
/// Reuse the same vector to avoid allocating a new CPU buffer for every frame.
/// The native host presents at [Self::pixel_size] and then reconciles native
/// children using [crate::native_view::NativeViewHost::layout].
pub fn draw(&mut self, pixels: &mut Vec<u8>) -> Result<bool, EmbeddedViewError> {
if !self.visible {
return Ok(false);
}
let update = self.shell.update();
let owed = self.shell.take_frame_owed();
if !(self.dirty || owed || update.visual_changed || self.shell.needs_redraw()) {
return Ok(false);
}
let (width, height) = self.target.size();
self.shell
.renderer()
.render(self.target.texture(), self.target.view(), width, height)
.map_err(|error| EmbeddedViewError::Render(format!("{error:?}")))?;
self.target.read_into(&self.device, &self.queue, pixels)?;
self.dirty = false;
Ok(true)
}
/// Physical dimensions of the image produced by [Self::draw].
pub fn pixel_size(&self) -> (u32, u32) {
self.target.size()
}
}
fn validate_size(
width: u32,
height: u32,
density: f32,
limit: u32,
) -> Result<(), EmbeddedViewError> {
if width == 0
|| height == 0
|| width > limit
|| height > limit
|| u64::from(width) * u64::from(height) > 16 * 1024 * 1024
|| !density.is_finite()
|| density <= 0.0
{
return Err(EmbeddedViewError::InvalidSize {
width,
height,
density,
});
}
Ok(())
}