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
use cranpose_app_shell::{AppShell, default_root_key};
use cranpose_render_wgpu::{WgpuRenderer, WgpuTextSystem};
use crate::{AppSettings, GpuContext};
/// Failure to size or draw a [`GuestView`].
#[derive(Debug, thiserror::Error)]
pub enum GuestViewError {
/// Dimensions must be nonzero and fit the device's texture limit, and the
/// density must be finite and positive.
#[error("invalid guest view 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,
},
/// Cranpose could not render the scene.
#[error("render: {0}")]
Render(String),
}
/// A Cranpose composition drawn into textures on another program's GPU
/// device, such as a game engine's HUD texture.
///
/// It starts no window or event loop. The host routes input through
/// [`Self::shell`] in logical coordinates, calls [`Self::draw`] once per host
/// frame and shows the texture it drew into. The frame is submitted to the
/// context's queue, so work the host submits there afterwards sees it.
pub struct GuestView {
shell: AppShell<WgpuRenderer>,
max_dimension: u32,
size: (u32, u32),
density: f32,
dirty: bool,
visible: bool,
}
impl GuestView {
/// Creates a composition that draws `width` by `height` pixel textures of
/// `format` at `density` pixels per logical point.
pub fn new(
settings: &AppSettings,
gpu: &GpuContext,
format: wgpu::TextureFormat,
width: u32,
height: u32,
density: f32,
content: impl FnMut() + 'static,
) -> Result<Self, GuestViewError> {
let max_dimension = gpu.device.limits().max_texture_dimension_2d;
validate_size(width, height, density, max_dimension)?;
let mut renderer = WgpuRenderer::with_text_system(WgpuTextSystem::from_font_set(
settings.resolve_font_set(),
));
renderer.init_gpu(
gpu.device.clone(),
gpu.queue.clone(),
format,
gpu.adapter.get_info().backend,
gpu.adapter.get_downlevel_capabilities().flags,
);
let shell = AppShell::new_with_size_and_density(
renderer,
default_root_key(),
crate::gpu_context::provide_gpu_context(gpu.clone(), content),
(width, height),
(width as f32 / density, height as f32 / density),
density,
);
Ok(Self {
shell,
max_dimension,
size: (width, height),
density,
dirty: true,
visible: true,
})
}
/// Accesses input, scheduling, semantics, text input and the app context.
pub fn shell(&mut self) -> &mut AppShell<WgpuRenderer> {
&mut self.shell
}
/// Sets the size of the textures [`Self::draw`] draws into. Returns
/// whether the pixel size changed. Invalid values leave the previous size.
pub fn resize(
&mut self,
width: u32,
height: u32,
density: f32,
) -> Result<bool, GuestViewError> {
validate_size(width, height, density, self.max_dimension)?;
let size_changed = self.size != (width, height);
if !size_changed && self.density == density {
return Ok(false);
}
self.shell
.primary()
.set_viewport(width as f32 / density, height as f32 / density);
self.shell.set_density(density);
self.shell.set_buffer_size(width, height);
self.size = (width, height);
self.density = density;
self.dirty = true;
Ok(size_changed)
}
/// Suspends drawing while hidden and cancels any active gesture.
/// Becoming visible draws the next frame whatever changed.
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 composition and, when it looks different, draws it into
/// `texture` through `view`. The texture must have
/// [`pixel_size`](Self::pixel_size) and the format the view was created
/// with. Returns whether it drew.
pub fn draw(
&mut self,
texture: &wgpu::Texture,
view: &wgpu::TextureView,
) -> Result<bool, GuestViewError> {
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.size;
self.shell
.renderer()
.render(texture, view, width, height)
.map_err(|error| GuestViewError::Render(format!("{error:?}")))?;
self.dirty = false;
Ok(true)
}
/// Physical dimensions of the textures [`Self::draw`] draws into.
pub fn pixel_size(&self) -> (u32, u32) {
self.size
}
}
fn validate_size(width: u32, height: u32, density: f32, limit: u32) -> Result<(), GuestViewError> {
if width == 0
|| height == 0
|| width > limit
|| height > limit
|| !density.is_finite()
|| density <= 0.0
{
return Err(GuestViewError::InvalidSize {
width,
height,
density,
});
}
Ok(())
}