1use std::{
42 sync::Arc,
43 time::{Duration, Instant},
44};
45
46use damascene_core::color::{ColorManagementStatus, ColorPreferences};
47use damascene_core::widgets::text_input::{self, ClipboardKind};
48use damascene_core::{
49 App, Cursor, FrameTrigger, HostDiagnostics, KeyModifiers, Pointer, PointerButton, Rect, Sides,
50 UiEvent, UiEventKind, UiKey, clipboard,
51};
52use damascene_wgpu::{MsaaTarget, Runner};
53
54#[cfg(all(target_os = "linux", feature = "wayland-color-management"))]
55mod wayland_color;
56
57const DEFAULT_SAMPLE_COUNT: u32 = 4;
58#[cfg(not(any(target_os = "android", target_os = "ios")))]
59type PlatformClipboard = Option<arboard::Clipboard>;
60#[cfg(target_os = "android")]
61struct PlatformClipboard {
62 app: AndroidApp,
63}
64#[cfg(target_os = "ios")]
65#[derive(Default)]
66struct PlatformClipboard;
67
68use winit::application::ApplicationHandler;
69use winit::dpi::PhysicalSize;
70use winit::event::{ElementState, Force, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent};
71use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
72use winit::keyboard::{Key, NamedKey};
73#[cfg(target_os = "android")]
74use winit::platform::android::{EventLoopExtAndroid, WindowExtAndroid, activity::AndroidApp};
75use winit::window::{CursorIcon, Window, WindowId};
76
77#[derive(Clone, Debug)]
79pub struct HostConfig {
80 pub sample_count: u32,
83 pub redraw_interval: Option<Duration>,
88 pub low_latency_present: bool,
108 pub app_id: Option<String>,
122 pub color_preferences: ColorPreferences,
134}
135
136impl Default for HostConfig {
137 fn default() -> Self {
138 Self {
139 sample_count: DEFAULT_SAMPLE_COUNT,
140 redraw_interval: None,
141 low_latency_present: false,
142 app_id: None,
143 color_preferences: ColorPreferences::default(),
144 }
145 }
146}
147
148impl HostConfig {
149 pub fn with_redraw_interval(mut self, interval: Duration) -> Self {
150 self.redraw_interval = Some(interval);
151 self
152 }
153
154 pub fn with_sample_count(mut self, sample_count: u32) -> Self {
155 self.sample_count = sample_count.max(1);
156 self
157 }
158
159 pub fn with_low_latency_present(mut self, low_latency_present: bool) -> Self {
160 self.low_latency_present = low_latency_present;
161 self
162 }
163
164 pub fn with_app_id(mut self, app_id: impl Into<String>) -> Self {
165 self.app_id = Some(app_id.into());
166 self
167 }
168
169 pub fn with_color_preferences(mut self, color_preferences: ColorPreferences) -> Self {
170 self.color_preferences = color_preferences;
171 self
172 }
173}
174
175pub trait WinitWgpuApp: App {
183 fn before_build(&mut self) {
184 App::before_build(self);
185 }
186
187 fn gpu_setup(&mut self, _device: &wgpu::Device, _queue: &wgpu::Queue) {}
196
197 fn before_paint(&mut self, _queue: &wgpu::Queue) {}
204}
205
206struct BasicApp<A>(A);
207
208impl<A: App> App for BasicApp<A> {
209 fn before_build(&mut self) {
210 self.0.before_build();
211 }
212
213 fn build(&self, cx: &damascene_core::BuildCx) -> damascene_core::El {
214 self.0.build(cx)
215 }
216
217 fn on_event(&mut self, event: damascene_core::UiEvent) {
218 self.0.on_event(event);
219 }
220
221 fn on_wheel_event(&mut self, event: damascene_core::UiEvent) -> bool {
222 self.0.on_wheel_event(event)
223 }
224
225 fn hotkeys(&self) -> Vec<(damascene_core::KeyChord, String)> {
226 self.0.hotkeys()
227 }
228
229 fn drain_toasts(&mut self) -> Vec<damascene_core::toast::ToastSpec> {
230 self.0.drain_toasts()
231 }
232
233 fn drain_focus_requests(&mut self) -> Vec<String> {
234 self.0.drain_focus_requests()
235 }
236
237 fn drain_scroll_requests(&mut self) -> Vec<damascene_core::scroll::ScrollRequest> {
238 self.0.drain_scroll_requests()
239 }
240
241 fn drain_link_opens(&mut self) -> Vec<String> {
242 self.0.drain_link_opens()
243 }
244
245 fn shaders(&self) -> Vec<damascene_core::AppShader> {
246 self.0.shaders()
247 }
248
249 fn theme(&self) -> damascene_core::Theme {
250 self.0.theme()
251 }
252
253 fn selection(&self) -> damascene_core::Selection {
254 self.0.selection()
255 }
256}
257
258impl<A: App> WinitWgpuApp for BasicApp<A> {}
259
260pub fn run<A: App + 'static>(
265 title: &'static str,
266 viewport: Rect,
267 app: A,
268) -> Result<(), Box<dyn std::error::Error>> {
269 run_host(title, viewport, BasicApp(app), HostConfig::default())
270}
271
272pub fn run_with_config<A: App + 'static>(
279 title: &'static str,
280 viewport: Rect,
281 app: A,
282 config: HostConfig,
283) -> Result<(), Box<dyn std::error::Error>> {
284 run_host(title, viewport, BasicApp(app), config)
285}
286
287pub fn run_on_event_loop<A: App + 'static>(
293 event_loop: EventLoop<()>,
294 title: &'static str,
295 viewport: Rect,
296 app: A,
297 config: HostConfig,
298) -> Result<(), Box<dyn std::error::Error>> {
299 run_host_on_event_loop(event_loop, title, viewport, BasicApp(app), config)
300}
301
302pub fn run_host_app_with_config<A: WinitWgpuApp + 'static>(
307 title: &'static str,
308 viewport: Rect,
309 app: A,
310 config: HostConfig,
311) -> Result<(), Box<dyn std::error::Error>> {
312 run_host(title, viewport, app, config)
313}
314
315pub fn run_host_app_on_event_loop<A: WinitWgpuApp + 'static>(
318 event_loop: EventLoop<()>,
319 title: &'static str,
320 viewport: Rect,
321 app: A,
322 config: HostConfig,
323) -> Result<(), Box<dyn std::error::Error>> {
324 run_host_on_event_loop(event_loop, title, viewport, app, config)
325}
326
327pub fn run_host_app<A: WinitWgpuApp + 'static>(
332 title: &'static str,
333 viewport: Rect,
334 app: A,
335) -> Result<(), Box<dyn std::error::Error>> {
336 run_host(title, viewport, app, HostConfig::default())
337}
338
339fn run_host<A: WinitWgpuApp + 'static>(
340 title: &'static str,
341 viewport: Rect,
342 app: A,
343 config: HostConfig,
344) -> Result<(), Box<dyn std::error::Error>> {
345 let event_loop = EventLoop::new()?;
346 run_host_on_event_loop(event_loop, title, viewport, app, config)
347}
348
349fn run_host_on_event_loop<A: WinitWgpuApp + 'static>(
350 event_loop: EventLoop<()>,
351 title: &'static str,
352 viewport: Rect,
353 app: A,
354 config: HostConfig,
355) -> Result<(), Box<dyn std::error::Error>> {
356 event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
357 #[cfg(target_os = "android")]
358 let android_app = event_loop.android_app().clone();
359 #[cfg(not(target_os = "android"))]
360 let clipboard = new_clipboard();
361 #[cfg(target_os = "android")]
362 let clipboard = new_clipboard(&android_app);
363 let mut host = Host {
364 title,
365 viewport,
366 config,
367 app,
368 #[cfg(target_os = "android")]
369 android_app,
370 gfx: None,
371 last_pointer: None,
372 modifiers: KeyModifiers::default(),
373 next_periodic_redraw: None,
374 last_cursor: Cursor::Default,
375 #[cfg(any(target_os = "android", target_os = "ios"))]
376 ime_allowed: false,
377 pending_resize: None,
378 next_layout_redraw: None,
379 next_paint_redraw: None,
380 next_trigger: FrameTrigger::Initial,
381 last_frame_at: None,
382 last_build: Duration::ZERO,
383 last_prepare: Duration::ZERO,
384 last_layout: Duration::ZERO,
385 last_layout_intrinsic_cache_hits: 0,
386 last_layout_intrinsic_cache_misses: 0,
387 last_layout_pruned_subtrees: 0,
388 last_layout_pruned_nodes: 0,
389 last_draw_ops: Duration::ZERO,
390 last_draw_ops_culled_text_ops: 0,
391 last_paint: Duration::ZERO,
392 last_paint_culled_ops: 0,
393 last_gpu_upload: Duration::ZERO,
394 last_snapshot: Duration::ZERO,
395 last_submit: Duration::ZERO,
396 last_text_layout_cache_hits: 0,
397 last_text_layout_cache_misses: 0,
398 last_text_layout_cache_evictions: 0,
399 last_text_layout_shaped_bytes: 0,
400 frame_index: 0,
401 backend: "?",
402 clipboard,
403 last_primary: String::new(),
404 };
405 event_loop.run_app(&mut host)?;
406 Ok(())
407}
408
409struct Host<A: WinitWgpuApp> {
410 title: &'static str,
411 viewport: Rect,
412 config: HostConfig,
413 app: A,
414 #[cfg(target_os = "android")]
415 android_app: AndroidApp,
416 gfx: Option<Gfx>,
417 last_pointer: Option<(f32, f32)>,
420 modifiers: KeyModifiers,
421 next_periodic_redraw: Option<Instant>,
422 last_cursor: Cursor,
427 #[cfg(any(target_os = "android", target_os = "ios"))]
430 ime_allowed: bool,
431 pending_resize: Option<PhysicalSize<u32>>,
438 next_layout_redraw: Option<Instant>,
444 next_paint_redraw: Option<Instant>,
451 next_trigger: FrameTrigger,
456 last_frame_at: Option<Instant>,
459 last_build: Duration,
461 last_prepare: Duration,
462 last_layout: Duration,
463 last_layout_intrinsic_cache_hits: u64,
464 last_layout_intrinsic_cache_misses: u64,
465 last_layout_pruned_subtrees: u64,
466 last_layout_pruned_nodes: u64,
467 last_draw_ops: Duration,
468 last_draw_ops_culled_text_ops: u64,
469 last_paint: Duration,
470 last_paint_culled_ops: u64,
471 last_gpu_upload: Duration,
472 last_snapshot: Duration,
473 last_submit: Duration,
474 last_text_layout_cache_hits: u64,
475 last_text_layout_cache_misses: u64,
476 last_text_layout_cache_evictions: u64,
477 last_text_layout_shaped_bytes: u64,
478 frame_index: u64,
481 backend: &'static str,
485 clipboard: PlatformClipboard,
489 last_primary: String,
491}
492
493struct Gfx {
494 color_management: ColorManagementStatus,
501 surface_color: damascene_core::SurfaceColorInfo,
506 renderer: Runner,
507 surface: wgpu::Surface<'static>,
508 queue: wgpu::Queue,
509 device: wgpu::Device,
510 window: Arc<Window>,
511 config: wgpu::SurfaceConfiguration,
512 msaa: Option<MsaaTarget>,
516}
517
518fn surface_extent(config: &wgpu::SurfaceConfiguration) -> wgpu::Extent3d {
519 wgpu::Extent3d {
520 width: config.width,
521 height: config.height,
522 depth_or_array_layers: 1,
523 }
524}
525
526fn srgb_format(caps: &wgpu::SurfaceCapabilities) -> wgpu::TextureFormat {
528 caps.formats
529 .iter()
530 .copied()
531 .find(|f| f.is_srgb())
532 .unwrap_or(caps.formats[0])
533}
534
535#[cfg(all(target_os = "linux", feature = "wayland-color-management"))]
550fn wide_format(caps: &wgpu::SurfaceCapabilities) -> Option<wgpu::TextureFormat> {
551 caps.formats
552 .iter()
553 .copied()
554 .find(|f| *f == wgpu::TextureFormat::Rgba16Float)
555}
556
557#[cfg(all(target_os = "linux", feature = "wayland-color-management"))]
570fn negotiate_output(
571 preferences: &ColorPreferences,
572 caps: &damascene_core::color::HostColorCapabilities,
573 surface_caps: &wgpu::SurfaceCapabilities,
574 targets: &damascene_core::color::CompositorColorTargets,
575) -> (wgpu::TextureFormat, damascene_core::color::ColorSpace) {
576 for &space in &preferences.working_spaces {
577 if caps.supports(space) {
578 if let Some(delivered) = deliver_space(space, surface_caps, targets) {
579 return delivered;
580 }
581 }
582 }
583 (
584 srgb_format(surface_caps),
585 damascene_core::color::ColorSpace::SRGB_LINEAR,
586 )
587}
588
589#[cfg(all(target_os = "linux", feature = "wayland-color-management"))]
594fn deliver_space(
595 space: damascene_core::color::ColorSpace,
596 surface_caps: &wgpu::SurfaceCapabilities,
597 targets: &damascene_core::color::CompositorColorTargets,
598) -> Option<(wgpu::TextureFormat, damascene_core::color::ColorSpace)> {
599 use damascene_core::color::{ColorSpace, Primaries, TransferFunction};
600 match (space.primaries, space.transfer) {
601 (Primaries::Srgb, TransferFunction::Srgb) => {
604 Some((srgb_format(surface_caps), ColorSpace::SRGB_LINEAR))
605 }
606 (Primaries::Srgb, TransferFunction::Linear) => {
612 if targets.indicates_hdr() {
613 wide_format(surface_caps).map(|f| (f, ColorSpace::SRGB_LINEAR))
614 } else {
615 None
616 }
617 }
618 _ => None,
622 }
623}
624
625fn build_surface_color_info(
630 adapter: &wgpu::Adapter,
631 surface_caps: &wgpu::SurfaceCapabilities,
632 chosen_format: wgpu::TextureFormat,
633 present_mode: wgpu::PresentMode,
634 alpha_mode: wgpu::CompositeAlphaMode,
635) -> damascene_core::SurfaceColorInfo {
636 let info = adapter.get_info();
637 let driver = match (info.driver.is_empty(), info.driver_info.is_empty()) {
638 (false, false) => format!("{} ({})", info.driver, info.driver_info),
639 (false, true) => info.driver.clone(),
640 (true, false) => info.driver_info.clone(),
641 (true, true) => String::new(),
642 };
643 damascene_core::SurfaceColorInfo {
644 adapter: info.name,
645 driver,
646 formats: surface_caps
647 .formats
648 .iter()
649 .map(|f| classify_surface_format(*f))
650 .collect(),
651 chosen_format: format!("{chosen_format:?}"),
652 present_mode: format!("{present_mode:?}"),
653 alpha_mode: format!("{alpha_mode:?}"),
654 }
655}
656
657fn classify_surface_format(f: wgpu::TextureFormat) -> damascene_core::SurfaceFormatInfo {
659 use wgpu::TextureFormat::{Rgb10a2Unorm, Rgba16Float, Rgba32Float};
660 damascene_core::SurfaceFormatInfo {
661 name: format!("{f:?}"),
662 srgb: f.is_srgb(),
663 wide: matches!(f, Rgba16Float | Rgba32Float | Rgb10a2Unorm),
667 }
668}
669
670#[cfg(all(target_os = "linux", feature = "wayland-color-management"))]
688fn negotiate_color(
689 window: &Window,
690 preferences: &ColorPreferences,
691 surface_caps: &wgpu::SurfaceCapabilities,
692) -> ColorSetup {
693 use damascene_core::color::HostColorCapabilities;
694 use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle};
695
696 let handles = (
698 window.display_handle().ok().map(|h| h.as_raw()),
699 window.window_handle().ok().map(|h| h.as_raw()),
700 );
701 let (display_ptr, surface_ptr) = match handles {
702 (Some(RawDisplayHandle::Wayland(d)), Some(RawWindowHandle::Wayland(w))) => {
703 (d.display.as_ptr(), w.surface.as_ptr())
704 }
705 _ => return ColorSetup::srgb_unavailable(surface_caps),
706 };
707
708 let mgr = unsafe { wayland_color::WaylandColorManager::try_new(display_ptr, surface_ptr) };
709 let compositor_caps = mgr
710 .as_ref()
711 .map(|m| m.capabilities())
712 .unwrap_or_else(HostColorCapabilities::srgb_only);
713 let targets = mgr
714 .as_ref()
715 .map(|m| m.preferred_targets())
716 .unwrap_or_default();
717
718 let (format, working_space) =
729 negotiate_output(preferences, &compositor_caps, surface_caps, &targets);
730
731 if std::env::var("DAMASCENE_COLOR_DEBUG").is_ok() {
735 eprintln!(
736 "damascene color: surface formats = {:?}",
737 surface_caps.formats
738 );
739 eprintln!(
740 "damascene color: compositor primaries={:?} transfers={:?} parametric={}",
741 compositor_caps.primaries,
742 compositor_caps.transfer_functions,
743 compositor_caps.parametric_creator(),
744 );
745 eprintln!(
746 "damascene color: preferred targets ref_white={:?} display_peak={:?} preferred_tf={:?} preferred_primaries={:?} indicates_hdr={}",
747 targets.reference_luminance_nits,
748 targets.target_max_luminance_nits,
749 targets.preferred_transfer,
750 targets.preferred_primaries,
751 targets.indicates_hdr(),
752 );
753 let wide = format == wgpu::TextureFormat::Rgba16Float;
754 eprintln!(
755 "damascene color: WSI owns surface color (no attach) — chose {format:?} ({})",
756 if wide {
757 "scRGB extended-range HDR"
758 } else {
759 "sRGB baseline"
760 },
761 );
762 }
763
764 let status = if mgr.is_some() {
772 ColorManagementStatus::Available {
773 capabilities: compositor_caps,
774 attached: None,
775 targets,
776 }
777 } else {
778 ColorManagementStatus::Unavailable
779 };
780 ColorSetup {
787 format,
788 working_space,
789 status,
790 }
791}
792
793#[cfg(all(target_os = "linux", feature = "wayland-color-management"))]
795struct ColorSetup {
796 format: wgpu::TextureFormat,
797 working_space: damascene_core::color::ColorSpace,
798 status: ColorManagementStatus,
799}
800
801#[cfg(all(target_os = "linux", feature = "wayland-color-management"))]
802impl ColorSetup {
803 fn srgb_unavailable(surface_caps: &wgpu::SurfaceCapabilities) -> Self {
804 Self {
805 format: srgb_format(surface_caps),
806 working_space: damascene_core::color::ColorSpace::SRGB_LINEAR,
807 status: ColorManagementStatus::Unavailable,
808 }
809 }
810}
811
812#[cfg(target_os = "android")]
813fn safe_area_for_window(window: &Window, surface_size: (u32, u32), scale_factor: f32) -> Sides {
814 let rect = window.content_rect();
815 if rect.right <= rect.left || rect.bottom <= rect.top || scale_factor <= 0.0 {
816 return Sides::default();
817 }
818 let (surface_w, surface_h) = (surface_size.0 as i32, surface_size.1 as i32);
819 Sides {
820 left: rect.left.max(0) as f32 / scale_factor,
821 top: rect.top.max(0) as f32 / scale_factor,
822 right: (surface_w - rect.right).max(0) as f32 / scale_factor,
823 bottom: (surface_h - rect.bottom).max(0) as f32 / scale_factor,
824 }
825}
826
827#[cfg(not(target_os = "android"))]
828fn safe_area_for_window(_window: &Window, _surface_size: (u32, u32), _scale_factor: f32) -> Sides {
829 Sides::default()
830}
831
832#[cfg(any(target_os = "android", target_os = "ios"))]
833fn sync_mobile_ime(window: &Window, renderer: &Runner, ime_allowed: &mut bool) {
834 let allowed = renderer.focused_captures_keys();
835 if allowed != *ime_allowed {
836 window.set_ime_allowed(allowed);
837 *ime_allowed = allowed;
838 }
839}
840
841impl<A: WinitWgpuApp> ApplicationHandler for Host<A> {
842 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
843 if self.gfx.is_some() {
844 return;
845 }
846 let attrs = Window::default_attributes()
847 .with_title(self.title)
848 .with_inner_size(PhysicalSize::new(
849 self.viewport.w as u32,
850 self.viewport.h as u32,
851 ));
852 #[cfg(target_os = "linux")]
853 let attrs = if let Some(app_id) = self.config.app_id.as_deref() {
854 use winit::platform::wayland::WindowAttributesExtWayland;
856 use winit::platform::x11::WindowAttributesExtX11;
857 let a = WindowAttributesExtWayland::with_name(attrs, app_id, "");
858 WindowAttributesExtX11::with_name(a, app_id, app_id)
859 } else {
860 attrs
861 };
862 let window = Arc::new(event_loop.create_window(attrs).expect("create window"));
863
864 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
865 let surface = instance
866 .create_surface(window.clone())
867 .expect("create surface");
868
869 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
870 power_preference: wgpu::PowerPreference::default(),
871 compatible_surface: Some(&surface),
872 force_fallback_adapter: false,
873 }))
874 .expect("no compatible adapter");
875 self.backend = backend_label(adapter.get_info().backend);
876
877 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
878 label: Some("damascene_winit_wgpu::device"),
879 required_features: wgpu::Features::empty(),
880 required_limits: wgpu::Limits::default(),
881 experimental_features: wgpu::ExperimentalFeatures::default(),
882 memory_hints: wgpu::MemoryHints::Performance,
883 trace: wgpu::Trace::Off,
884 }))
885 .expect("request_device");
886
887 let size = window.inner_size();
888 let surface_caps = surface.get_capabilities(&adapter);
889
890 #[cfg(all(target_os = "linux", feature = "wayland-color-management"))]
897 let (format, working_space, color_management) = {
898 let setup = negotiate_color(&window, &self.config.color_preferences, &surface_caps);
899 (setup.format, setup.working_space, setup.status)
900 };
901 #[cfg(not(all(target_os = "linux", feature = "wayland-color-management")))]
902 let (format, working_space, color_management) = (
903 srgb_format(&surface_caps),
904 damascene_core::color::ColorSpace::SRGB_LINEAR,
905 ColorManagementStatus::Unavailable,
906 );
907
908 let mode_override = std::env::var("DAMASCENE_PRESENT_MODE").ok();
918 let prefer_mailbox =
919 self.config.low_latency_present || mode_override.as_deref() == Some("mailbox");
920 let prefer_immediate = mode_override.as_deref() == Some("immediate");
921 let prefer_fifo = mode_override.as_deref() == Some("fifo");
922 let present_mode = if prefer_immediate
923 && surface_caps
924 .present_modes
925 .contains(&wgpu::PresentMode::Immediate)
926 {
927 wgpu::PresentMode::Immediate
928 } else if prefer_mailbox
929 && !prefer_fifo
930 && surface_caps
931 .present_modes
932 .contains(&wgpu::PresentMode::Mailbox)
933 {
934 wgpu::PresentMode::Mailbox
935 } else if surface_caps
936 .present_modes
937 .contains(&wgpu::PresentMode::Fifo)
938 {
939 wgpu::PresentMode::Fifo
940 } else {
941 surface_caps.present_modes[0]
942 };
943 let config = wgpu::SurfaceConfiguration {
944 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
949 format,
950 width: size.width.max(1),
951 height: size.height.max(1),
952 present_mode,
953 alpha_mode: surface_caps.alpha_modes[0],
954 view_formats: vec![],
955 desired_maximum_frame_latency: 1,
964 };
965 surface.configure(&device, &config);
966
967 let sample_count = self.config.sample_count.max(1);
968 let mut renderer = Runner::with_sample_count(&device, &queue, format, sample_count);
969 renderer.set_theme(self.app.theme());
970 renderer.set_surface_size(config.width, config.height);
971 renderer.set_working_color_space(working_space);
976 renderer.warm_default_glyphs();
981 for s in self.app.shaders() {
984 renderer.register_shader_with(
985 &device,
986 s.name,
987 s.wgsl,
988 s.samples_backdrop,
989 s.samples_time,
990 );
991 }
992
993 let msaa = (sample_count > 1)
994 .then(|| MsaaTarget::new(&device, format, surface_extent(&config), sample_count));
995
996 let surface_color = build_surface_color_info(
997 &adapter,
998 &surface_caps,
999 format,
1000 present_mode,
1001 config.alpha_mode,
1002 );
1003
1004 self.gfx = Some(Gfx {
1005 color_management,
1006 surface_color,
1007 renderer,
1008 surface,
1009 queue,
1010 device,
1011 window,
1012 config,
1013 msaa,
1014 });
1015 let gfx = self.gfx.as_ref().unwrap();
1021 self.app.gpu_setup(&gfx.device, &gfx.queue);
1022 self.next_periodic_redraw = self
1023 .config
1024 .redraw_interval
1025 .map(|interval| Instant::now() + interval);
1026 gfx.window.request_redraw();
1027 }
1028
1029 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
1030 #[cfg(target_os = "android")]
1031 {
1032 self.gfx.take();
1038 self.pending_resize = None;
1039 self.last_pointer = None;
1040 self.last_frame_at = None;
1041 self.next_periodic_redraw = None;
1042 self.ime_allowed = false;
1043 }
1044 }
1045
1046 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
1047 match event {
1048 WindowEvent::CloseRequested => {
1049 self.gfx.take();
1050 event_loop.exit();
1051 }
1052
1053 event => {
1054 let Some(gfx) = self.gfx.as_mut() else {
1055 return;
1056 };
1057 let scale = gfx.window.scale_factor() as f32;
1058
1059 match event {
1060 WindowEvent::Resized(size) => {
1061 let w = size.width.max(1);
1062 let h = size.height.max(1);
1063 let already_pending = self
1068 .pending_resize
1069 .map(|s| s.width == w && s.height == h)
1070 .unwrap_or(false);
1071 let same_as_current = self.pending_resize.is_none()
1072 && w == gfx.config.width
1073 && h == gfx.config.height;
1074 if already_pending || same_as_current {
1075 return;
1076 }
1077 self.pending_resize = Some(PhysicalSize::new(w, h));
1078 self.next_trigger = FrameTrigger::Resize;
1079 gfx.window.request_redraw();
1080 }
1081
1082 WindowEvent::CursorMoved { position, .. } => {
1083 let lx = position.x as f32 / scale;
1084 let ly = position.y as f32 / scale;
1085 self.last_pointer = Some((lx, ly));
1086 let moved = gfx.renderer.pointer_moved(Pointer::moving(lx, ly));
1087 for event in moved.events {
1088 dispatch_app_event(
1089 &mut self.app,
1090 event,
1091 &gfx.renderer,
1092 &mut self.clipboard,
1093 &mut self.last_primary,
1094 );
1095 }
1096 if moved.needs_redraw {
1103 self.next_trigger = FrameTrigger::Pointer;
1104 gfx.window.request_redraw();
1105 }
1106 }
1107
1108 WindowEvent::CursorLeft { .. } => {
1109 self.last_pointer = None;
1110 for event in gfx.renderer.pointer_left() {
1111 dispatch_app_event(
1112 &mut self.app,
1113 event,
1114 &gfx.renderer,
1115 &mut self.clipboard,
1116 &mut self.last_primary,
1117 );
1118 }
1119 self.next_trigger = FrameTrigger::Pointer;
1120 gfx.window.request_redraw();
1121 }
1122
1123 WindowEvent::HoveredFile(path) => {
1124 let (lx, ly) = self.last_pointer.unwrap_or((0.0, 0.0));
1129 for event in gfx.renderer.file_hovered(path, lx, ly) {
1130 dispatch_app_event(
1131 &mut self.app,
1132 event,
1133 &gfx.renderer,
1134 &mut self.clipboard,
1135 &mut self.last_primary,
1136 );
1137 }
1138 self.next_trigger = FrameTrigger::Pointer;
1139 gfx.window.request_redraw();
1140 }
1141
1142 WindowEvent::HoveredFileCancelled => {
1143 for event in gfx.renderer.file_hover_cancelled() {
1144 dispatch_app_event(
1145 &mut self.app,
1146 event,
1147 &gfx.renderer,
1148 &mut self.clipboard,
1149 &mut self.last_primary,
1150 );
1151 }
1152 self.next_trigger = FrameTrigger::Pointer;
1153 gfx.window.request_redraw();
1154 }
1155
1156 WindowEvent::DroppedFile(path) => {
1157 let (lx, ly) = self.last_pointer.unwrap_or((0.0, 0.0));
1158 for event in gfx.renderer.file_dropped(path, lx, ly) {
1159 dispatch_app_event(
1160 &mut self.app,
1161 event,
1162 &gfx.renderer,
1163 &mut self.clipboard,
1164 &mut self.last_primary,
1165 );
1166 }
1167 self.next_trigger = FrameTrigger::Pointer;
1168 gfx.window.request_redraw();
1169 }
1170
1171 WindowEvent::MouseInput { state, button, .. } => {
1172 let Some(button) = pointer_button(button) else {
1173 return;
1174 };
1175 let Some((lx, ly)) = self.last_pointer else {
1176 return;
1177 };
1178 match state {
1179 ElementState::Pressed => {
1180 for event in
1181 gfx.renderer.pointer_down(Pointer::mouse(lx, ly, button))
1182 {
1183 dispatch_app_event(
1184 &mut self.app,
1185 event,
1186 &gfx.renderer,
1187 &mut self.clipboard,
1188 &mut self.last_primary,
1189 );
1190 }
1191 #[cfg(any(target_os = "android", target_os = "ios"))]
1192 sync_mobile_ime(&gfx.window, &gfx.renderer, &mut self.ime_allowed);
1193 self.next_trigger = FrameTrigger::Pointer;
1194 gfx.window.request_redraw();
1195 }
1196 ElementState::Released => {
1197 for event in gfx.renderer.pointer_up(Pointer::mouse(lx, ly, button))
1198 {
1199 let event =
1200 attach_primary_selection_text(event, &mut self.clipboard);
1201 dispatch_app_event(
1202 &mut self.app,
1203 event,
1204 &gfx.renderer,
1205 &mut self.clipboard,
1206 &mut self.last_primary,
1207 );
1208 }
1209 self.next_trigger = FrameTrigger::Pointer;
1210 gfx.window.request_redraw();
1211 }
1212 }
1213 }
1214
1215 WindowEvent::MouseWheel { delta, .. } => {
1216 let Some((lx, ly)) = self.last_pointer else {
1217 return;
1218 };
1219 let dy = match delta {
1223 MouseScrollDelta::LineDelta(_, y) => -y * 50.0,
1224 MouseScrollDelta::PixelDelta(p) => -(p.y as f32) / scale,
1225 };
1226 let mut needs_redraw = false;
1227 let consumed = if let Some(event) =
1228 gfx.renderer.pointer_wheel_event(lx, ly, 0.0, dy)
1229 {
1230 needs_redraw = true;
1231 dispatch_app_wheel_event(
1232 &mut self.app,
1233 event,
1234 &gfx.renderer,
1235 &mut self.clipboard,
1236 &mut self.last_primary,
1237 )
1238 } else {
1239 false
1240 };
1241 if !consumed && gfx.renderer.pointer_wheel(lx, ly, dy) {
1242 needs_redraw = true;
1243 }
1244 if needs_redraw {
1245 self.next_trigger = FrameTrigger::Pointer;
1246 gfx.window.request_redraw();
1247 }
1248 }
1249
1250 WindowEvent::ModifiersChanged(modifiers) => {
1251 self.modifiers = key_modifiers(modifiers.state());
1252 gfx.renderer.set_modifiers(self.modifiers);
1253 }
1254
1255 WindowEvent::KeyboardInput {
1256 event:
1257 key_event @ winit::event::KeyEvent {
1258 state: ElementState::Pressed,
1259 ..
1260 },
1261 is_synthetic: false,
1262 ..
1263 } => {
1264 if let Some(key) = map_key(&key_event.logical_key) {
1265 for event in
1266 gfx.renderer.key_down(key, self.modifiers, key_event.repeat)
1267 {
1268 match text_input::clipboard_request(&event) {
1269 Some(ClipboardKind::Copy) => {
1270 copy_current_selection(&gfx.renderer, &mut self.clipboard);
1271 dispatch_app_event(
1272 &mut self.app,
1273 event,
1274 &gfx.renderer,
1275 &mut self.clipboard,
1276 &mut self.last_primary,
1277 );
1278 }
1279 Some(ClipboardKind::Cut) => {
1280 copy_current_selection(&gfx.renderer, &mut self.clipboard);
1281 let delete = clipboard::delete_selection_event(event);
1282 dispatch_app_event(
1283 &mut self.app,
1284 delete,
1285 &gfx.renderer,
1286 &mut self.clipboard,
1287 &mut self.last_primary,
1288 );
1289 }
1290 Some(ClipboardKind::Paste) => {
1291 if let Some(paste) = paste_text_from_clipboard(
1292 event.clone(),
1293 &mut self.clipboard,
1294 ) {
1295 dispatch_app_event(
1296 &mut self.app,
1297 paste,
1298 &gfx.renderer,
1299 &mut self.clipboard,
1300 &mut self.last_primary,
1301 );
1302 } else {
1303 dispatch_app_event(
1304 &mut self.app,
1305 event,
1306 &gfx.renderer,
1307 &mut self.clipboard,
1308 &mut self.last_primary,
1309 );
1310 }
1311 }
1312 None => dispatch_app_event(
1313 &mut self.app,
1314 event,
1315 &gfx.renderer,
1316 &mut self.clipboard,
1317 &mut self.last_primary,
1318 ),
1319 }
1320 }
1321 }
1322 if let Some(text) = &key_event.text
1327 && let Some(event) = gfx.renderer.text_input(text.to_string())
1328 {
1329 dispatch_app_event(
1330 &mut self.app,
1331 event,
1332 &gfx.renderer,
1333 &mut self.clipboard,
1334 &mut self.last_primary,
1335 );
1336 }
1337 self.next_trigger = FrameTrigger::Keyboard;
1338 gfx.window.request_redraw();
1339 }
1340 WindowEvent::Ime(winit::event::Ime::Commit(text)) => {
1341 if let Some(event) = gfx.renderer.text_input(text) {
1342 dispatch_app_event(
1343 &mut self.app,
1344 event,
1345 &gfx.renderer,
1346 &mut self.clipboard,
1347 &mut self.last_primary,
1348 );
1349 }
1350 self.next_trigger = FrameTrigger::Keyboard;
1351 gfx.window.request_redraw();
1352 }
1353
1354 WindowEvent::Touch(touch) => {
1355 let lx = touch.location.x as f32 / scale;
1356 let ly = touch.location.y as f32 / scale;
1357 self.last_pointer = Some((lx, ly));
1358 let mut pointer = Pointer::touch(
1359 lx,
1360 ly,
1361 PointerButton::Primary,
1362 damascene_core::PointerId(touch.id as u32),
1363 );
1364 pointer.pressure = touch_pressure(touch.force);
1365 match touch.phase {
1366 TouchPhase::Started => {
1367 for event in gfx.renderer.pointer_down(pointer) {
1368 dispatch_app_event(
1369 &mut self.app,
1370 event,
1371 &gfx.renderer,
1372 &mut self.clipboard,
1373 &mut self.last_primary,
1374 );
1375 }
1376 #[cfg(any(target_os = "android", target_os = "ios"))]
1377 sync_mobile_ime(&gfx.window, &gfx.renderer, &mut self.ime_allowed);
1378 }
1379 TouchPhase::Moved => {
1380 let moved = gfx.renderer.pointer_moved(pointer);
1381 for event in moved.events {
1382 dispatch_app_event(
1383 &mut self.app,
1384 event,
1385 &gfx.renderer,
1386 &mut self.clipboard,
1387 &mut self.last_primary,
1388 );
1389 }
1390 if !moved.needs_redraw {
1391 return;
1392 }
1393 }
1394 TouchPhase::Ended => {
1395 for event in gfx.renderer.pointer_up(pointer) {
1396 dispatch_app_event(
1397 &mut self.app,
1398 event,
1399 &gfx.renderer,
1400 &mut self.clipboard,
1401 &mut self.last_primary,
1402 );
1403 }
1404 self.last_pointer = None;
1405 }
1406 TouchPhase::Cancelled => {
1407 for event in gfx.renderer.pointer_left() {
1408 dispatch_app_event(
1409 &mut self.app,
1410 event,
1411 &gfx.renderer,
1412 &mut self.clipboard,
1413 &mut self.last_primary,
1414 );
1415 }
1416 self.last_pointer = None;
1417 }
1418 }
1419 self.next_trigger = FrameTrigger::Pointer;
1420 gfx.window.request_redraw();
1421 }
1422
1423 WindowEvent::RedrawRequested => {
1424 for event in gfx.renderer.poll_input(Instant::now()) {
1433 self.app.on_event(event);
1434 }
1435 if let Some(size) = self.pending_resize.take() {
1440 gfx.config.width = size.width;
1441 gfx.config.height = size.height;
1442 gfx.surface.configure(&gfx.device, &gfx.config);
1443 gfx.renderer
1444 .set_surface_size(gfx.config.width, gfx.config.height);
1445 let extent = surface_extent(&gfx.config);
1446 if let Some(msaa) = gfx.msaa.as_mut()
1447 && !msaa.matches(extent)
1448 {
1449 *msaa = MsaaTarget::new(
1450 &gfx.device,
1451 gfx.config.format,
1452 extent,
1453 msaa.sample_count,
1454 );
1455 }
1456 }
1457 let frame = match gfx.surface.get_current_texture() {
1458 wgpu::CurrentSurfaceTexture::Success(t)
1459 | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
1460 wgpu::CurrentSurfaceTexture::Lost
1461 | wgpu::CurrentSurfaceTexture::Outdated => {
1462 gfx.surface.configure(&gfx.device, &gfx.config);
1471 gfx.window.request_redraw();
1472 return;
1473 }
1474 other => {
1475 eprintln!("surface unavailable: {other:?}");
1476 return;
1477 }
1478 };
1479 let view = frame
1480 .texture
1481 .create_view(&wgpu::TextureViewDescriptor::default());
1482
1483 let frame_start = Instant::now();
1493 let last_frame_dt = self
1494 .last_frame_at
1495 .map(|t| frame_start.duration_since(t))
1496 .unwrap_or(Duration::ZERO);
1497 self.last_frame_at = Some(frame_start);
1498 let trigger = std::mem::take(&mut self.next_trigger);
1499 let scale_factor = gfx.window.scale_factor() as f32;
1500 let viewport = Rect::new(
1501 0.0,
1502 0.0,
1503 gfx.config.width as f32 / scale_factor,
1504 gfx.config.height as f32 / scale_factor,
1505 );
1506 let paint_only =
1514 trigger == FrameTrigger::ShaderPaint && self.pending_resize.is_none();
1515
1516 let (prepare, palette, t_after_build, t_after_prepare) = if paint_only {
1517 damascene_core::profile_span!("frame::repaint");
1518 let palette = gfx.renderer.theme().palette().clone();
1522 let t_after_build = Instant::now();
1523 let prepare = gfx.renderer.repaint(
1524 &gfx.device,
1525 &gfx.queue,
1526 viewport,
1527 scale_factor,
1528 );
1529 let t_after_prepare = Instant::now();
1530 (prepare, palette, t_after_build, t_after_prepare)
1531 } else {
1532 let msaa_samples =
1533 gfx.msaa.as_ref().map(|m| m.sample_count).unwrap_or(1);
1534 self.frame_index = self.frame_index.wrapping_add(1);
1535 let diagnostics = HostDiagnostics {
1536 backend: self.backend,
1537 surface_size: (gfx.config.width, gfx.config.height),
1538 scale_factor,
1539 msaa_samples,
1540 frame_index: self.frame_index,
1541 last_frame_dt,
1542 last_build: self.last_build,
1543 last_prepare: self.last_prepare,
1544 last_layout: self.last_layout,
1545 last_layout_intrinsic_cache_hits: self
1546 .last_layout_intrinsic_cache_hits,
1547 last_layout_intrinsic_cache_misses: self
1548 .last_layout_intrinsic_cache_misses,
1549 last_layout_pruned_subtrees: self.last_layout_pruned_subtrees,
1550 last_layout_pruned_nodes: self.last_layout_pruned_nodes,
1551 last_draw_ops: self.last_draw_ops,
1552 last_draw_ops_culled_text_ops: self.last_draw_ops_culled_text_ops,
1553 last_paint: self.last_paint,
1554 last_paint_culled_ops: self.last_paint_culled_ops,
1555 last_gpu_upload: self.last_gpu_upload,
1556 last_snapshot: self.last_snapshot,
1557 last_submit: self.last_submit,
1558 last_text_layout_cache_hits: self.last_text_layout_cache_hits,
1559 last_text_layout_cache_misses: self.last_text_layout_cache_misses,
1560 last_text_layout_cache_evictions: self
1561 .last_text_layout_cache_evictions,
1562 last_text_layout_shaped_bytes: self.last_text_layout_shaped_bytes,
1563 trigger,
1564 working_color_space: gfx.renderer.working_color_space(),
1565 color_management: gfx.color_management.clone(),
1566 surface_color: Some(gfx.surface_color.clone()),
1567 };
1568 let (mut tree, palette) = {
1569 damascene_core::profile_span!("frame::build");
1570 self.app.before_paint(&gfx.queue);
1571 WinitWgpuApp::before_build(&mut self.app);
1572 let theme = self.app.theme();
1573 let palette = theme.palette().clone();
1574 let cx = damascene_core::BuildCx::new(&theme)
1575 .with_ui_state(gfx.renderer.ui_state())
1576 .with_diagnostics(&diagnostics)
1577 .with_viewport(viewport.w, viewport.h)
1578 .with_safe_area(safe_area_for_window(
1579 &gfx.window,
1580 (gfx.config.width, gfx.config.height),
1581 scale_factor,
1582 ));
1583 let tree = self.app.build(&cx);
1584 gfx.renderer.set_theme(theme);
1585 gfx.renderer.set_hotkeys(self.app.hotkeys());
1586 gfx.renderer.set_selection(self.app.selection());
1587 gfx.renderer.push_toasts(self.app.drain_toasts());
1588 gfx.renderer
1589 .push_focus_requests(self.app.drain_focus_requests());
1590 gfx.renderer
1591 .push_scroll_requests(self.app.drain_scroll_requests());
1592 for url in self.app.drain_link_opens() {
1593 #[cfg(target_os = "android")]
1594 open_link(&self.android_app, &url);
1595 #[cfg(not(any(target_os = "android", target_os = "ios")))]
1596 open_link(&url);
1597 #[cfg(target_os = "ios")]
1598 open_link(&url);
1599 }
1600 (tree, palette)
1601 };
1602 let t_after_build = Instant::now();
1603 let prepare = {
1604 damascene_core::profile_span!("frame::prepare");
1605 gfx.renderer.prepare(
1606 &gfx.device,
1607 &gfx.queue,
1608 &mut tree,
1609 viewport,
1610 scale_factor,
1611 )
1612 };
1613 #[cfg(any(target_os = "android", target_os = "ios"))]
1614 sync_mobile_ime(&gfx.window, &gfx.renderer, &mut self.ime_allowed);
1615 let t_after_prepare = Instant::now();
1616 let cursor = gfx.renderer.ui_state().cursor(&tree);
1621 if cursor != self.last_cursor {
1622 gfx.window.set_cursor(winit_cursor(cursor));
1623 self.last_cursor = cursor;
1624 }
1625 (prepare, palette, t_after_build, t_after_prepare)
1626 };
1627
1628 {
1629 damascene_core::profile_span!("frame::submit");
1630 let mut encoder = gfx.device.create_command_encoder(
1631 &wgpu::CommandEncoderDescriptor {
1632 label: Some("damascene_winit_wgpu::encoder"),
1633 },
1634 );
1635 gfx.renderer.render(
1641 &gfx.device,
1642 &mut encoder,
1643 &frame.texture,
1644 &view,
1645 gfx.msaa.as_ref().map(|msaa| &msaa.view),
1646 wgpu::LoadOp::Clear(bg_color(&palette)),
1647 );
1648 gfx.queue.submit(Some(encoder.finish()));
1649 frame.present();
1650 let t_after_submit = Instant::now();
1651 self.last_build = t_after_build - frame_start;
1652 self.last_prepare = t_after_prepare - t_after_build;
1653 self.last_submit = t_after_submit - t_after_prepare;
1654 self.last_layout = prepare.timings.layout;
1655 self.last_layout_intrinsic_cache_hits =
1656 prepare.timings.layout_intrinsic_cache.hits;
1657 self.last_layout_intrinsic_cache_misses =
1658 prepare.timings.layout_intrinsic_cache.misses;
1659 self.last_layout_pruned_subtrees =
1660 prepare.timings.layout_prune.subtrees;
1661 self.last_layout_pruned_nodes = prepare.timings.layout_prune.nodes;
1662 self.last_draw_ops = prepare.timings.draw_ops;
1663 self.last_draw_ops_culled_text_ops =
1664 prepare.timings.draw_ops_culled_text_ops;
1665 self.last_paint = prepare.timings.paint;
1666 self.last_paint_culled_ops = prepare.timings.paint_culled_ops;
1667 self.last_gpu_upload = prepare.timings.gpu_upload;
1668 self.last_snapshot = prepare.timings.snapshot;
1669 self.last_text_layout_cache_hits =
1670 prepare.timings.text_layout_cache.hits;
1671 self.last_text_layout_cache_misses =
1672 prepare.timings.text_layout_cache.misses;
1673 self.last_text_layout_cache_evictions =
1674 prepare.timings.text_layout_cache.evictions;
1675 self.last_text_layout_shaped_bytes =
1676 prepare.timings.text_layout_cache.shaped_bytes;
1677 }
1678
1679 let now = Instant::now();
1697 if !paint_only {
1698 match prepare.next_layout_redraw_in {
1699 None => self.next_layout_redraw = None,
1700 Some(d) if d.is_zero() => {
1701 self.next_layout_redraw = None;
1702 self.next_trigger = FrameTrigger::Animation;
1703 gfx.window.request_redraw();
1704 }
1705 Some(d) => self.next_layout_redraw = Some(now + d),
1706 }
1707 }
1708 match prepare.next_paint_redraw_in {
1709 None => self.next_paint_redraw = None,
1710 Some(d) if d.is_zero() => {
1711 self.next_paint_redraw = None;
1715 if !matches!(self.next_trigger, FrameTrigger::Animation) {
1716 self.next_trigger = FrameTrigger::ShaderPaint;
1717 }
1718 gfx.window.request_redraw();
1719 }
1720 Some(d) => self.next_paint_redraw = Some(now + d),
1721 }
1722 }
1723 _ => {}
1724 }
1725 }
1726 }
1727 }
1728
1729 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
1730 let Some(gfx) = self.gfx.as_ref() else {
1731 event_loop.set_control_flow(ControlFlow::Wait);
1732 return;
1733 };
1734
1735 let now = Instant::now();
1736
1737 if let Some(interval) = self.config.redraw_interval {
1743 let next = self
1744 .next_periodic_redraw
1745 .get_or_insert_with(|| now + interval);
1746 if now >= *next {
1747 self.next_trigger = FrameTrigger::Periodic;
1748 gfx.window.request_redraw();
1749 *next = now + interval;
1750 }
1751 }
1752
1753 let mut wake_up = self.next_periodic_redraw;
1760 if let Some(t) = self.next_layout_redraw {
1761 if now >= t {
1762 self.next_trigger = FrameTrigger::Animation;
1763 gfx.window.request_redraw();
1764 self.next_layout_redraw = None;
1765 } else {
1766 wake_up = Some(match wake_up {
1767 Some(p) => p.min(t),
1768 None => t,
1769 });
1770 }
1771 }
1772 if let Some(t) = self.next_paint_redraw {
1773 if now >= t {
1774 if !matches!(self.next_trigger, FrameTrigger::Animation) {
1778 self.next_trigger = FrameTrigger::ShaderPaint;
1779 }
1780 gfx.window.request_redraw();
1781 self.next_paint_redraw = None;
1782 } else {
1783 wake_up = Some(match wake_up {
1784 Some(p) => p.min(t),
1785 None => t,
1786 });
1787 }
1788 }
1789
1790 match wake_up {
1791 Some(t) => event_loop.set_control_flow(ControlFlow::WaitUntil(t)),
1792 None => event_loop.set_control_flow(ControlFlow::Wait),
1793 }
1794 }
1795}
1796
1797fn map_key(key: &Key) -> Option<UiKey> {
1798 match key {
1799 Key::Named(NamedKey::Enter) => Some(UiKey::Enter),
1800 Key::Named(NamedKey::Escape) => Some(UiKey::Escape),
1801 Key::Named(NamedKey::Tab) => Some(UiKey::Tab),
1802 Key::Named(NamedKey::Space) => Some(UiKey::Space),
1803 Key::Named(NamedKey::ArrowUp) => Some(UiKey::ArrowUp),
1804 Key::Named(NamedKey::ArrowDown) => Some(UiKey::ArrowDown),
1805 Key::Named(NamedKey::ArrowLeft) => Some(UiKey::ArrowLeft),
1806 Key::Named(NamedKey::ArrowRight) => Some(UiKey::ArrowRight),
1807 Key::Named(NamedKey::Backspace) => Some(UiKey::Backspace),
1808 Key::Named(NamedKey::Delete) => Some(UiKey::Delete),
1809 Key::Named(NamedKey::Home) => Some(UiKey::Home),
1810 Key::Named(NamedKey::End) => Some(UiKey::End),
1811 Key::Named(NamedKey::PageUp) => Some(UiKey::PageUp),
1812 Key::Named(NamedKey::PageDown) => Some(UiKey::PageDown),
1813 Key::Character(s) => Some(UiKey::Character(s.to_string())),
1814 Key::Named(named) => Some(UiKey::Other(format!("{named:?}"))),
1815 _ => None,
1816 }
1817}
1818
1819fn pointer_button(b: MouseButton) -> Option<PointerButton> {
1820 match b {
1821 MouseButton::Left => Some(PointerButton::Primary),
1822 MouseButton::Right => Some(PointerButton::Secondary),
1823 MouseButton::Middle => Some(PointerButton::Middle),
1824 _ => None,
1827 }
1828}
1829
1830#[cfg(not(any(target_os = "android", target_os = "ios")))]
1831fn new_clipboard() -> PlatformClipboard {
1832 arboard::Clipboard::new().ok()
1833}
1834
1835#[cfg(target_os = "ios")]
1836fn new_clipboard() -> PlatformClipboard {
1837 PlatformClipboard
1838}
1839
1840#[cfg(target_os = "android")]
1841fn new_clipboard(app: &AndroidApp) -> PlatformClipboard {
1842 PlatformClipboard { app: app.clone() }
1843}
1844
1845#[cfg(not(any(target_os = "android", target_os = "ios")))]
1850fn open_link(url: &str) {
1851 if let Err(err) = open::that_detached(url) {
1852 eprintln!("damascene-winit-wgpu: failed to open {url}: {err}");
1853 }
1854}
1855
1856#[cfg(target_os = "ios")]
1857fn open_link(url: &str) {
1858 eprintln!("damascene-winit-wgpu: opening links is not wired on iOS yet: {url}");
1859}
1860
1861#[cfg(target_os = "android")]
1862fn open_link(app: &AndroidApp, url: &str) {
1863 let app_for_thread = app.clone();
1864 let url = url.to_string();
1865 app.run_on_java_main_thread(Box::new(move || {
1866 let result = (|| -> jni::errors::Result<()> {
1867 let jvm = unsafe { jni::JavaVM::from_raw(app_for_thread.vm_as_ptr().cast()) };
1868 jvm.attach_current_thread(|env| {
1869 let url = env.new_string(&url)?;
1870 let uri = env
1871 .call_static_method(
1872 jni::jni_str!("android/net/Uri"),
1873 jni::jni_str!("parse"),
1874 jni::jni_sig!("(Ljava/lang/String;)Landroid/net/Uri;"),
1875 &[jni::JValue::Object(url.as_ref())],
1876 )?
1877 .l()?;
1878 let action = env
1879 .get_static_field(
1880 jni::jni_str!("android/content/Intent"),
1881 jni::jni_str!("ACTION_VIEW"),
1882 jni::jni_sig!("Ljava/lang/String;"),
1883 )?
1884 .l()?;
1885 let intent = env.new_object(
1886 jni::jni_str!("android/content/Intent"),
1887 jni::jni_sig!("(Ljava/lang/String;Landroid/net/Uri;)V"),
1888 &[jni::JValue::Object(&action), jni::JValue::Object(&uri)],
1889 )?;
1890 let activity = unsafe {
1891 jni::objects::JObject::from_raw(
1892 env,
1893 app_for_thread.activity_as_ptr() as jni::sys::jobject,
1894 )
1895 };
1896 env.call_method(
1897 &activity,
1898 jni::jni_str!("startActivity"),
1899 jni::jni_sig!("(Landroid/content/Intent;)V"),
1900 &[jni::JValue::Object(&intent)],
1901 )?;
1902 Ok(())
1903 })
1904 })();
1905 if let Err(err) = result {
1906 eprintln!("damascene-winit-wgpu: failed to open link on Android: {err}");
1907 }
1908 }));
1909}
1910
1911fn touch_pressure(force: Option<Force>) -> Option<f32> {
1912 match force? {
1913 Force::Calibrated {
1914 force,
1915 max_possible_force,
1916 ..
1917 } if max_possible_force > 0.0 => Some((force / max_possible_force).clamp(0.0, 1.0) as f32),
1918 Force::Calibrated { force, .. } => Some(force.clamp(0.0, 1.0) as f32),
1919 Force::Normalized(v) => Some(v.clamp(0.0, 1.0) as f32),
1920 }
1921}
1922
1923fn winit_cursor(cursor: Cursor) -> CursorIcon {
1929 match cursor {
1930 Cursor::Default => CursorIcon::Default,
1931 Cursor::Pointer => CursorIcon::Pointer,
1932 Cursor::Text => CursorIcon::Text,
1933 Cursor::NotAllowed => CursorIcon::NotAllowed,
1934 Cursor::Grab => CursorIcon::Grab,
1935 Cursor::Grabbing => CursorIcon::Grabbing,
1936 Cursor::Move => CursorIcon::Move,
1937 Cursor::EwResize => CursorIcon::EwResize,
1938 Cursor::NsResize => CursorIcon::NsResize,
1939 Cursor::NwseResize => CursorIcon::NwseResize,
1940 Cursor::NeswResize => CursorIcon::NeswResize,
1941 Cursor::ColResize => CursorIcon::ColResize,
1942 Cursor::RowResize => CursorIcon::RowResize,
1943 Cursor::Crosshair => CursorIcon::Crosshair,
1944 _ => CursorIcon::Default,
1945 }
1946}
1947
1948fn key_modifiers(mods: winit::keyboard::ModifiersState) -> KeyModifiers {
1949 KeyModifiers {
1950 shift: mods.shift_key(),
1951 ctrl: mods.control_key(),
1952 alt: mods.alt_key(),
1953 logo: mods.super_key(),
1954 }
1955}
1956
1957fn bg_color(palette: &damascene_core::Palette) -> wgpu::Color {
1958 let c = palette.background;
1959 wgpu::Color {
1960 r: srgb_to_linear(c.r as f64 / 255.0),
1961 g: srgb_to_linear(c.g as f64 / 255.0),
1962 b: srgb_to_linear(c.b as f64 / 255.0),
1963 a: c.a as f64 / 255.0,
1964 }
1965}
1966
1967fn copy_current_selection(renderer: &Runner, clipboard: &mut PlatformClipboard) {
1968 let Some(text) = renderer.selected_text() else {
1972 return;
1973 };
1974 set_clipboard_text(clipboard, text);
1975}
1976
1977fn dispatch_app_event<A: App>(
1978 app: &mut A,
1979 event: UiEvent,
1980 renderer: &Runner,
1981 clipboard: &mut PlatformClipboard,
1982 last_primary: &mut String,
1983) {
1984 let before = app.selection();
1985 app.on_event(event);
1986 if app.selection() != before {
1987 sync_primary_selection(&app.selection(), renderer, clipboard, last_primary);
1988 }
1989}
1990
1991fn dispatch_app_wheel_event<A: App>(
1992 app: &mut A,
1993 event: UiEvent,
1994 renderer: &Runner,
1995 clipboard: &mut PlatformClipboard,
1996 last_primary: &mut String,
1997) -> bool {
1998 let before = app.selection();
1999 let consumed = app.on_wheel_event(event);
2000 if app.selection() != before {
2001 sync_primary_selection(&app.selection(), renderer, clipboard, last_primary);
2002 }
2003 consumed
2004}
2005
2006fn sync_primary_selection(
2007 selection: &damascene_core::selection::Selection,
2008 renderer: &Runner,
2009 clipboard: &mut PlatformClipboard,
2010 last_primary: &mut String,
2011) {
2012 let text = renderer
2013 .selected_text_for(selection)
2014 .filter(|s| !s.is_empty())
2015 .unwrap_or_default();
2016 if text == *last_primary {
2017 return;
2018 }
2019 if !text.is_empty() {
2020 primary::set(clipboard, &text);
2021 }
2022 *last_primary = text;
2023}
2024
2025fn paste_text_from_clipboard(event: UiEvent, clipboard: &mut PlatformClipboard) -> Option<UiEvent> {
2026 let text = get_clipboard_text(clipboard)?;
2027 Some(clipboard::paste_text_event(event, text))
2028}
2029
2030fn attach_primary_selection_text(mut event: UiEvent, clipboard: &mut PlatformClipboard) -> UiEvent {
2031 if event.kind == UiEventKind::MiddleClick {
2032 event.text = primary::get(clipboard);
2033 }
2034 event
2035}
2036
2037#[cfg(not(any(target_os = "android", target_os = "ios")))]
2038fn set_clipboard_text(clipboard: &mut PlatformClipboard, text: String) {
2039 if let Some(cb) = clipboard {
2040 let _ = cb.set_text(text);
2041 }
2042}
2043
2044#[cfg(target_os = "ios")]
2045fn set_clipboard_text(_clipboard: &mut PlatformClipboard, _text: String) {}
2046
2047#[cfg(target_os = "android")]
2048fn set_clipboard_text(clipboard: &mut PlatformClipboard, text: String) {
2049 if let Err(err) = set_android_clipboard_text(&clipboard.app, &text) {
2050 eprintln!("damascene-winit-wgpu: failed to set Android clipboard: {err}");
2051 }
2052}
2053
2054#[cfg(not(any(target_os = "android", target_os = "ios")))]
2055fn get_clipboard_text(clipboard: &mut PlatformClipboard) -> Option<String> {
2056 clipboard.as_mut()?.get_text().ok()
2057}
2058
2059#[cfg(target_os = "ios")]
2060fn get_clipboard_text(_clipboard: &mut PlatformClipboard) -> Option<String> {
2061 None
2062}
2063
2064#[cfg(target_os = "android")]
2065fn get_clipboard_text(clipboard: &mut PlatformClipboard) -> Option<String> {
2066 match get_android_clipboard_text(&clipboard.app) {
2067 Ok(text) => text,
2068 Err(err) => {
2069 eprintln!("damascene-winit-wgpu: failed to read Android clipboard: {err}");
2070 None
2071 }
2072 }
2073}
2074
2075#[cfg(target_os = "android")]
2076fn set_android_clipboard_text(app: &AndroidApp, text: &str) -> jni::errors::Result<()> {
2077 use jni::refs::Reference as _;
2078
2079 let jvm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr().cast()) };
2080 jvm.attach_current_thread(|env| {
2081 let activity = unsafe {
2082 jni::objects::JObject::from_raw(env, app.activity_as_ptr() as jni::sys::jobject)
2083 };
2084 let service_name = env.new_string("clipboard")?;
2085 let clipboard = env
2086 .call_method(
2087 &activity,
2088 jni::jni_str!("getSystemService"),
2089 jni::jni_sig!("(Ljava/lang/String;)Ljava/lang/Object;"),
2090 &[jni::JValue::Object(service_name.as_ref())],
2091 )?
2092 .l()?;
2093 if clipboard.is_null() {
2094 return Ok(());
2095 }
2096
2097 let label = env.new_string("Damascene")?;
2098 let text = env.new_string(text)?;
2099 let clip = env
2100 .call_static_method(
2101 jni::jni_str!("android/content/ClipData"),
2102 jni::jni_str!("newPlainText"),
2103 jni::jni_sig!(
2104 "(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Landroid/content/ClipData;"
2105 ),
2106 &[
2107 jni::JValue::Object(label.as_ref()),
2108 jni::JValue::Object(text.as_ref()),
2109 ],
2110 )?
2111 .l()?;
2112 env.call_method(
2113 &clipboard,
2114 jni::jni_str!("setPrimaryClip"),
2115 jni::jni_sig!("(Landroid/content/ClipData;)V"),
2116 &[jni::JValue::Object(&clip)],
2117 )?;
2118 Ok(())
2119 })
2120}
2121
2122#[cfg(target_os = "android")]
2123fn get_android_clipboard_text(app: &AndroidApp) -> jni::errors::Result<Option<String>> {
2124 use jni::refs::Reference as _;
2125
2126 let jvm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr().cast()) };
2127 jvm.attach_current_thread(|env| {
2128 let activity = unsafe {
2129 jni::objects::JObject::from_raw(env, app.activity_as_ptr() as jni::sys::jobject)
2130 };
2131 let service_name = env.new_string("clipboard")?;
2132 let clipboard = env
2133 .call_method(
2134 &activity,
2135 jni::jni_str!("getSystemService"),
2136 jni::jni_sig!("(Ljava/lang/String;)Ljava/lang/Object;"),
2137 &[jni::JValue::Object(service_name.as_ref())],
2138 )?
2139 .l()?;
2140 if clipboard.is_null() {
2141 return Ok(None);
2142 }
2143
2144 let clip = env
2145 .call_method(
2146 &clipboard,
2147 jni::jni_str!("getPrimaryClip"),
2148 jni::jni_sig!("()Landroid/content/ClipData;"),
2149 &[],
2150 )?
2151 .l()?;
2152 if clip.is_null() {
2153 return Ok(None);
2154 }
2155
2156 let item_count = env
2157 .call_method(
2158 &clip,
2159 jni::jni_str!("getItemCount"),
2160 jni::jni_sig!("()I"),
2161 &[],
2162 )?
2163 .i()?;
2164 if item_count <= 0 {
2165 return Ok(None);
2166 }
2167
2168 let item = env
2169 .call_method(
2170 &clip,
2171 jni::jni_str!("getItemAt"),
2172 jni::jni_sig!("(I)Landroid/content/ClipData$Item;"),
2173 &[jni::JValue::Int(0)],
2174 )?
2175 .l()?;
2176 if item.is_null() {
2177 return Ok(None);
2178 }
2179
2180 let text = env
2181 .call_method(
2182 &item,
2183 jni::jni_str!("coerceToText"),
2184 jni::jni_sig!("(Landroid/content/Context;)Ljava/lang/CharSequence;"),
2185 &[jni::JValue::Object(&activity)],
2186 )?
2187 .l()?;
2188 if text.is_null() {
2189 return Ok(None);
2190 }
2191
2192 let text = env
2193 .call_method(
2194 &text,
2195 jni::jni_str!("toString"),
2196 jni::jni_sig!("()Ljava/lang/String;"),
2197 &[],
2198 )?
2199 .l()?;
2200 if text.is_null() {
2201 return Ok(None);
2202 }
2203
2204 let text = env.cast_local::<jni::objects::JString>(text)?;
2205 Ok(Some(text.try_to_string(env)?))
2206 })
2207}
2208
2209mod primary {
2210 #[cfg(target_os = "linux")]
2211 pub fn set(clipboard: &mut super::PlatformClipboard, text: &str) {
2212 use arboard::{LinuxClipboardKind, SetExtLinux};
2213 if let Some(cb) = clipboard {
2214 let _ = cb.set().clipboard(LinuxClipboardKind::Primary).text(text);
2215 }
2216 }
2217
2218 #[cfg(target_os = "linux")]
2219 pub fn get(clipboard: &mut super::PlatformClipboard) -> Option<String> {
2220 use arboard::{GetExtLinux, LinuxClipboardKind};
2221 let cb = clipboard.as_mut()?;
2222 cb.get().clipboard(LinuxClipboardKind::Primary).text().ok()
2223 }
2224
2225 #[cfg(not(target_os = "linux"))]
2226 pub fn set(_clipboard: &mut super::PlatformClipboard, _text: &str) {}
2227
2228 #[cfg(not(target_os = "linux"))]
2229 pub fn get(_clipboard: &mut super::PlatformClipboard) -> Option<String> {
2230 None
2231 }
2232}
2233
2234fn backend_label(backend: wgpu::Backend) -> &'static str {
2240 match backend {
2241 wgpu::Backend::Vulkan => "Vulkan",
2242 wgpu::Backend::Metal => "Metal",
2243 wgpu::Backend::Dx12 => "DX12",
2244 wgpu::Backend::Gl => "GL",
2245 wgpu::Backend::BrowserWebGpu => "WebGPU",
2246 wgpu::Backend::Noop => "noop",
2247 }
2248}
2249
2250fn srgb_to_linear(c: f64) -> f64 {
2253 if c <= 0.04045 {
2254 c / 12.92
2255 } else {
2256 ((c + 0.055) / 1.055).powf(2.4)
2257 }
2258}
2259
2260#[cfg(test)]
2261mod tests {
2262 use super::*;
2263 use damascene_core::Selection;
2264 use damascene_core::SelectionPoint;
2265 use damascene_core::SelectionRange;
2266
2267 #[test]
2274 fn basic_app_forwards_selection_to_inner() {
2275 struct AppWithSelection;
2276 impl App for AppWithSelection {
2277 fn build(&self, _cx: &damascene_core::BuildCx) -> damascene_core::El {
2278 damascene_core::widgets::text::text("hi")
2279 }
2280 fn selection(&self) -> Selection {
2281 Selection {
2282 range: Some(SelectionRange {
2283 anchor: SelectionPoint::new("p", 0),
2284 head: SelectionPoint::new("p", 5),
2285 }),
2286 }
2287 }
2288 }
2289 let basic = BasicApp(AppWithSelection);
2290 let sel = basic.selection();
2291 let r = sel.range.as_ref().expect("range forwarded through wrapper");
2292 assert_eq!(r.anchor.key, "p");
2293 assert_eq!(r.head.byte, 5);
2294 }
2295
2296 #[test]
2297 fn basic_app_forwards_wheel_events_to_inner() {
2298 struct AppWithWheel;
2299 impl App for AppWithWheel {
2300 fn build(&self, _cx: &damascene_core::BuildCx) -> damascene_core::El {
2301 damascene_core::widgets::text::text("hi")
2302 }
2303
2304 fn on_wheel_event(&mut self, event: damascene_core::UiEvent) -> bool {
2305 event.kind == UiEventKind::PointerWheel && event.wheel_dy() == Some(40.0)
2306 }
2307 }
2308
2309 let mut event = UiEvent::synthetic_click("wheel");
2310 event.kind = UiEventKind::PointerWheel;
2311 event.wheel_delta = Some((0.0, 40.0));
2312
2313 let mut basic = BasicApp(AppWithWheel);
2314 assert!(basic.on_wheel_event(event));
2315 }
2316}