1use std::error::Error;
2
3use operad::platform::PixelSize;
4use operad::{
5 layout, process_document_frame, root_style, AccessibilityMeta, AccessibilityRole,
6 ApproxTextMeasurer, ColorRgba, HostDocumentFrameOutput, HostDocumentFrameRequest,
7 HostFrameOutput, HostInteractionState, InputBehavior, RenderTarget, StrokeStyle, TextStyle,
8 UiDocument, UiNode, UiSize, UiVisual,
9};
10
11#[cfg(all(feature = "accesskit-winit", feature = "native-window"))]
12use operad::{AccessKitTreeOptions, AccessKitWinitAdapter};
13
14#[cfg(feature = "wgpu")]
15use operad::{EmptyResourceResolver, RendererAdapter, WgpuRenderer};
16
17#[cfg(all(feature = "wgpu", feature = "native-window"))]
18use {
19 operad::WgpuSurfaceRenderer,
20 std::{sync::Arc, time::Duration},
21 winit::{
22 application::ApplicationHandler,
23 dpi::PhysicalSize,
24 event::WindowEvent,
25 event_loop::{ActiveEventLoop, EventLoop},
26 window::{Window, WindowId},
27 },
28};
29
30fn main() -> Result<(), Box<dyn Error>> {
31 #[cfg(all(feature = "wgpu", feature = "native-window"))]
32 if std::env::var_os("OPERAD_RUN_WGPU_EXAMPLE_WINDOW").is_some() {
33 return run_windowed_wgpu_example();
34 }
35
36 let viewport = UiSize::new(640.0, 360.0);
37 let frame = sample_frame(viewport, RenderTarget::offscreen(PixelSize::new(640, 360)))?;
38
39 println!(
40 "native_wgpu_host: {} paint items, {} accessibility nodes",
41 frame.render_request.paint.items.len(),
42 frame.accessibility_tree.nodes.len()
43 );
44
45 #[cfg(feature = "wgpu")]
46 if std::env::var_os("OPERAD_RUN_WGPU_EXAMPLE").is_some() {
47 let mut renderer = WgpuRenderer::new();
48 let output = renderer.render_frame(frame.render_request, &EmptyResourceResolver)?;
49 println!(
50 "native_wgpu_host: rendered {} items into {:?}",
51 output.painted_items, output.target
52 );
53 }
54
55 Ok(())
56}
57
58fn sample_frame(
59 viewport: UiSize,
60 target: RenderTarget,
61) -> Result<HostDocumentFrameOutput, Box<dyn Error>> {
62 let mut document = build_document();
63 let mut measurer = ApproxTextMeasurer;
64 let host_output = HostFrameOutput::new(HostInteractionState::default());
65 Ok(process_document_frame(
66 &mut document,
67 &mut measurer,
68 HostDocumentFrameRequest::new(viewport, target, host_output),
69 )?)
70}
71
72#[cfg(all(feature = "wgpu", feature = "native-window"))]
73fn run_windowed_wgpu_example() -> Result<(), Box<dyn Error>> {
74 let event_loop = EventLoop::new()?;
75 let mut app = NativeWindowApp::new(window_frame_limit()?);
76 event_loop.run_app(&mut app)?;
77 if let Some(error) = app.error {
78 Err(error.into())
79 } else {
80 if app.presented_frames > 0 {
81 println!(
82 "native_wgpu_host: native surface presented {} frame(s), p95 render {:?}",
83 app.presented_frames,
84 percentile_duration(&app.render_samples, 95.0).unwrap_or_default()
85 );
86 }
87 Ok(())
88 }
89}
90
91#[cfg(all(feature = "wgpu", feature = "native-window"))]
92struct NativeWindowApp {
93 window: Option<Arc<Window>>,
94 window_id: Option<WindowId>,
95 renderer: Option<WgpuSurfaceRenderer<'static>>,
96 #[cfg(feature = "accesskit-winit")]
97 accesskit: Option<AccessKitWinitAdapter>,
98 error: Option<String>,
99 frame_limit: Option<usize>,
100 presented_frames: usize,
101 render_samples: Vec<Duration>,
102}
103
104#[cfg(all(feature = "wgpu", feature = "native-window"))]
105impl NativeWindowApp {
106 fn new(frame_limit: Option<usize>) -> Self {
107 Self {
108 window: None,
109 window_id: None,
110 renderer: None,
111 #[cfg(feature = "accesskit-winit")]
112 accesskit: None,
113 error: None,
114 frame_limit,
115 presented_frames: 0,
116 render_samples: Vec::new(),
117 }
118 }
119
120 fn init_window(&mut self, event_loop: &ActiveEventLoop) -> Result<(), Box<dyn Error>> {
121 let window = Arc::new(
122 event_loop.create_window(
123 Window::default_attributes()
124 .with_title("Operad native WGPU host")
125 .with_inner_size(PhysicalSize::new(640, 360))
126 .with_visible(false),
127 )?,
128 );
129 let size = nonzero_window_size(window.inner_size());
130
131 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
132 let surface = instance.create_surface(window.clone())?;
133 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
134 compatible_surface: Some(&surface),
135 power_preference: wgpu::PowerPreference::default(),
136 force_fallback_adapter: false,
137 }))?;
138 let (device, queue) =
139 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
140 label: Some("native-wgpu-host-device"),
141 required_features: wgpu::Features::empty(),
142 required_limits: wgpu::Limits::default(),
143 experimental_features: wgpu::ExperimentalFeatures::disabled(),
144 memory_hints: wgpu::MemoryHints::Performance,
145 trace: wgpu::Trace::Off,
146 }))?;
147 let surface_config = surface
148 .get_default_config(&adapter, size.width, size.height)
149 .ok_or("adapter does not support the native window surface")?;
150
151 self.window_id = Some(window.id());
152 self.renderer = Some(WgpuSurfaceRenderer::new(
153 surface,
154 device,
155 queue,
156 surface_config,
157 )?);
158 #[cfg(feature = "accesskit-winit")]
159 {
160 let viewport = UiSize::new(size.width as f32, size.height as f32);
161 let frame = sample_frame(viewport, RenderTarget::window("native-wgpu-host", viewport))?;
162 self.accesskit = Some(AccessKitWinitAdapter::new(
163 event_loop,
164 &window,
165 frame.accessibility_tree,
166 None,
167 AccessKitTreeOptions::default(),
168 ));
169 }
170 window.set_visible(true);
171 self.window = Some(window);
172 Ok(())
173 }
174
175 fn render(&mut self) -> Result<bool, Box<dyn Error>> {
176 let Some(window) = self.window.as_ref() else {
177 return Ok(false);
178 };
179 let Some(renderer) = self.renderer.as_mut() else {
180 return Ok(false);
181 };
182
183 let size = window.inner_size();
184 if size.width == 0 || size.height == 0 {
185 return Ok(false);
186 }
187 let viewport = UiSize::new(size.width as f32, size.height as f32);
188 let frame = sample_frame(viewport, RenderTarget::window("native-wgpu-host", viewport))?;
189 #[cfg(feature = "accesskit-winit")]
190 if let Some(accesskit) = self.accesskit.as_mut() {
191 accesskit.publish_tree(frame.accessibility_tree.clone(), None);
192 }
193 let output = renderer.render_frame(frame.render_request, &EmptyResourceResolver)?;
194 if output.snapshot.is_some() {
195 return Err("native surface smoke must present without snapshot readback".into());
196 }
197 if let Some(duration) = output.timings.duration("render") {
198 self.render_samples.push(duration);
199 }
200 self.presented_frames += 1;
201 Ok(self
202 .frame_limit
203 .is_some_and(|frame_limit| self.presented_frames >= frame_limit))
204 }
205
206 fn fail_and_exit(&mut self, event_loop: &ActiveEventLoop, error: impl ToString) {
207 self.error = Some(error.to_string());
208 event_loop.exit();
209 }
210}
211
212#[cfg(all(feature = "wgpu", feature = "native-window"))]
213impl ApplicationHandler for NativeWindowApp {
214 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
215 if self.window.is_some() {
216 return;
217 }
218 if let Err(error) = self.init_window(event_loop) {
219 self.fail_and_exit(event_loop, error);
220 return;
221 }
222 if let Some(window) = self.window.as_ref() {
223 window.request_redraw();
224 }
225 }
226
227 fn window_event(
228 &mut self,
229 event_loop: &ActiveEventLoop,
230 window_id: WindowId,
231 event: WindowEvent,
232 ) {
233 if Some(window_id) != self.window_id {
234 return;
235 }
236
237 #[cfg(feature = "accesskit-winit")]
238 if let (Some(window), Some(accesskit)) = (self.window.as_ref(), self.accesskit.as_mut()) {
239 accesskit.process_event(window, &event);
240 }
241
242 match event {
243 WindowEvent::CloseRequested | WindowEvent::Destroyed => event_loop.exit(),
244 WindowEvent::Resized(size) => {
245 if size.width > 0 && size.height > 0 {
246 if let Some(window) = self.window.as_ref() {
247 window.request_redraw();
248 }
249 }
250 }
251 WindowEvent::RedrawRequested => match self.render() {
252 Ok(true) => event_loop.exit(),
253 Ok(false) => {
254 if self.frame_limit.is_some() {
255 if let Some(window) = self.window.as_ref() {
256 window.request_redraw();
257 }
258 }
259 }
260 Err(error) => self.fail_and_exit(event_loop, error),
261 },
262 _ => {}
263 }
264 }
265}
266
267#[cfg(all(feature = "wgpu", feature = "native-window"))]
268fn nonzero_window_size(size: PhysicalSize<u32>) -> PhysicalSize<u32> {
269 PhysicalSize::new(size.width.max(1), size.height.max(1))
270}
271
272#[cfg(all(feature = "wgpu", feature = "native-window"))]
273fn window_frame_limit() -> Result<Option<usize>, Box<dyn Error>> {
274 let Some(value) = std::env::var_os("OPERAD_WGPU_EXAMPLE_WINDOW_FRAMES") else {
275 return Ok(None);
276 };
277 let frames = value
278 .to_string_lossy()
279 .parse::<usize>()
280 .map_err(|error| format!("invalid OPERAD_WGPU_EXAMPLE_WINDOW_FRAMES: {error}"))?;
281 if frames == 0 {
282 return Err("OPERAD_WGPU_EXAMPLE_WINDOW_FRAMES must be greater than zero".into());
283 }
284 Ok(Some(frames))
285}
286
287#[cfg(all(feature = "wgpu", feature = "native-window"))]
288fn percentile_duration(samples: &[Duration], percentile: f64) -> Option<Duration> {
289 if samples.is_empty() {
290 return None;
291 }
292 let mut sorted = samples.to_vec();
293 sorted.sort_unstable();
294 let clamped = percentile.clamp(0.0, 100.0);
295 let index = ((clamped / 100.0) * (sorted.len().saturating_sub(1) as f64)).ceil() as usize;
296 sorted.get(index).copied()
297}
298
299fn build_document() -> UiDocument {
300 let mut document = UiDocument::new(root_style(640.0, 360.0));
301 let panel = document.add_child(
302 document.root,
303 UiNode::container(
304 "native.panel",
305 layout::node_style(layout::with_margin_all(
306 layout::with_size(layout::column(), layout::px(280.0), layout::px(340.0)),
307 24.0,
308 )),
309 )
310 .with_visual(UiVisual::panel(
311 ColorRgba::new(24, 29, 36, 255),
312 Some(StrokeStyle::new(ColorRgba::new(91, 110, 132, 255), 1.0)),
313 6.0,
314 )),
315 );
316
317 document.add_child(
318 panel,
319 UiNode::text(
320 "native.title",
321 "Operad native WGPU host",
322 TextStyle {
323 font_size: 18.0,
324 line_height: 24.0,
325 color: ColorRgba::WHITE,
326 ..TextStyle::default()
327 },
328 layout::size(layout::percent(1.0), layout::px(32.0)),
329 ),
330 );
331
332 for (index, label) in ["Play", "Select"].into_iter().enumerate() {
333 document.add_child(
334 panel,
335 UiNode::text(
336 format!("native.button.{index}"),
337 label,
338 TextStyle::default(),
339 layout::size(layout::percent(1.0), layout::px(34.0)),
340 )
341 .with_input(InputBehavior::BUTTON)
342 .with_visual(UiVisual::panel(
343 ColorRgba::new(42, 51, 63, 255),
344 Some(StrokeStyle::new(ColorRgba::new(112, 135, 162, 255), 1.0)),
345 4.0,
346 ))
347 .with_accessibility(
348 AccessibilityMeta::new(AccessibilityRole::Button)
349 .label(label)
350 .focusable(),
351 ),
352 );
353 }
354
355 let text_input = document.add_child(
356 panel,
357 UiNode::container(
358 "native.text_input",
359 layout::node_style(layout::with_size(
360 layout::row(),
361 layout::percent(1.0),
362 layout::px(34.0),
363 )),
364 )
365 .with_input(InputBehavior::BUTTON)
366 .with_visual(UiVisual::panel(
367 ColorRgba::new(18, 22, 28, 255),
368 Some(StrokeStyle::new(ColorRgba::new(72, 84, 104, 255), 1.0)),
369 4.0,
370 ))
371 .with_accessibility(
372 AccessibilityMeta::new(AccessibilityRole::TextBox)
373 .label("Filter")
374 .value("filter clips")
375 .focusable(),
376 ),
377 );
378 document.add_child(
379 text_input,
380 UiNode::text(
381 "native.text_input.value",
382 "filter clips",
383 TextStyle::default(),
384 layout::size(layout::percent(1.0), layout::px(30.0)),
385 ),
386 );
387
388 let menu = document.add_child(
389 panel,
390 UiNode::container(
391 "native.popup.menu",
392 layout::node_style(layout::with_size(
393 layout::column(),
394 layout::percent(1.0),
395 layout::px(68.0),
396 )),
397 )
398 .with_visual(UiVisual::panel(
399 ColorRgba::new(31, 38, 48, 255),
400 Some(StrokeStyle::new(ColorRgba::new(112, 135, 162, 255), 1.0)),
401 4.0,
402 ))
403 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Menu).label("Mode menu")),
404 );
405 for (index, label) in ["Auto", "Manual"].into_iter().enumerate() {
406 document.add_child(
407 menu,
408 UiNode::text(
409 format!("native.popup.item.{index}"),
410 label,
411 TextStyle::default(),
412 layout::size(layout::percent(1.0), layout::px(30.0)),
413 )
414 .with_input(InputBehavior::BUTTON)
415 .with_accessibility(
416 AccessibilityMeta::new(AccessibilityRole::MenuItem)
417 .label(label)
418 .focusable(),
419 ),
420 );
421 }
422
423 document.add_child(
424 panel,
425 UiNode::text(
426 "native.drag.handle",
427 "Drag handle",
428 TextStyle::default(),
429 layout::size(layout::percent(1.0), layout::px(28.0)),
430 )
431 .with_input(InputBehavior::BUTTON)
432 .with_accessibility(
433 AccessibilityMeta::new(AccessibilityRole::Slider)
434 .label("Drag handle")
435 .focusable(),
436 ),
437 );
438
439 document.add_child(
440 panel,
441 UiNode::canvas(
442 "native.canvas.viewport",
443 "native.canvas.viewport",
444 layout::size(layout::percent(1.0), layout::px(52.0)),
445 )
446 .with_visual(UiVisual::panel(
447 ColorRgba::new(18, 22, 28, 255),
448 Some(StrokeStyle::new(ColorRgba::new(72, 84, 104, 255), 1.0)),
449 4.0,
450 ))
451 .with_accessibility(
452 AccessibilityMeta::new(AccessibilityRole::Group)
453 .label("Canvas viewport")
454 .focusable(),
455 ),
456 );
457
458 document
459}