1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::rc::Rc;
4use std::sync::Arc;
5
6use egui_wgpu::{Renderer, RendererOptions, ScreenDescriptor};
7use loam_egui::egui;
8use loam_render::device::GpuContext;
9use loam_render::pass::{FrameFormat, FramePass, FrameTarget, PassStage};
10use wgpu::{CommandBuffer, CommandEncoder, Device, Queue, TextureFormat, TextureView};
11#[cfg(not(target_arch = "wasm32"))]
12use winit::event::WindowEvent;
13#[cfg(not(target_arch = "wasm32"))]
14use winit::window::Window;
15
16#[cfg(any(target_arch = "wasm32", test))]
17use super::input::TouchCapture;
18
19pub const DEBUG_LAYER: &str = "debug-layer";
20
21enum Feed {
22 #[cfg(not(target_arch = "wasm32"))]
23 Winit(Box<WinitInput>),
24 Raw(RawFeed),
25}
26
27#[cfg(not(target_arch = "wasm32"))]
28struct WinitInput {
29 state: egui_winit::State,
30}
31
32#[cfg(not(target_arch = "wasm32"))]
33impl WinitInput {
34 fn new(ctx: &egui::Context, window: &Window) -> Self {
35 Self {
36 state: egui_winit::State::new(
37 ctx.clone(),
38 egui::ViewportId::ROOT,
39 window,
40 Some(window.scale_factor() as f32),
41 window.theme(),
42 None,
43 ),
44 }
45 }
46
47 fn on_event(&mut self, window: &Window, event: &WindowEvent) -> egui_winit::EventResponse {
48 self.state.on_window_event(window, event)
49 }
50
51 fn take(&mut self, window: &Window) -> egui::RawInput {
52 self.state.take_egui_input(window)
53 }
54
55 fn handle_output(&mut self, window: &Window, output: egui::PlatformOutput) {
56 self.state.handle_platform_output(window, output);
57 }
58}
59
60struct RawFeed {
61 events: Vec<egui::Event>,
62 pending: Vec<egui::Event>,
63 modifiers: egui::Modifiers,
64 canceled: bool,
65 quiet: bool,
66 #[cfg(any(target_arch = "wasm32", test))]
67 reset: bool,
68}
69
70impl RawFeed {
71 fn push(&mut self, event: egui::Event) {
72 let delayed = matches!(
73 event,
74 egui::Event::PointerMoved(_)
75 | egui::Event::PointerButton { .. }
76 | egui::Event::PointerGone
77 | egui::Event::Touch { .. }
78 );
79 if self.canceled && delayed {
80 self.pending.push(event);
81 return;
82 }
83 let cancellation = matches!(event, egui::Event::WindowFocused(false));
84 self.events.push(event);
85 self.canceled |= cancellation;
86 }
87
88 #[cfg(any(target_arch = "wasm32", test))]
89 fn cancel_touch(&mut self, id: u64, pos: egui::Pos2, pointer: bool) {
90 self.push(egui::Event::Touch {
91 device_id: egui::TouchDeviceId(0),
92 id: egui::TouchId::from(id),
93 phase: egui::TouchPhase::Cancel,
94 pos,
95 force: None,
96 });
97 self.canceled |= pointer;
98 }
99
100 #[cfg(any(target_arch = "wasm32", test))]
101 fn cancel_pointer(&mut self) {
102 self.canceled = true;
103 }
104
105 fn take(&mut self) -> Vec<egui::Event> {
106 let current = std::mem::take(&mut self.events);
107 if self.quiet {
108 self.quiet = false;
109 self.canceled = false;
110 self.events.append(&mut self.pending);
111 } else if self.canceled {
112 self.quiet = true;
113 #[cfg(any(target_arch = "wasm32", test))]
114 {
115 self.reset = true;
116 }
117 }
118 current
119 }
120}
121
122#[cfg(any(target_arch = "wasm32", test))]
123fn reset_pointer(context: &egui::Context) {
124 context.input_mut(|input| input.pointer = egui::PointerState::default());
125 context.stop_dragging();
126 egui::DragAndDrop::clear_payload(context);
127}
128
129struct ManagedTexture {
130 image: egui::ImageData,
131 options: egui::TextureOptions,
132}
133
134struct Layer {
135 ctx: egui::Context,
136 renderer: Renderer,
137 device: Device,
138 queue: Queue,
139 format: TextureFormat,
140 feed: Feed,
141 primitives: Vec<egui::ClippedPrimitive>,
142 textures: egui::TexturesDelta,
143 screen: ScreenDescriptor,
144 native_pixels_per_point: f32,
145 callbacks: Vec<CommandBuffer>,
146 managed_textures: HashMap<egui::TextureId, ManagedTexture>,
147}
148
149fn renderer(device: &Device, format: TextureFormat) -> Renderer {
150 Renderer::new(
151 device,
152 format,
153 RendererOptions {
154 msaa_samples: 1,
155 ..Default::default()
156 },
157 )
158}
159
160impl Layer {
161 fn new(
162 gpu: &GpuContext,
163 format: TextureFormat,
164 ctx: egui::Context,
165 feed: Feed,
166 size: (u32, u32),
167 scale: f32,
168 ) -> Self {
169 Self {
170 ctx,
171 renderer: renderer(&gpu.device, format),
172 device: gpu.device.clone(),
173 queue: gpu.queue.clone(),
174 format,
175 feed,
176 primitives: Vec::new(),
177 textures: egui::TexturesDelta::default(),
178 screen: ScreenDescriptor {
179 size_in_pixels: [size.0.max(1), size.1.max(1)],
180 pixels_per_point: scale,
181 },
182 native_pixels_per_point: scale,
183 callbacks: Vec::new(),
184 managed_textures: HashMap::new(),
185 }
186 }
187
188 fn raw(&mut self) -> Option<&mut RawFeed> {
189 match &mut self.feed {
190 Feed::Raw(feed) => Some(feed),
191 #[cfg(not(target_arch = "wasm32"))]
192 Feed::Winit(_) => None,
193 }
194 }
195
196 fn raw_input(
197 &mut self,
198 #[cfg(not(target_arch = "wasm32"))] window: Option<&Window>,
199 ) -> egui::RawInput {
200 let points = self.native_pixels_per_point * self.ctx.zoom_factor();
201 let size = self.screen.size_in_pixels;
202 match &mut self.feed {
203 #[cfg(not(target_arch = "wasm32"))]
204 Feed::Winit(input) => match window {
205 Some(window) => input.take(window),
206 None => egui::RawInput::default(),
207 },
208 Feed::Raw(feed) => {
209 let mut raw = egui::RawInput {
210 screen_rect: Some(egui::Rect::from_min_size(
211 egui::Pos2::ZERO,
212 egui::vec2(size[0] as f32 / points, size[1] as f32 / points),
213 )),
214 events: feed.take(),
215 modifiers: feed.modifiers,
216 viewport_id: egui::ViewportId::ROOT,
217 ..egui::RawInput::default()
218 };
219 raw.viewports
220 .entry(egui::ViewportId::ROOT)
221 .or_default()
222 .native_pixels_per_point = Some(self.native_pixels_per_point);
223 raw
224 }
225 }
226 }
227
228 fn finish(&mut self, #[cfg(not(target_arch = "wasm32"))] window: Option<&Window>) {
229 let output = self.ctx.end_pass();
230 #[cfg(any(target_arch = "wasm32", test))]
231 if self
232 .raw()
233 .is_some_and(|feed| std::mem::take(&mut feed.reset))
234 {
235 reset_pointer(&self.ctx);
236 }
237 #[cfg(not(target_arch = "wasm32"))]
238 if let (Feed::Winit(input), Some(window)) = (&mut self.feed, window) {
239 input.handle_output(window, output.platform_output);
240 }
241 self.primitives.clear();
242 self.primitives
243 .extend(self.ctx.tessellate(output.shapes, output.pixels_per_point));
244 self.textures = output.textures_delta;
245 self.screen.pixels_per_point = output.pixels_per_point;
246 }
247
248 fn paint(&mut self, encoder: &mut CommandEncoder, target: &TextureView) -> anyhow::Result<()> {
249 for (id, delta) in &self.textures.set {
250 retain_managed_texture(&mut self.managed_textures, *id, delta)?;
251 self.renderer
252 .update_texture(&self.device, &self.queue, *id, delta);
253 }
254 self.callbacks.extend(self.renderer.update_buffers(
255 &self.device,
256 &self.queue,
257 encoder,
258 &self.primitives,
259 &self.screen,
260 ));
261 {
262 let pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
263 label: Some("loam-app::session::debug-layer"),
264 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
265 view: target,
266 depth_slice: None,
267 resolve_target: None,
268 ops: wgpu::Operations {
269 load: wgpu::LoadOp::Load,
270 store: wgpu::StoreOp::Store,
271 },
272 })],
273 depth_stencil_attachment: None,
274 timestamp_writes: None,
275 occlusion_query_set: None,
276 });
277 self.renderer
278 .render(&mut pass.forget_lifetime(), &self.primitives, &self.screen);
279 }
280 for id in &self.textures.free {
281 self.renderer.free_texture(id);
282 self.managed_textures.remove(id);
283 }
284 self.textures.set.clear();
285 self.textures.free.clear();
286 Ok(())
287 }
288}
289
290fn retain_managed_texture(
291 textures: &mut HashMap<egui::TextureId, ManagedTexture>,
292 id: egui::TextureId,
293 delta: &egui::epaint::ImageDelta,
294) -> anyhow::Result<()> {
295 if !matches!(id, egui::TextureId::Managed(_)) {
296 return Ok(());
297 }
298 let Some(pos) = delta.pos else {
299 textures.insert(
300 id,
301 ManagedTexture {
302 image: delta.image.clone(),
303 options: delta.options,
304 },
305 );
306 return Ok(());
307 };
308 let texture = textures
309 .get_mut(&id)
310 .ok_or_else(|| anyhow::anyhow!("managed texture {id:?} has no full image"))?;
311 let egui::ImageData::Color(image) = &mut texture.image;
312 let egui::ImageData::Color(patch) = &delta.image;
313 let end_x = pos[0]
314 .checked_add(patch.size[0])
315 .filter(|end| *end <= image.size[0]);
316 let end_y = pos[1]
317 .checked_add(patch.size[1])
318 .filter(|end| *end <= image.size[1]);
319 if end_x.is_none() || end_y.is_none() {
320 anyhow::bail!("managed texture {id:?} partial update is out of bounds");
321 }
322 let image = Arc::make_mut(image);
323 for row in 0..patch.size[1] {
324 let source = row * patch.size[0];
325 let target = (pos[1] + row) * image.size[0] + pos[0];
326 image.pixels[target..target + patch.size[0]]
327 .copy_from_slice(&patch.pixels[source..source + patch.size[0]]);
328 }
329 texture.options = delta.options;
330 Ok(())
331}
332
333#[derive(Clone)]
334pub struct DebugLayer {
335 shared: Rc<RefCell<Layer>>,
336 #[cfg(not(target_arch = "wasm32"))]
337 window: Option<Arc<Window>>,
338}
339
340impl DebugLayer {
341 #[cfg(not(target_arch = "wasm32"))]
342 pub fn on_window(
343 gpu: &GpuContext,
344 format: TextureFormat,
345 window: Arc<Window>,
346 size: (u32, u32),
347 ) -> Self {
348 let ctx = egui::Context::default();
349 let feed = Feed::Winit(Box::new(WinitInput::new(&ctx, window.as_ref())));
350 let scale = window.scale_factor() as f32;
351 let layer = Layer::new(gpu, format, ctx, feed, size, scale);
352 Self {
353 shared: Rc::new(RefCell::new(layer)),
354 window: Some(window),
355 }
356 }
357
358 pub fn offscreen(
359 gpu: &GpuContext,
360 format: TextureFormat,
361 size: (u32, u32),
362 scale: f32,
363 ) -> Self {
364 let feed = Feed::Raw(RawFeed {
365 events: Vec::new(),
366 pending: Vec::new(),
367 modifiers: egui::Modifiers::default(),
368 canceled: false,
369 quiet: false,
370 #[cfg(any(target_arch = "wasm32", test))]
371 reset: false,
372 });
373 let layer = Layer::new(gpu, format, egui::Context::default(), feed, size, scale);
374 Self {
375 shared: Rc::new(RefCell::new(layer)),
376 #[cfg(not(target_arch = "wasm32"))]
377 window: None,
378 }
379 }
380
381 pub fn pass(&self) -> Box<dyn FramePass> {
382 Box::new(LayerPass {
383 shared: self.shared.clone(),
384 })
385 }
386
387 pub fn context(&self) -> egui::Context {
388 self.shared.borrow().ctx.clone()
389 }
390
391 pub fn resize(&self, width: u32, height: u32, scale: f32) {
392 let mut layer = self.shared.borrow_mut();
393 layer.screen.size_in_pixels = [width.max(1), height.max(1)];
394 if scale.is_finite() && scale > 0.0 {
395 layer.native_pixels_per_point = scale;
396 }
397 }
398
399 #[cfg(not(target_arch = "wasm32"))]
400 pub fn on_window_event(&self, event: &WindowEvent) -> bool {
401 let Some(window) = self.window.clone() else {
402 return false;
403 };
404 let mut layer = self.shared.borrow_mut();
405 let Feed::Winit(input) = &mut layer.feed else {
406 return false;
407 };
408 input.on_event(window.as_ref(), event).consumed
409 }
410
411 pub fn push(&self, event: egui::Event) {
412 if let Some(feed) = self.shared.borrow_mut().raw() {
413 feed.push(event);
414 }
415 }
416
417 #[cfg(any(target_arch = "wasm32", test))]
418 pub(super) fn cancel_touch(&self, id: u64, pos: egui::Pos2, pointer: bool) {
419 if let Some(feed) = self.shared.borrow_mut().raw() {
420 feed.cancel_touch(id, pos, pointer);
421 }
422 }
423
424 #[cfg(any(target_arch = "wasm32", test))]
425 pub(super) fn cancel_touches(&self, touches: &mut TouchCapture) {
426 for (id, pos) in touches.cancel_all() {
427 self.cancel_touch(id, egui::pos2(pos[0], pos[1]), false);
428 }
429 self.cancel_pointer();
430 }
431
432 #[cfg(any(target_arch = "wasm32", test))]
433 fn cancel_pointer(&self) {
434 if let Some(feed) = self.shared.borrow_mut().raw() {
435 feed.cancel_pointer();
436 }
437 }
438
439 pub fn modifiers(&self) -> egui::Modifiers {
440 match &self.shared.borrow().feed {
441 Feed::Raw(feed) => feed.modifiers,
442 #[cfg(not(target_arch = "wasm32"))]
443 Feed::Winit(_) => egui::Modifiers::default(),
444 }
445 }
446
447 pub fn set_modifiers(&self, next: egui::Modifiers) {
448 if let Some(feed) = self.shared.borrow_mut().raw() {
449 feed.modifiers = next;
450 }
451 }
452
453 pub fn begin(&self) -> egui::Context {
454 let mut layer = self.shared.borrow_mut();
455 let raw = layer.raw_input(
456 #[cfg(not(target_arch = "wasm32"))]
457 self.window.as_deref(),
458 );
459 layer.ctx.begin_pass(raw);
460 layer.ctx.clone()
461 }
462
463 pub fn finish(&self) {
464 #[cfg(not(target_arch = "wasm32"))]
465 let window = self.window.clone();
466 self.shared.borrow_mut().finish(
467 #[cfg(not(target_arch = "wasm32"))]
468 window.as_deref(),
469 );
470 }
471
472 pub fn take_callbacks(&self, into: &mut Vec<CommandBuffer>) {
473 into.append(&mut self.shared.borrow_mut().callbacks);
474 }
475}
476
477struct LayerPass {
478 shared: Rc<RefCell<Layer>>,
479}
480
481impl FramePass for LayerPass {
482 fn name(&self) -> &'static str {
483 DEBUG_LAYER
484 }
485
486 fn stage(&self) -> PassStage {
487 PassStage::Overlay
488 }
489
490 fn record(
491 &mut self,
492 encoder: &mut CommandEncoder,
493 target: &FrameTarget<'_>,
494 ) -> anyhow::Result<()> {
495 self.shared.borrow_mut().paint(encoder, target.color)
496 }
497
498 fn attach(&mut self, gpu: &GpuContext, frame: FrameFormat) -> anyhow::Result<()> {
499 let mut layer = self.shared.borrow_mut();
500 layer.device = gpu.device.clone();
501 layer.queue = gpu.queue.clone();
502 layer.format = frame.color;
503 layer.renderer = renderer(&gpu.device, frame.color);
504 let Layer {
505 renderer,
506 managed_textures,
507 ..
508 } = &mut *layer;
509 for (id, texture) in managed_textures {
510 renderer.update_texture(
511 &gpu.device,
512 &gpu.queue,
513 *id,
514 &egui::epaint::ImageDelta::full(texture.image.clone(), texture.options),
515 );
516 }
517 Ok(())
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use loam_render::device::FeatureRequest;
524 use wgpu::{
525 BackendOptions, Backends, Extent3d, Instance, InstanceDescriptor, NoopBackendOptions,
526 TextureDescriptor, TextureDimension, TextureUsages, TextureViewDescriptor,
527 };
528
529 use crate::wasm::input_queue::PointerPhase;
530
531 use super::*;
532
533 fn noop_gpu() -> GpuContext {
534 let instance = Instance::new(&InstanceDescriptor {
535 backends: Backends::NOOP,
536 backend_options: BackendOptions {
537 noop: NoopBackendOptions { enable: true },
538 ..Default::default()
539 },
540 ..Default::default()
541 });
542 pollster::block_on(GpuContext::new(instance, FeatureRequest::default(), None))
543 .expect("the noop backend always yields a context")
544 }
545
546 #[test]
547 fn browser_zoom_does_not_feed_output_scale_back_as_native_dpr() {
548 let gpu = noop_gpu();
549 let layer = DebugLayer::offscreen(&gpu, TextureFormat::Rgba8UnormSrgb, (600, 300), 2.0);
550 let mut layer = layer.shared.borrow_mut();
551 layer.ctx.set_zoom_factor(1.5);
552
553 let first = layer.raw_input(None);
554 layer.ctx.begin_pass(first);
555 layer.finish(None);
556 let second = layer.raw_input(None);
557
558 assert_eq!(layer.native_pixels_per_point, 2.0);
559 assert_eq!(layer.screen.pixels_per_point, 3.0);
560 assert_eq!(
561 second.screen_rect.map(|rect| rect.size()),
562 Some(egui::vec2(200.0, 100.0))
563 );
564 }
565
566 #[test]
567 fn managed_partial_texture_after_recovery_uses_replay_and_free_removes_it() {
568 let gpu = noop_gpu();
569 let format = TextureFormat::Rgba8UnormSrgb;
570 let layer = DebugLayer::offscreen(&gpu, format, (16, 16), 1.0);
571 let target = gpu.device.create_texture(&TextureDescriptor {
572 label: Some("debug layer texture recovery probe"),
573 size: Extent3d {
574 width: 16,
575 height: 16,
576 depth_or_array_layers: 1,
577 },
578 mip_level_count: 1,
579 sample_count: 1,
580 dimension: TextureDimension::D2,
581 format,
582 usage: TextureUsages::RENDER_ATTACHMENT,
583 view_formats: &[],
584 });
585 let target = target.create_view(&TextureViewDescriptor::default());
586 let paint = || {
587 let mut encoder = gpu
588 .device
589 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
590 layer
591 .shared
592 .borrow_mut()
593 .paint(&mut encoder, &target)
594 .expect("texture deltas applied");
595 gpu.queue.submit(Some(encoder.finish()));
596 };
597 let red = egui::Color32::from_rgb(255, 0, 0);
598 let blue = egui::Color32::from_rgb(0, 0, 255);
599 let mut texture = layer.context().load_texture(
600 "managed recovery probe",
601 egui::ColorImage::filled([2, 2], red),
602 egui::TextureOptions::LINEAR,
603 );
604 let id = texture.id();
605 layer.begin();
606 layer.finish();
607 paint();
608
609 let frame = FrameFormat {
610 color: format,
611 depth: TextureFormat::Depth32Float,
612 };
613 let mut pass = layer.pass();
614 pass.attach(&gpu, frame).expect("renderer recreated");
615 texture.set_partial(
616 [1, 0],
617 egui::ColorImage::filled([1, 2], blue),
618 egui::TextureOptions::NEAREST,
619 );
620 layer.begin();
621 layer.finish();
622 paint();
623
624 {
625 let layer = layer.shared.borrow();
626 let cached = layer.managed_textures.get(&id).expect("texture retained");
627 let egui::ImageData::Color(image) = &cached.image;
628 assert_eq!(image.pixels, [red, blue, red, blue]);
629 assert_eq!(cached.options, egui::TextureOptions::NEAREST);
630 }
631
632 drop(texture);
633 layer.begin();
634 layer.finish();
635 paint();
636 pass.attach(&gpu, frame)
637 .expect("renderer recreated after free");
638 assert!(!layer.shared.borrow().managed_textures.contains_key(&id));
639 }
640 #[test]
641 fn canceled_touch_never_clicks_and_later_input_lands_in_the_next_pass() {
642 struct Observed {
643 a: egui::Rect,
644 b: egui::Rect,
645 c: egui::Rect,
646 clicked_a: bool,
647 clicked_b: bool,
648 clicked_c: bool,
649 released: usize,
650 any_released: bool,
651 down: bool,
652 touches: bool,
653 payload: bool,
654 }
655
656 fn observe(layer: &DebugLayer) -> Observed {
657 let context = layer.begin();
658 let mut a = egui::Rect::NOTHING;
659 let mut b = egui::Rect::NOTHING;
660 let mut c = egui::Rect::NOTHING;
661 let mut clicked_a = false;
662 let mut clicked_b = false;
663 let mut clicked_c = false;
664 egui::CentralPanel::default().show(&context, |ui| {
665 let (_, response) =
666 ui.allocate_exact_size(egui::vec2(120.0, 48.0), egui::Sense::click_and_drag());
667 a = response.rect;
668 clicked_a = response.clicked();
669 if response.is_pointer_button_down_on() {
670 context.set_dragged_id(response.id);
671 egui::DragAndDrop::set_payload(&context, 7_u32);
672 }
673 let (_, response) =
674 ui.allocate_exact_size(egui::vec2(120.0, 48.0), egui::Sense::click());
675 b = response.rect;
676 clicked_b = response.clicked();
677 let (_, response) =
678 ui.allocate_exact_size(egui::vec2(120.0, 48.0), egui::Sense::click());
679 c = response.rect;
680 clicked_c = response.clicked();
681 });
682 let (released, any_released, down, touches) = context.input(|input| {
683 let released = input
684 .raw
685 .events
686 .iter()
687 .filter(|event| {
688 matches!(event, egui::Event::PointerButton { pressed: false, .. })
689 })
690 .count();
691 (
692 released,
693 input.pointer.any_released(),
694 input.pointer.any_down(),
695 input.any_touches(),
696 )
697 });
698 let payload = egui::DragAndDrop::has_any_payload(&context);
699 layer.finish();
700 Observed {
701 a,
702 b,
703 c,
704 clicked_a,
705 clicked_b,
706 clicked_c,
707 released,
708 any_released,
709 down,
710 touches,
711 payload,
712 }
713 }
714
715 fn down(layer: &DebugLayer, touches: &mut TouchCapture, id: u64, pos: egui::Pos2) {
716 assert!(touches.route(id, PointerPhase::Down, true, [pos.x, pos.y]));
717 layer.push(egui::Event::Touch {
718 device_id: egui::TouchDeviceId(0),
719 id: egui::TouchId::from(id),
720 phase: egui::TouchPhase::Start,
721 pos,
722 force: None,
723 });
724 layer.push(egui::Event::PointerMoved(pos));
725 layer.push(egui::Event::PointerButton {
726 pos,
727 button: egui::PointerButton::Primary,
728 pressed: true,
729 modifiers: egui::Modifiers::default(),
730 });
731 }
732
733 fn cancel(layer: &DebugLayer, touches: &mut TouchCapture, id: u64, pos: egui::Pos2) {
734 let pointer = touches.is_pointer(id);
735 assert!(touches.route(id, PointerPhase::Cancel, false, [pos.x, pos.y]));
736 layer.cancel_touch(id, pos, pointer);
737 }
738
739 fn up(layer: &DebugLayer, touches: &mut TouchCapture, id: u64, pos: egui::Pos2) {
740 let pointer = touches.is_pointer(id);
741 assert!(touches.route(id, PointerPhase::Up, false, [pos.x, pos.y]));
742 layer.push(egui::Event::Touch {
743 device_id: egui::TouchDeviceId(0),
744 id: egui::TouchId::from(id),
745 phase: egui::TouchPhase::End,
746 pos,
747 force: None,
748 });
749 if pointer {
750 layer.push(egui::Event::PointerButton {
751 pos,
752 button: egui::PointerButton::Primary,
753 pressed: false,
754 modifiers: egui::Modifiers::default(),
755 });
756 layer.push(egui::Event::PointerGone);
757 }
758 }
759
760 let gpu = noop_gpu();
761 let layer = DebugLayer::offscreen(&gpu, TextureFormat::Rgba8UnormSrgb, (256, 192), 1.0);
762 let mut touches = TouchCapture::default();
763 let initial = observe(&layer);
764
765 down(&layer, &mut touches, 1, initial.a.center());
766 let held_a = observe(&layer);
767 assert!(!held_a.clicked_a && !held_a.clicked_b && !held_a.clicked_c);
768 assert!(held_a.down && held_a.touches && held_a.payload);
769
770 layer.cancel_touches(&mut touches);
771 down(&layer, &mut touches, 2, initial.c.center());
772 cancel(&layer, &mut touches, 2, initial.c.center());
773 down(&layer, &mut touches, 3, initial.b.center());
774 up(&layer, &mut touches, 3, initial.b.center());
775
776 let canceled = observe(&layer);
777 assert!(!canceled.clicked_a && !canceled.clicked_b && !canceled.clicked_c);
778 assert_eq!(canceled.released, 0);
779 assert!(!canceled.any_released && !canceled.touches);
780
781 let quiet = observe(&layer);
782 assert!(!quiet.clicked_a && !quiet.clicked_b && !quiet.clicked_c);
783 assert!(!quiet.down && !quiet.touches);
784
785 let valid_b = observe(&layer);
786 assert!(!valid_b.clicked_a && valid_b.clicked_b && !valid_b.clicked_c);
787 assert_eq!(valid_b.released, 1);
788 assert!(valid_b.any_released);
789 assert!(!valid_b.down && !valid_b.touches && !valid_b.payload);
790
791 down(&layer, &mut touches, 4, initial.a.center());
792 let held_a = observe(&layer);
793 assert!(held_a.down && held_a.touches && held_a.payload);
794
795 up(&layer, &mut touches, 4, initial.a.center());
796 down(&layer, &mut touches, 5, initial.c.center());
797 cancel(&layer, &mut touches, 5, initial.c.center());
798 down(&layer, &mut touches, 6, initial.b.center());
799 up(&layer, &mut touches, 6, initial.b.center());
800
801 let valid_a = observe(&layer);
802 assert!(valid_a.clicked_a && !valid_a.clicked_b && !valid_a.clicked_c);
803 assert_eq!(valid_a.released, 1);
804 assert!(valid_a.any_released && !valid_a.touches);
805
806 let quiet = observe(&layer);
807 assert!(!quiet.clicked_a && !quiet.clicked_b && !quiet.clicked_c);
808 assert!(!quiet.down && !quiet.touches);
809
810 let valid_b = observe(&layer);
811 assert!(!valid_b.clicked_a && valid_b.clicked_b && !valid_b.clicked_c);
812 assert_eq!(valid_b.released, 1);
813 assert!(!valid_b.down && !valid_b.touches && !valid_b.payload);
814 assert!(!touches.is_pointer(6));
815 assert_eq!(touches.cancel_all().count(), 0);
816 }
817}