1use std::{
42 sync::Arc,
43 time::{Duration, Instant},
44};
45
46use aetna_core::widgets::text_input::{self, ClipboardKind};
47use aetna_core::{
48 App, Cursor, FrameTrigger, HostDiagnostics, KeyModifiers, Pointer, PointerButton, Rect, Sides,
49 UiEvent, UiEventKind, UiKey, clipboard,
50};
51use aetna_wgpu::{MsaaTarget, Runner};
52
53const DEFAULT_SAMPLE_COUNT: u32 = 4;
54#[cfg(not(any(target_os = "android", target_os = "ios")))]
55type PlatformClipboard = Option<arboard::Clipboard>;
56#[cfg(target_os = "android")]
57struct PlatformClipboard {
58 app: AndroidApp,
59}
60#[cfg(target_os = "ios")]
61#[derive(Default)]
62struct PlatformClipboard;
63
64use winit::application::ApplicationHandler;
65use winit::dpi::PhysicalSize;
66use winit::event::{ElementState, Force, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent};
67use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
68use winit::keyboard::{Key, NamedKey};
69#[cfg(target_os = "android")]
70use winit::platform::android::{EventLoopExtAndroid, WindowExtAndroid, activity::AndroidApp};
71use winit::window::{CursorIcon, Window, WindowId};
72
73#[derive(Clone, Copy, Debug)]
75pub struct HostConfig {
76 pub sample_count: u32,
79 pub redraw_interval: Option<Duration>,
84 pub low_latency_present: bool,
104}
105
106impl Default for HostConfig {
107 fn default() -> Self {
108 Self {
109 sample_count: DEFAULT_SAMPLE_COUNT,
110 redraw_interval: None,
111 low_latency_present: false,
112 }
113 }
114}
115
116impl HostConfig {
117 pub fn with_redraw_interval(mut self, interval: Duration) -> Self {
118 self.redraw_interval = Some(interval);
119 self
120 }
121
122 pub fn with_sample_count(mut self, sample_count: u32) -> Self {
123 self.sample_count = sample_count.max(1);
124 self
125 }
126
127 pub fn with_low_latency_present(mut self, low_latency_present: bool) -> Self {
128 self.low_latency_present = low_latency_present;
129 self
130 }
131}
132
133pub trait WinitWgpuApp: App {
141 fn before_build(&mut self) {
142 App::before_build(self);
143 }
144
145 fn gpu_setup(&mut self, _device: &wgpu::Device, _queue: &wgpu::Queue) {}
154
155 fn before_paint(&mut self, _queue: &wgpu::Queue) {}
162}
163
164struct BasicApp<A>(A);
165
166impl<A: App> App for BasicApp<A> {
167 fn before_build(&mut self) {
168 self.0.before_build();
169 }
170
171 fn build(&self, cx: &aetna_core::BuildCx) -> aetna_core::El {
172 self.0.build(cx)
173 }
174
175 fn on_event(&mut self, event: aetna_core::UiEvent) {
176 self.0.on_event(event);
177 }
178
179 fn hotkeys(&self) -> Vec<(aetna_core::KeyChord, String)> {
180 self.0.hotkeys()
181 }
182
183 fn drain_toasts(&mut self) -> Vec<aetna_core::toast::ToastSpec> {
184 self.0.drain_toasts()
185 }
186
187 fn drain_focus_requests(&mut self) -> Vec<String> {
188 self.0.drain_focus_requests()
189 }
190
191 fn drain_scroll_requests(&mut self) -> Vec<aetna_core::scroll::ScrollRequest> {
192 self.0.drain_scroll_requests()
193 }
194
195 fn drain_link_opens(&mut self) -> Vec<String> {
196 self.0.drain_link_opens()
197 }
198
199 fn shaders(&self) -> Vec<aetna_core::AppShader> {
200 self.0.shaders()
201 }
202
203 fn theme(&self) -> aetna_core::Theme {
204 self.0.theme()
205 }
206
207 fn selection(&self) -> aetna_core::Selection {
208 self.0.selection()
209 }
210}
211
212impl<A: App> WinitWgpuApp for BasicApp<A> {}
213
214pub fn run<A: App + 'static>(
219 title: &'static str,
220 viewport: Rect,
221 app: A,
222) -> Result<(), Box<dyn std::error::Error>> {
223 run_host(title, viewport, BasicApp(app), HostConfig::default())
224}
225
226pub fn run_with_config<A: App + 'static>(
233 title: &'static str,
234 viewport: Rect,
235 app: A,
236 config: HostConfig,
237) -> Result<(), Box<dyn std::error::Error>> {
238 run_host(title, viewport, BasicApp(app), config)
239}
240
241pub fn run_on_event_loop<A: App + 'static>(
247 event_loop: EventLoop<()>,
248 title: &'static str,
249 viewport: Rect,
250 app: A,
251 config: HostConfig,
252) -> Result<(), Box<dyn std::error::Error>> {
253 run_host_on_event_loop(event_loop, title, viewport, BasicApp(app), config)
254}
255
256pub fn run_host_app_with_config<A: WinitWgpuApp + 'static>(
261 title: &'static str,
262 viewport: Rect,
263 app: A,
264 config: HostConfig,
265) -> Result<(), Box<dyn std::error::Error>> {
266 run_host(title, viewport, app, config)
267}
268
269pub fn run_host_app_on_event_loop<A: WinitWgpuApp + 'static>(
272 event_loop: EventLoop<()>,
273 title: &'static str,
274 viewport: Rect,
275 app: A,
276 config: HostConfig,
277) -> Result<(), Box<dyn std::error::Error>> {
278 run_host_on_event_loop(event_loop, title, viewport, app, config)
279}
280
281pub fn run_host_app<A: WinitWgpuApp + 'static>(
286 title: &'static str,
287 viewport: Rect,
288 app: A,
289) -> Result<(), Box<dyn std::error::Error>> {
290 run_host(title, viewport, app, HostConfig::default())
291}
292
293fn run_host<A: WinitWgpuApp + 'static>(
294 title: &'static str,
295 viewport: Rect,
296 app: A,
297 config: HostConfig,
298) -> Result<(), Box<dyn std::error::Error>> {
299 let event_loop = EventLoop::new()?;
300 run_host_on_event_loop(event_loop, title, viewport, app, config)
301}
302
303fn run_host_on_event_loop<A: WinitWgpuApp + 'static>(
304 event_loop: EventLoop<()>,
305 title: &'static str,
306 viewport: Rect,
307 app: A,
308 config: HostConfig,
309) -> Result<(), Box<dyn std::error::Error>> {
310 event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
311 #[cfg(target_os = "android")]
312 let android_app = event_loop.android_app().clone();
313 #[cfg(not(target_os = "android"))]
314 let clipboard = new_clipboard();
315 #[cfg(target_os = "android")]
316 let clipboard = new_clipboard(&android_app);
317 let mut host = Host {
318 title,
319 viewport,
320 config,
321 app,
322 #[cfg(target_os = "android")]
323 android_app,
324 gfx: None,
325 last_pointer: None,
326 modifiers: KeyModifiers::default(),
327 next_periodic_redraw: None,
328 last_cursor: Cursor::Default,
329 #[cfg(any(target_os = "android", target_os = "ios"))]
330 ime_allowed: false,
331 pending_resize: None,
332 next_layout_redraw: None,
333 next_paint_redraw: None,
334 next_trigger: FrameTrigger::Initial,
335 last_frame_at: None,
336 last_build: Duration::ZERO,
337 last_prepare: Duration::ZERO,
338 last_layout: Duration::ZERO,
339 last_layout_intrinsic_cache_hits: 0,
340 last_layout_intrinsic_cache_misses: 0,
341 last_layout_pruned_subtrees: 0,
342 last_layout_pruned_nodes: 0,
343 last_draw_ops: Duration::ZERO,
344 last_draw_ops_culled_text_ops: 0,
345 last_paint: Duration::ZERO,
346 last_paint_culled_ops: 0,
347 last_gpu_upload: Duration::ZERO,
348 last_snapshot: Duration::ZERO,
349 last_submit: Duration::ZERO,
350 last_text_layout_cache_hits: 0,
351 last_text_layout_cache_misses: 0,
352 last_text_layout_cache_evictions: 0,
353 last_text_layout_shaped_bytes: 0,
354 frame_index: 0,
355 backend: "?",
356 clipboard,
357 last_primary: String::new(),
358 };
359 event_loop.run_app(&mut host)?;
360 Ok(())
361}
362
363struct Host<A: WinitWgpuApp> {
364 title: &'static str,
365 viewport: Rect,
366 config: HostConfig,
367 app: A,
368 #[cfg(target_os = "android")]
369 android_app: AndroidApp,
370 gfx: Option<Gfx>,
371 last_pointer: Option<(f32, f32)>,
374 modifiers: KeyModifiers,
375 next_periodic_redraw: Option<Instant>,
376 last_cursor: Cursor,
381 #[cfg(any(target_os = "android", target_os = "ios"))]
384 ime_allowed: bool,
385 pending_resize: Option<PhysicalSize<u32>>,
392 next_layout_redraw: Option<Instant>,
398 next_paint_redraw: Option<Instant>,
405 next_trigger: FrameTrigger,
410 last_frame_at: Option<Instant>,
413 last_build: Duration,
415 last_prepare: Duration,
416 last_layout: Duration,
417 last_layout_intrinsic_cache_hits: u64,
418 last_layout_intrinsic_cache_misses: u64,
419 last_layout_pruned_subtrees: u64,
420 last_layout_pruned_nodes: u64,
421 last_draw_ops: Duration,
422 last_draw_ops_culled_text_ops: u64,
423 last_paint: Duration,
424 last_paint_culled_ops: u64,
425 last_gpu_upload: Duration,
426 last_snapshot: Duration,
427 last_submit: Duration,
428 last_text_layout_cache_hits: u64,
429 last_text_layout_cache_misses: u64,
430 last_text_layout_cache_evictions: u64,
431 last_text_layout_shaped_bytes: u64,
432 frame_index: u64,
435 backend: &'static str,
439 clipboard: PlatformClipboard,
443 last_primary: String,
445}
446
447struct Gfx {
448 renderer: Runner,
452 surface: wgpu::Surface<'static>,
453 queue: wgpu::Queue,
454 device: wgpu::Device,
455 window: Arc<Window>,
456 config: wgpu::SurfaceConfiguration,
457 msaa: Option<MsaaTarget>,
461}
462
463fn surface_extent(config: &wgpu::SurfaceConfiguration) -> wgpu::Extent3d {
464 wgpu::Extent3d {
465 width: config.width,
466 height: config.height,
467 depth_or_array_layers: 1,
468 }
469}
470
471#[cfg(target_os = "android")]
472fn safe_area_for_window(window: &Window, surface_size: (u32, u32), scale_factor: f32) -> Sides {
473 let rect = window.content_rect();
474 if rect.right <= rect.left || rect.bottom <= rect.top || scale_factor <= 0.0 {
475 return Sides::default();
476 }
477 let (surface_w, surface_h) = (surface_size.0 as i32, surface_size.1 as i32);
478 Sides {
479 left: rect.left.max(0) as f32 / scale_factor,
480 top: rect.top.max(0) as f32 / scale_factor,
481 right: (surface_w - rect.right).max(0) as f32 / scale_factor,
482 bottom: (surface_h - rect.bottom).max(0) as f32 / scale_factor,
483 }
484}
485
486#[cfg(not(target_os = "android"))]
487fn safe_area_for_window(_window: &Window, _surface_size: (u32, u32), _scale_factor: f32) -> Sides {
488 Sides::default()
489}
490
491#[cfg(any(target_os = "android", target_os = "ios"))]
492fn sync_mobile_ime(window: &Window, renderer: &Runner, ime_allowed: &mut bool) {
493 let allowed = renderer.focused_captures_keys();
494 if allowed != *ime_allowed {
495 window.set_ime_allowed(allowed);
496 *ime_allowed = allowed;
497 }
498}
499
500impl<A: WinitWgpuApp> ApplicationHandler for Host<A> {
501 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
502 if self.gfx.is_some() {
503 return;
504 }
505 let attrs = Window::default_attributes()
506 .with_title(self.title)
507 .with_inner_size(PhysicalSize::new(
508 self.viewport.w as u32,
509 self.viewport.h as u32,
510 ));
511 let window = Arc::new(event_loop.create_window(attrs).expect("create window"));
512
513 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
514 let surface = instance
515 .create_surface(window.clone())
516 .expect("create surface");
517
518 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
519 power_preference: wgpu::PowerPreference::default(),
520 compatible_surface: Some(&surface),
521 force_fallback_adapter: false,
522 }))
523 .expect("no compatible adapter");
524 self.backend = backend_label(adapter.get_info().backend);
525
526 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
527 label: Some("aetna_winit_wgpu::device"),
528 required_features: wgpu::Features::empty(),
529 required_limits: wgpu::Limits::default(),
530 experimental_features: wgpu::ExperimentalFeatures::default(),
531 memory_hints: wgpu::MemoryHints::Performance,
532 trace: wgpu::Trace::Off,
533 }))
534 .expect("request_device");
535
536 let size = window.inner_size();
537 let surface_caps = surface.get_capabilities(&adapter);
538 let format = surface_caps
539 .formats
540 .iter()
541 .copied()
542 .find(|f| f.is_srgb())
543 .unwrap_or(surface_caps.formats[0]);
544 let mode_override = std::env::var("AETNA_PRESENT_MODE").ok();
554 let prefer_mailbox =
555 self.config.low_latency_present || mode_override.as_deref() == Some("mailbox");
556 let prefer_immediate = mode_override.as_deref() == Some("immediate");
557 let prefer_fifo = mode_override.as_deref() == Some("fifo");
558 let present_mode = if prefer_immediate
559 && surface_caps
560 .present_modes
561 .contains(&wgpu::PresentMode::Immediate)
562 {
563 wgpu::PresentMode::Immediate
564 } else if prefer_mailbox
565 && !prefer_fifo
566 && surface_caps
567 .present_modes
568 .contains(&wgpu::PresentMode::Mailbox)
569 {
570 wgpu::PresentMode::Mailbox
571 } else if surface_caps
572 .present_modes
573 .contains(&wgpu::PresentMode::Fifo)
574 {
575 wgpu::PresentMode::Fifo
576 } else {
577 surface_caps.present_modes[0]
578 };
579 let config = wgpu::SurfaceConfiguration {
580 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
585 format,
586 width: size.width.max(1),
587 height: size.height.max(1),
588 present_mode,
589 alpha_mode: surface_caps.alpha_modes[0],
590 view_formats: vec![],
591 desired_maximum_frame_latency: 1,
600 };
601 surface.configure(&device, &config);
602
603 let sample_count = self.config.sample_count.max(1);
604 let mut renderer = Runner::with_sample_count(&device, &queue, format, sample_count);
605 renderer.set_theme(self.app.theme());
606 renderer.set_surface_size(config.width, config.height);
607 renderer.warm_default_glyphs();
612 for s in self.app.shaders() {
615 renderer.register_shader_with(
616 &device,
617 s.name,
618 s.wgsl,
619 s.samples_backdrop,
620 s.samples_time,
621 );
622 }
623
624 let msaa = (sample_count > 1)
625 .then(|| MsaaTarget::new(&device, format, surface_extent(&config), sample_count));
626
627 self.gfx = Some(Gfx {
628 renderer,
629 surface,
630 queue,
631 device,
632 window,
633 config,
634 msaa,
635 });
636 let gfx = self.gfx.as_ref().unwrap();
642 self.app.gpu_setup(&gfx.device, &gfx.queue);
643 self.next_periodic_redraw = self
644 .config
645 .redraw_interval
646 .map(|interval| Instant::now() + interval);
647 gfx.window.request_redraw();
648 }
649
650 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
651 #[cfg(target_os = "android")]
652 {
653 self.gfx.take();
659 self.pending_resize = None;
660 self.last_pointer = None;
661 self.last_frame_at = None;
662 self.next_periodic_redraw = None;
663 self.ime_allowed = false;
664 }
665 }
666
667 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
668 match event {
669 WindowEvent::CloseRequested => {
670 self.gfx.take();
671 event_loop.exit();
672 }
673
674 event => {
675 let Some(gfx) = self.gfx.as_mut() else {
676 return;
677 };
678 let scale = gfx.window.scale_factor() as f32;
679
680 match event {
681 WindowEvent::Resized(size) => {
682 let w = size.width.max(1);
683 let h = size.height.max(1);
684 let already_pending = self
689 .pending_resize
690 .map(|s| s.width == w && s.height == h)
691 .unwrap_or(false);
692 let same_as_current = self.pending_resize.is_none()
693 && w == gfx.config.width
694 && h == gfx.config.height;
695 if already_pending || same_as_current {
696 return;
697 }
698 self.pending_resize = Some(PhysicalSize::new(w, h));
699 self.next_trigger = FrameTrigger::Resize;
700 gfx.window.request_redraw();
701 }
702
703 WindowEvent::CursorMoved { position, .. } => {
704 let lx = position.x as f32 / scale;
705 let ly = position.y as f32 / scale;
706 self.last_pointer = Some((lx, ly));
707 let moved = gfx.renderer.pointer_moved(Pointer::moving(lx, ly));
708 for event in moved.events {
709 dispatch_app_event(
710 &mut self.app,
711 event,
712 &gfx.renderer,
713 &mut self.clipboard,
714 &mut self.last_primary,
715 );
716 }
717 if moved.needs_redraw {
724 self.next_trigger = FrameTrigger::Pointer;
725 gfx.window.request_redraw();
726 }
727 }
728
729 WindowEvent::CursorLeft { .. } => {
730 self.last_pointer = None;
731 for event in gfx.renderer.pointer_left() {
732 dispatch_app_event(
733 &mut self.app,
734 event,
735 &gfx.renderer,
736 &mut self.clipboard,
737 &mut self.last_primary,
738 );
739 }
740 self.next_trigger = FrameTrigger::Pointer;
741 gfx.window.request_redraw();
742 }
743
744 WindowEvent::HoveredFile(path) => {
745 let (lx, ly) = self.last_pointer.unwrap_or((0.0, 0.0));
750 for event in gfx.renderer.file_hovered(path, lx, ly) {
751 dispatch_app_event(
752 &mut self.app,
753 event,
754 &gfx.renderer,
755 &mut self.clipboard,
756 &mut self.last_primary,
757 );
758 }
759 self.next_trigger = FrameTrigger::Pointer;
760 gfx.window.request_redraw();
761 }
762
763 WindowEvent::HoveredFileCancelled => {
764 for event in gfx.renderer.file_hover_cancelled() {
765 dispatch_app_event(
766 &mut self.app,
767 event,
768 &gfx.renderer,
769 &mut self.clipboard,
770 &mut self.last_primary,
771 );
772 }
773 self.next_trigger = FrameTrigger::Pointer;
774 gfx.window.request_redraw();
775 }
776
777 WindowEvent::DroppedFile(path) => {
778 let (lx, ly) = self.last_pointer.unwrap_or((0.0, 0.0));
779 for event in gfx.renderer.file_dropped(path, lx, ly) {
780 dispatch_app_event(
781 &mut self.app,
782 event,
783 &gfx.renderer,
784 &mut self.clipboard,
785 &mut self.last_primary,
786 );
787 }
788 self.next_trigger = FrameTrigger::Pointer;
789 gfx.window.request_redraw();
790 }
791
792 WindowEvent::MouseInput { state, button, .. } => {
793 let Some(button) = pointer_button(button) else {
794 return;
795 };
796 let Some((lx, ly)) = self.last_pointer else {
797 return;
798 };
799 match state {
800 ElementState::Pressed => {
801 for event in
802 gfx.renderer.pointer_down(Pointer::mouse(lx, ly, button))
803 {
804 dispatch_app_event(
805 &mut self.app,
806 event,
807 &gfx.renderer,
808 &mut self.clipboard,
809 &mut self.last_primary,
810 );
811 }
812 #[cfg(any(target_os = "android", target_os = "ios"))]
813 sync_mobile_ime(&gfx.window, &gfx.renderer, &mut self.ime_allowed);
814 self.next_trigger = FrameTrigger::Pointer;
815 gfx.window.request_redraw();
816 }
817 ElementState::Released => {
818 for event in gfx.renderer.pointer_up(Pointer::mouse(lx, ly, button))
819 {
820 let event =
821 attach_primary_selection_text(event, &mut self.clipboard);
822 dispatch_app_event(
823 &mut self.app,
824 event,
825 &gfx.renderer,
826 &mut self.clipboard,
827 &mut self.last_primary,
828 );
829 }
830 self.next_trigger = FrameTrigger::Pointer;
831 gfx.window.request_redraw();
832 }
833 }
834 }
835
836 WindowEvent::MouseWheel { delta, .. } => {
837 let Some((lx, ly)) = self.last_pointer else {
838 return;
839 };
840 let dy = match delta {
844 MouseScrollDelta::LineDelta(_, y) => -y * 50.0,
845 MouseScrollDelta::PixelDelta(p) => -(p.y as f32) / scale,
846 };
847 if gfx.renderer.pointer_wheel(lx, ly, dy) {
848 self.next_trigger = FrameTrigger::Pointer;
849 gfx.window.request_redraw();
850 }
851 }
852
853 WindowEvent::ModifiersChanged(modifiers) => {
854 self.modifiers = key_modifiers(modifiers.state());
855 gfx.renderer.set_modifiers(self.modifiers);
856 }
857
858 WindowEvent::KeyboardInput {
859 event:
860 key_event @ winit::event::KeyEvent {
861 state: ElementState::Pressed,
862 ..
863 },
864 is_synthetic: false,
865 ..
866 } => {
867 if let Some(key) = map_key(&key_event.logical_key) {
868 for event in
869 gfx.renderer.key_down(key, self.modifiers, key_event.repeat)
870 {
871 match text_input::clipboard_request(&event) {
872 Some(ClipboardKind::Copy) => {
873 copy_current_selection(&gfx.renderer, &mut self.clipboard);
874 dispatch_app_event(
875 &mut self.app,
876 event,
877 &gfx.renderer,
878 &mut self.clipboard,
879 &mut self.last_primary,
880 );
881 }
882 Some(ClipboardKind::Cut) => {
883 copy_current_selection(&gfx.renderer, &mut self.clipboard);
884 let delete = clipboard::delete_selection_event(event);
885 dispatch_app_event(
886 &mut self.app,
887 delete,
888 &gfx.renderer,
889 &mut self.clipboard,
890 &mut self.last_primary,
891 );
892 }
893 Some(ClipboardKind::Paste) => {
894 if let Some(paste) = paste_text_from_clipboard(
895 event.clone(),
896 &mut self.clipboard,
897 ) {
898 dispatch_app_event(
899 &mut self.app,
900 paste,
901 &gfx.renderer,
902 &mut self.clipboard,
903 &mut self.last_primary,
904 );
905 } else {
906 dispatch_app_event(
907 &mut self.app,
908 event,
909 &gfx.renderer,
910 &mut self.clipboard,
911 &mut self.last_primary,
912 );
913 }
914 }
915 None => dispatch_app_event(
916 &mut self.app,
917 event,
918 &gfx.renderer,
919 &mut self.clipboard,
920 &mut self.last_primary,
921 ),
922 }
923 }
924 }
925 if let Some(text) = &key_event.text
930 && let Some(event) = gfx.renderer.text_input(text.to_string())
931 {
932 dispatch_app_event(
933 &mut self.app,
934 event,
935 &gfx.renderer,
936 &mut self.clipboard,
937 &mut self.last_primary,
938 );
939 }
940 self.next_trigger = FrameTrigger::Keyboard;
941 gfx.window.request_redraw();
942 }
943 WindowEvent::Ime(winit::event::Ime::Commit(text)) => {
944 if let Some(event) = gfx.renderer.text_input(text) {
945 dispatch_app_event(
946 &mut self.app,
947 event,
948 &gfx.renderer,
949 &mut self.clipboard,
950 &mut self.last_primary,
951 );
952 }
953 self.next_trigger = FrameTrigger::Keyboard;
954 gfx.window.request_redraw();
955 }
956
957 WindowEvent::Touch(touch) => {
958 let lx = touch.location.x as f32 / scale;
959 let ly = touch.location.y as f32 / scale;
960 self.last_pointer = Some((lx, ly));
961 let mut pointer = Pointer::touch(
962 lx,
963 ly,
964 PointerButton::Primary,
965 aetna_core::PointerId(touch.id as u32),
966 );
967 pointer.pressure = touch_pressure(touch.force);
968 match touch.phase {
969 TouchPhase::Started => {
970 for event in gfx.renderer.pointer_down(pointer) {
971 dispatch_app_event(
972 &mut self.app,
973 event,
974 &gfx.renderer,
975 &mut self.clipboard,
976 &mut self.last_primary,
977 );
978 }
979 #[cfg(any(target_os = "android", target_os = "ios"))]
980 sync_mobile_ime(&gfx.window, &gfx.renderer, &mut self.ime_allowed);
981 }
982 TouchPhase::Moved => {
983 let moved = gfx.renderer.pointer_moved(pointer);
984 for event in moved.events {
985 dispatch_app_event(
986 &mut self.app,
987 event,
988 &gfx.renderer,
989 &mut self.clipboard,
990 &mut self.last_primary,
991 );
992 }
993 if !moved.needs_redraw {
994 return;
995 }
996 }
997 TouchPhase::Ended => {
998 for event in gfx.renderer.pointer_up(pointer) {
999 dispatch_app_event(
1000 &mut self.app,
1001 event,
1002 &gfx.renderer,
1003 &mut self.clipboard,
1004 &mut self.last_primary,
1005 );
1006 }
1007 self.last_pointer = None;
1008 }
1009 TouchPhase::Cancelled => {
1010 for event in gfx.renderer.pointer_left() {
1011 dispatch_app_event(
1012 &mut self.app,
1013 event,
1014 &gfx.renderer,
1015 &mut self.clipboard,
1016 &mut self.last_primary,
1017 );
1018 }
1019 self.last_pointer = None;
1020 }
1021 }
1022 self.next_trigger = FrameTrigger::Pointer;
1023 gfx.window.request_redraw();
1024 }
1025
1026 WindowEvent::RedrawRequested => {
1027 for event in gfx.renderer.poll_input(Instant::now()) {
1036 self.app.on_event(event);
1037 }
1038 if let Some(size) = self.pending_resize.take() {
1043 gfx.config.width = size.width;
1044 gfx.config.height = size.height;
1045 gfx.surface.configure(&gfx.device, &gfx.config);
1046 gfx.renderer
1047 .set_surface_size(gfx.config.width, gfx.config.height);
1048 let extent = surface_extent(&gfx.config);
1049 if let Some(msaa) = gfx.msaa.as_mut()
1050 && !msaa.matches(extent)
1051 {
1052 *msaa = MsaaTarget::new(
1053 &gfx.device,
1054 gfx.config.format,
1055 extent,
1056 msaa.sample_count,
1057 );
1058 }
1059 }
1060 let frame = match gfx.surface.get_current_texture() {
1061 wgpu::CurrentSurfaceTexture::Success(t)
1062 | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
1063 wgpu::CurrentSurfaceTexture::Lost
1064 | wgpu::CurrentSurfaceTexture::Outdated => {
1065 gfx.surface.configure(&gfx.device, &gfx.config);
1066 return;
1067 }
1068 other => {
1069 eprintln!("surface unavailable: {other:?}");
1070 return;
1071 }
1072 };
1073 let view = frame
1074 .texture
1075 .create_view(&wgpu::TextureViewDescriptor::default());
1076
1077 let frame_start = Instant::now();
1087 let last_frame_dt = self
1088 .last_frame_at
1089 .map(|t| frame_start.duration_since(t))
1090 .unwrap_or(Duration::ZERO);
1091 self.last_frame_at = Some(frame_start);
1092 let trigger = std::mem::take(&mut self.next_trigger);
1093 let scale_factor = gfx.window.scale_factor() as f32;
1094 let viewport = Rect::new(
1095 0.0,
1096 0.0,
1097 gfx.config.width as f32 / scale_factor,
1098 gfx.config.height as f32 / scale_factor,
1099 );
1100 let paint_only =
1108 trigger == FrameTrigger::ShaderPaint && self.pending_resize.is_none();
1109
1110 let (prepare, palette, t_after_build, t_after_prepare) = if paint_only {
1111 aetna_core::profile_span!("frame::repaint");
1112 let palette = gfx.renderer.theme().palette().clone();
1116 let t_after_build = Instant::now();
1117 let prepare = gfx.renderer.repaint(
1118 &gfx.device,
1119 &gfx.queue,
1120 viewport,
1121 scale_factor,
1122 );
1123 let t_after_prepare = Instant::now();
1124 (prepare, palette, t_after_build, t_after_prepare)
1125 } else {
1126 let msaa_samples =
1127 gfx.msaa.as_ref().map(|m| m.sample_count).unwrap_or(1);
1128 self.frame_index = self.frame_index.wrapping_add(1);
1129 let diagnostics = HostDiagnostics {
1130 backend: self.backend,
1131 surface_size: (gfx.config.width, gfx.config.height),
1132 scale_factor,
1133 msaa_samples,
1134 frame_index: self.frame_index,
1135 last_frame_dt,
1136 last_build: self.last_build,
1137 last_prepare: self.last_prepare,
1138 last_layout: self.last_layout,
1139 last_layout_intrinsic_cache_hits: self
1140 .last_layout_intrinsic_cache_hits,
1141 last_layout_intrinsic_cache_misses: self
1142 .last_layout_intrinsic_cache_misses,
1143 last_layout_pruned_subtrees: self.last_layout_pruned_subtrees,
1144 last_layout_pruned_nodes: self.last_layout_pruned_nodes,
1145 last_draw_ops: self.last_draw_ops,
1146 last_draw_ops_culled_text_ops: self.last_draw_ops_culled_text_ops,
1147 last_paint: self.last_paint,
1148 last_paint_culled_ops: self.last_paint_culled_ops,
1149 last_gpu_upload: self.last_gpu_upload,
1150 last_snapshot: self.last_snapshot,
1151 last_submit: self.last_submit,
1152 last_text_layout_cache_hits: self.last_text_layout_cache_hits,
1153 last_text_layout_cache_misses: self.last_text_layout_cache_misses,
1154 last_text_layout_cache_evictions: self
1155 .last_text_layout_cache_evictions,
1156 last_text_layout_shaped_bytes: self.last_text_layout_shaped_bytes,
1157 trigger,
1158 };
1159 let (mut tree, palette) = {
1160 aetna_core::profile_span!("frame::build");
1161 self.app.before_paint(&gfx.queue);
1162 WinitWgpuApp::before_build(&mut self.app);
1163 let theme = self.app.theme();
1164 let palette = theme.palette().clone();
1165 let cx = aetna_core::BuildCx::new(&theme)
1166 .with_ui_state(gfx.renderer.ui_state())
1167 .with_diagnostics(&diagnostics)
1168 .with_viewport(viewport.w, viewport.h)
1169 .with_safe_area(safe_area_for_window(
1170 &gfx.window,
1171 (gfx.config.width, gfx.config.height),
1172 scale_factor,
1173 ));
1174 let tree = self.app.build(&cx);
1175 gfx.renderer.set_theme(theme);
1176 gfx.renderer.set_hotkeys(self.app.hotkeys());
1177 gfx.renderer.set_selection(self.app.selection());
1178 gfx.renderer.push_toasts(self.app.drain_toasts());
1179 gfx.renderer
1180 .push_focus_requests(self.app.drain_focus_requests());
1181 gfx.renderer
1182 .push_scroll_requests(self.app.drain_scroll_requests());
1183 for url in self.app.drain_link_opens() {
1184 #[cfg(target_os = "android")]
1185 open_link(&self.android_app, &url);
1186 #[cfg(not(any(target_os = "android", target_os = "ios")))]
1187 open_link(&url);
1188 #[cfg(target_os = "ios")]
1189 open_link(&url);
1190 }
1191 (tree, palette)
1192 };
1193 let t_after_build = Instant::now();
1194 let prepare = {
1195 aetna_core::profile_span!("frame::prepare");
1196 gfx.renderer.prepare(
1197 &gfx.device,
1198 &gfx.queue,
1199 &mut tree,
1200 viewport,
1201 scale_factor,
1202 )
1203 };
1204 #[cfg(any(target_os = "android", target_os = "ios"))]
1205 sync_mobile_ime(&gfx.window, &gfx.renderer, &mut self.ime_allowed);
1206 let t_after_prepare = Instant::now();
1207 let cursor = gfx.renderer.ui_state().cursor(&tree);
1212 if cursor != self.last_cursor {
1213 gfx.window.set_cursor(winit_cursor(cursor));
1214 self.last_cursor = cursor;
1215 }
1216 (prepare, palette, t_after_build, t_after_prepare)
1217 };
1218
1219 {
1220 aetna_core::profile_span!("frame::submit");
1221 let mut encoder = gfx.device.create_command_encoder(
1222 &wgpu::CommandEncoderDescriptor {
1223 label: Some("aetna_winit_wgpu::encoder"),
1224 },
1225 );
1226 gfx.renderer.render(
1232 &gfx.device,
1233 &mut encoder,
1234 &frame.texture,
1235 &view,
1236 gfx.msaa.as_ref().map(|msaa| &msaa.view),
1237 wgpu::LoadOp::Clear(bg_color(&palette)),
1238 );
1239 gfx.queue.submit(Some(encoder.finish()));
1240 frame.present();
1241 let t_after_submit = Instant::now();
1242 self.last_build = t_after_build - frame_start;
1243 self.last_prepare = t_after_prepare - t_after_build;
1244 self.last_submit = t_after_submit - t_after_prepare;
1245 self.last_layout = prepare.timings.layout;
1246 self.last_layout_intrinsic_cache_hits =
1247 prepare.timings.layout_intrinsic_cache.hits;
1248 self.last_layout_intrinsic_cache_misses =
1249 prepare.timings.layout_intrinsic_cache.misses;
1250 self.last_layout_pruned_subtrees =
1251 prepare.timings.layout_prune.subtrees;
1252 self.last_layout_pruned_nodes = prepare.timings.layout_prune.nodes;
1253 self.last_draw_ops = prepare.timings.draw_ops;
1254 self.last_draw_ops_culled_text_ops =
1255 prepare.timings.draw_ops_culled_text_ops;
1256 self.last_paint = prepare.timings.paint;
1257 self.last_paint_culled_ops = prepare.timings.paint_culled_ops;
1258 self.last_gpu_upload = prepare.timings.gpu_upload;
1259 self.last_snapshot = prepare.timings.snapshot;
1260 self.last_text_layout_cache_hits =
1261 prepare.timings.text_layout_cache.hits;
1262 self.last_text_layout_cache_misses =
1263 prepare.timings.text_layout_cache.misses;
1264 self.last_text_layout_cache_evictions =
1265 prepare.timings.text_layout_cache.evictions;
1266 self.last_text_layout_shaped_bytes =
1267 prepare.timings.text_layout_cache.shaped_bytes;
1268 }
1269
1270 let now = Instant::now();
1288 if !paint_only {
1289 match prepare.next_layout_redraw_in {
1290 None => self.next_layout_redraw = None,
1291 Some(d) if d.is_zero() => {
1292 self.next_layout_redraw = None;
1293 self.next_trigger = FrameTrigger::Animation;
1294 gfx.window.request_redraw();
1295 }
1296 Some(d) => self.next_layout_redraw = Some(now + d),
1297 }
1298 }
1299 match prepare.next_paint_redraw_in {
1300 None => self.next_paint_redraw = None,
1301 Some(d) if d.is_zero() => {
1302 self.next_paint_redraw = None;
1306 if !matches!(self.next_trigger, FrameTrigger::Animation) {
1307 self.next_trigger = FrameTrigger::ShaderPaint;
1308 }
1309 gfx.window.request_redraw();
1310 }
1311 Some(d) => self.next_paint_redraw = Some(now + d),
1312 }
1313 }
1314 _ => {}
1315 }
1316 }
1317 }
1318 }
1319
1320 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1321 let Some(gfx) = self.gfx.as_ref() else {
1322 event_loop.set_control_flow(ControlFlow::Wait);
1323 return;
1324 };
1325
1326 let now = Instant::now();
1327
1328 if let Some(interval) = self.config.redraw_interval {
1334 let next = self
1335 .next_periodic_redraw
1336 .get_or_insert_with(|| now + interval);
1337 if now >= *next {
1338 self.next_trigger = FrameTrigger::Periodic;
1339 gfx.window.request_redraw();
1340 *next = now + interval;
1341 }
1342 }
1343
1344 let mut wake_up = self.next_periodic_redraw;
1351 if let Some(t) = self.next_layout_redraw {
1352 if now >= t {
1353 self.next_trigger = FrameTrigger::Animation;
1354 gfx.window.request_redraw();
1355 self.next_layout_redraw = None;
1356 } else {
1357 wake_up = Some(match wake_up {
1358 Some(p) => p.min(t),
1359 None => t,
1360 });
1361 }
1362 }
1363 if let Some(t) = self.next_paint_redraw {
1364 if now >= t {
1365 if !matches!(self.next_trigger, FrameTrigger::Animation) {
1369 self.next_trigger = FrameTrigger::ShaderPaint;
1370 }
1371 gfx.window.request_redraw();
1372 self.next_paint_redraw = None;
1373 } else {
1374 wake_up = Some(match wake_up {
1375 Some(p) => p.min(t),
1376 None => t,
1377 });
1378 }
1379 }
1380
1381 match wake_up {
1382 Some(t) => event_loop.set_control_flow(ControlFlow::WaitUntil(t)),
1383 None => event_loop.set_control_flow(ControlFlow::Wait),
1384 }
1385 }
1386}
1387
1388fn map_key(key: &Key) -> Option<UiKey> {
1389 match key {
1390 Key::Named(NamedKey::Enter) => Some(UiKey::Enter),
1391 Key::Named(NamedKey::Escape) => Some(UiKey::Escape),
1392 Key::Named(NamedKey::Tab) => Some(UiKey::Tab),
1393 Key::Named(NamedKey::Space) => Some(UiKey::Space),
1394 Key::Named(NamedKey::ArrowUp) => Some(UiKey::ArrowUp),
1395 Key::Named(NamedKey::ArrowDown) => Some(UiKey::ArrowDown),
1396 Key::Named(NamedKey::ArrowLeft) => Some(UiKey::ArrowLeft),
1397 Key::Named(NamedKey::ArrowRight) => Some(UiKey::ArrowRight),
1398 Key::Named(NamedKey::Backspace) => Some(UiKey::Backspace),
1399 Key::Named(NamedKey::Delete) => Some(UiKey::Delete),
1400 Key::Named(NamedKey::Home) => Some(UiKey::Home),
1401 Key::Named(NamedKey::End) => Some(UiKey::End),
1402 Key::Named(NamedKey::PageUp) => Some(UiKey::PageUp),
1403 Key::Named(NamedKey::PageDown) => Some(UiKey::PageDown),
1404 Key::Character(s) => Some(UiKey::Character(s.to_string())),
1405 Key::Named(named) => Some(UiKey::Other(format!("{named:?}"))),
1406 _ => None,
1407 }
1408}
1409
1410fn pointer_button(b: MouseButton) -> Option<PointerButton> {
1411 match b {
1412 MouseButton::Left => Some(PointerButton::Primary),
1413 MouseButton::Right => Some(PointerButton::Secondary),
1414 MouseButton::Middle => Some(PointerButton::Middle),
1415 _ => None,
1418 }
1419}
1420
1421#[cfg(not(any(target_os = "android", target_os = "ios")))]
1422fn new_clipboard() -> PlatformClipboard {
1423 arboard::Clipboard::new().ok()
1424}
1425
1426#[cfg(target_os = "ios")]
1427fn new_clipboard() -> PlatformClipboard {
1428 PlatformClipboard
1429}
1430
1431#[cfg(target_os = "android")]
1432fn new_clipboard(app: &AndroidApp) -> PlatformClipboard {
1433 PlatformClipboard { app: app.clone() }
1434}
1435
1436#[cfg(not(any(target_os = "android", target_os = "ios")))]
1441fn open_link(url: &str) {
1442 if let Err(err) = open::that_detached(url) {
1443 eprintln!("aetna-winit-wgpu: failed to open {url}: {err}");
1444 }
1445}
1446
1447#[cfg(target_os = "ios")]
1448fn open_link(url: &str) {
1449 eprintln!("aetna-winit-wgpu: opening links is not wired on iOS yet: {url}");
1450}
1451
1452#[cfg(target_os = "android")]
1453fn open_link(app: &AndroidApp, url: &str) {
1454 let app_for_thread = app.clone();
1455 let url = url.to_string();
1456 app.run_on_java_main_thread(Box::new(move || {
1457 let result = (|| -> jni::errors::Result<()> {
1458 let jvm = unsafe { jni::JavaVM::from_raw(app_for_thread.vm_as_ptr().cast()) };
1459 jvm.attach_current_thread(|env| {
1460 let url = env.new_string(&url)?;
1461 let uri = env
1462 .call_static_method(
1463 jni::jni_str!("android/net/Uri"),
1464 jni::jni_str!("parse"),
1465 jni::jni_sig!("(Ljava/lang/String;)Landroid/net/Uri;"),
1466 &[jni::JValue::Object(url.as_ref())],
1467 )?
1468 .l()?;
1469 let action = env
1470 .get_static_field(
1471 jni::jni_str!("android/content/Intent"),
1472 jni::jni_str!("ACTION_VIEW"),
1473 jni::jni_sig!("Ljava/lang/String;"),
1474 )?
1475 .l()?;
1476 let intent = env.new_object(
1477 jni::jni_str!("android/content/Intent"),
1478 jni::jni_sig!("(Ljava/lang/String;Landroid/net/Uri;)V"),
1479 &[jni::JValue::Object(&action), jni::JValue::Object(&uri)],
1480 )?;
1481 let activity = unsafe {
1482 jni::objects::JObject::from_raw(
1483 env,
1484 app_for_thread.activity_as_ptr() as jni::sys::jobject,
1485 )
1486 };
1487 env.call_method(
1488 &activity,
1489 jni::jni_str!("startActivity"),
1490 jni::jni_sig!("(Landroid/content/Intent;)V"),
1491 &[jni::JValue::Object(&intent)],
1492 )?;
1493 Ok(())
1494 })
1495 })();
1496 if let Err(err) = result {
1497 eprintln!("aetna-winit-wgpu: failed to open link on Android: {err}");
1498 }
1499 }));
1500}
1501
1502fn touch_pressure(force: Option<Force>) -> Option<f32> {
1503 match force? {
1504 Force::Calibrated {
1505 force,
1506 max_possible_force,
1507 ..
1508 } if max_possible_force > 0.0 => Some((force / max_possible_force).clamp(0.0, 1.0) as f32),
1509 Force::Calibrated { force, .. } => Some(force.clamp(0.0, 1.0) as f32),
1510 Force::Normalized(v) => Some(v.clamp(0.0, 1.0) as f32),
1511 }
1512}
1513
1514fn winit_cursor(cursor: Cursor) -> CursorIcon {
1520 match cursor {
1521 Cursor::Default => CursorIcon::Default,
1522 Cursor::Pointer => CursorIcon::Pointer,
1523 Cursor::Text => CursorIcon::Text,
1524 Cursor::NotAllowed => CursorIcon::NotAllowed,
1525 Cursor::Grab => CursorIcon::Grab,
1526 Cursor::Grabbing => CursorIcon::Grabbing,
1527 Cursor::Move => CursorIcon::Move,
1528 Cursor::EwResize => CursorIcon::EwResize,
1529 Cursor::NsResize => CursorIcon::NsResize,
1530 Cursor::NwseResize => CursorIcon::NwseResize,
1531 Cursor::NeswResize => CursorIcon::NeswResize,
1532 Cursor::ColResize => CursorIcon::ColResize,
1533 Cursor::RowResize => CursorIcon::RowResize,
1534 Cursor::Crosshair => CursorIcon::Crosshair,
1535 _ => CursorIcon::Default,
1536 }
1537}
1538
1539fn key_modifiers(mods: winit::keyboard::ModifiersState) -> KeyModifiers {
1540 KeyModifiers {
1541 shift: mods.shift_key(),
1542 ctrl: mods.control_key(),
1543 alt: mods.alt_key(),
1544 logo: mods.super_key(),
1545 }
1546}
1547
1548fn bg_color(palette: &aetna_core::Palette) -> wgpu::Color {
1549 let c = palette.background;
1550 wgpu::Color {
1551 r: srgb_to_linear(c.r as f64 / 255.0),
1552 g: srgb_to_linear(c.g as f64 / 255.0),
1553 b: srgb_to_linear(c.b as f64 / 255.0),
1554 a: c.a as f64 / 255.0,
1555 }
1556}
1557
1558fn copy_current_selection(renderer: &Runner, clipboard: &mut PlatformClipboard) {
1559 let Some(text) = renderer.selected_text() else {
1563 return;
1564 };
1565 set_clipboard_text(clipboard, text);
1566}
1567
1568fn dispatch_app_event<A: App>(
1569 app: &mut A,
1570 event: UiEvent,
1571 renderer: &Runner,
1572 clipboard: &mut PlatformClipboard,
1573 last_primary: &mut String,
1574) {
1575 let before = app.selection();
1576 app.on_event(event);
1577 if app.selection() != before {
1578 sync_primary_selection(&app.selection(), renderer, clipboard, last_primary);
1579 }
1580}
1581
1582fn sync_primary_selection(
1583 selection: &aetna_core::selection::Selection,
1584 renderer: &Runner,
1585 clipboard: &mut PlatformClipboard,
1586 last_primary: &mut String,
1587) {
1588 let text = renderer
1589 .selected_text_for(selection)
1590 .filter(|s| !s.is_empty())
1591 .unwrap_or_default();
1592 if text == *last_primary {
1593 return;
1594 }
1595 if !text.is_empty() {
1596 primary::set(clipboard, &text);
1597 }
1598 *last_primary = text;
1599}
1600
1601fn paste_text_from_clipboard(event: UiEvent, clipboard: &mut PlatformClipboard) -> Option<UiEvent> {
1602 let text = get_clipboard_text(clipboard)?;
1603 Some(clipboard::paste_text_event(event, text))
1604}
1605
1606fn attach_primary_selection_text(mut event: UiEvent, clipboard: &mut PlatformClipboard) -> UiEvent {
1607 if event.kind == UiEventKind::MiddleClick {
1608 event.text = primary::get(clipboard);
1609 }
1610 event
1611}
1612
1613#[cfg(not(any(target_os = "android", target_os = "ios")))]
1614fn set_clipboard_text(clipboard: &mut PlatformClipboard, text: String) {
1615 if let Some(cb) = clipboard {
1616 let _ = cb.set_text(text);
1617 }
1618}
1619
1620#[cfg(target_os = "ios")]
1621fn set_clipboard_text(_clipboard: &mut PlatformClipboard, _text: String) {}
1622
1623#[cfg(target_os = "android")]
1624fn set_clipboard_text(clipboard: &mut PlatformClipboard, text: String) {
1625 if let Err(err) = set_android_clipboard_text(&clipboard.app, &text) {
1626 eprintln!("aetna-winit-wgpu: failed to set Android clipboard: {err}");
1627 }
1628}
1629
1630#[cfg(not(any(target_os = "android", target_os = "ios")))]
1631fn get_clipboard_text(clipboard: &mut PlatformClipboard) -> Option<String> {
1632 clipboard.as_mut()?.get_text().ok()
1633}
1634
1635#[cfg(target_os = "ios")]
1636fn get_clipboard_text(_clipboard: &mut PlatformClipboard) -> Option<String> {
1637 None
1638}
1639
1640#[cfg(target_os = "android")]
1641fn get_clipboard_text(clipboard: &mut PlatformClipboard) -> Option<String> {
1642 match get_android_clipboard_text(&clipboard.app) {
1643 Ok(text) => text,
1644 Err(err) => {
1645 eprintln!("aetna-winit-wgpu: failed to read Android clipboard: {err}");
1646 None
1647 }
1648 }
1649}
1650
1651#[cfg(target_os = "android")]
1652fn set_android_clipboard_text(app: &AndroidApp, text: &str) -> jni::errors::Result<()> {
1653 use jni::refs::Reference as _;
1654
1655 let jvm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr().cast()) };
1656 jvm.attach_current_thread(|env| {
1657 let activity = unsafe {
1658 jni::objects::JObject::from_raw(env, app.activity_as_ptr() as jni::sys::jobject)
1659 };
1660 let service_name = env.new_string("clipboard")?;
1661 let clipboard = env
1662 .call_method(
1663 &activity,
1664 jni::jni_str!("getSystemService"),
1665 jni::jni_sig!("(Ljava/lang/String;)Ljava/lang/Object;"),
1666 &[jni::JValue::Object(service_name.as_ref())],
1667 )?
1668 .l()?;
1669 if clipboard.is_null() {
1670 return Ok(());
1671 }
1672
1673 let label = env.new_string("Aetna")?;
1674 let text = env.new_string(text)?;
1675 let clip = env
1676 .call_static_method(
1677 jni::jni_str!("android/content/ClipData"),
1678 jni::jni_str!("newPlainText"),
1679 jni::jni_sig!(
1680 "(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Landroid/content/ClipData;"
1681 ),
1682 &[
1683 jni::JValue::Object(label.as_ref()),
1684 jni::JValue::Object(text.as_ref()),
1685 ],
1686 )?
1687 .l()?;
1688 env.call_method(
1689 &clipboard,
1690 jni::jni_str!("setPrimaryClip"),
1691 jni::jni_sig!("(Landroid/content/ClipData;)V"),
1692 &[jni::JValue::Object(&clip)],
1693 )?;
1694 Ok(())
1695 })
1696}
1697
1698#[cfg(target_os = "android")]
1699fn get_android_clipboard_text(app: &AndroidApp) -> jni::errors::Result<Option<String>> {
1700 use jni::refs::Reference as _;
1701
1702 let jvm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr().cast()) };
1703 jvm.attach_current_thread(|env| {
1704 let activity = unsafe {
1705 jni::objects::JObject::from_raw(env, app.activity_as_ptr() as jni::sys::jobject)
1706 };
1707 let service_name = env.new_string("clipboard")?;
1708 let clipboard = env
1709 .call_method(
1710 &activity,
1711 jni::jni_str!("getSystemService"),
1712 jni::jni_sig!("(Ljava/lang/String;)Ljava/lang/Object;"),
1713 &[jni::JValue::Object(service_name.as_ref())],
1714 )?
1715 .l()?;
1716 if clipboard.is_null() {
1717 return Ok(None);
1718 }
1719
1720 let clip = env
1721 .call_method(
1722 &clipboard,
1723 jni::jni_str!("getPrimaryClip"),
1724 jni::jni_sig!("()Landroid/content/ClipData;"),
1725 &[],
1726 )?
1727 .l()?;
1728 if clip.is_null() {
1729 return Ok(None);
1730 }
1731
1732 let item_count = env
1733 .call_method(
1734 &clip,
1735 jni::jni_str!("getItemCount"),
1736 jni::jni_sig!("()I"),
1737 &[],
1738 )?
1739 .i()?;
1740 if item_count <= 0 {
1741 return Ok(None);
1742 }
1743
1744 let item = env
1745 .call_method(
1746 &clip,
1747 jni::jni_str!("getItemAt"),
1748 jni::jni_sig!("(I)Landroid/content/ClipData$Item;"),
1749 &[jni::JValue::Int(0)],
1750 )?
1751 .l()?;
1752 if item.is_null() {
1753 return Ok(None);
1754 }
1755
1756 let text = env
1757 .call_method(
1758 &item,
1759 jni::jni_str!("coerceToText"),
1760 jni::jni_sig!("(Landroid/content/Context;)Ljava/lang/CharSequence;"),
1761 &[jni::JValue::Object(&activity)],
1762 )?
1763 .l()?;
1764 if text.is_null() {
1765 return Ok(None);
1766 }
1767
1768 let text = env
1769 .call_method(
1770 &text,
1771 jni::jni_str!("toString"),
1772 jni::jni_sig!("()Ljava/lang/String;"),
1773 &[],
1774 )?
1775 .l()?;
1776 if text.is_null() {
1777 return Ok(None);
1778 }
1779
1780 let text = env.cast_local::<jni::objects::JString>(text)?;
1781 Ok(Some(text.try_to_string(env)?))
1782 })
1783}
1784
1785mod primary {
1786 #[cfg(target_os = "linux")]
1787 pub fn set(clipboard: &mut super::PlatformClipboard, text: &str) {
1788 use arboard::{LinuxClipboardKind, SetExtLinux};
1789 if let Some(cb) = clipboard {
1790 let _ = cb.set().clipboard(LinuxClipboardKind::Primary).text(text);
1791 }
1792 }
1793
1794 #[cfg(target_os = "linux")]
1795 pub fn get(clipboard: &mut super::PlatformClipboard) -> Option<String> {
1796 use arboard::{GetExtLinux, LinuxClipboardKind};
1797 let cb = clipboard.as_mut()?;
1798 cb.get().clipboard(LinuxClipboardKind::Primary).text().ok()
1799 }
1800
1801 #[cfg(not(target_os = "linux"))]
1802 pub fn set(_clipboard: &mut super::PlatformClipboard, _text: &str) {}
1803
1804 #[cfg(not(target_os = "linux"))]
1805 pub fn get(_clipboard: &mut super::PlatformClipboard) -> Option<String> {
1806 None
1807 }
1808}
1809
1810fn backend_label(backend: wgpu::Backend) -> &'static str {
1816 match backend {
1817 wgpu::Backend::Vulkan => "Vulkan",
1818 wgpu::Backend::Metal => "Metal",
1819 wgpu::Backend::Dx12 => "DX12",
1820 wgpu::Backend::Gl => "GL",
1821 wgpu::Backend::BrowserWebGpu => "WebGPU",
1822 wgpu::Backend::Noop => "noop",
1823 }
1824}
1825
1826fn srgb_to_linear(c: f64) -> f64 {
1829 if c <= 0.04045 {
1830 c / 12.92
1831 } else {
1832 ((c + 0.055) / 1.055).powf(2.4)
1833 }
1834}
1835
1836#[cfg(test)]
1837mod tests {
1838 use super::*;
1839 use aetna_core::Selection;
1840 use aetna_core::SelectionPoint;
1841 use aetna_core::SelectionRange;
1842
1843 #[test]
1850 fn basic_app_forwards_selection_to_inner() {
1851 struct AppWithSelection;
1852 impl App for AppWithSelection {
1853 fn build(&self, _cx: &aetna_core::BuildCx) -> aetna_core::El {
1854 aetna_core::widgets::text::text("hi")
1855 }
1856 fn selection(&self) -> Selection {
1857 Selection {
1858 range: Some(SelectionRange {
1859 anchor: SelectionPoint::new("p", 0),
1860 head: SelectionPoint::new("p", 5),
1861 }),
1862 }
1863 }
1864 }
1865 let basic = BasicApp(AppWithSelection);
1866 let sel = basic.selection();
1867 let r = sel.range.as_ref().expect("range forwarded through wrapper");
1868 assert_eq!(r.anchor.key, "p");
1869 assert_eq!(r.head.byte, 5);
1870 }
1871}