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