diff --git a/Cargo.lock b/Cargo.lock index c78ac1f0cd..5c23ca443f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1316,6 +1316,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber", + "windows 0.60.0", "workspace-hack", ] @@ -9103,8 +9104,6 @@ dependencies = [ [[package]] name = "tao" version = "0.34.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7" dependencies = [ "bitflags 2.9.4", "block2 0.6.1", diff --git a/Cargo.toml b/Cargo.toml index b67052237e..44097ca757 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,6 +108,7 @@ cidre = { git = "https://github.com/CapSoftware/cidre", rev = "bf84b67079a8" } # https://github.com/gfx-rs/wgpu/pull/7550 # wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "cd41a6e32a6239b65d1cecbeccde6a43a100914a" } wgpu-hal = { path = "vendor/wgpu-hal" } +tao = { path = "vendor/tao" } # https://github.com/CapSoftware/posthog-rs/commit/c7e9712be2f9a9122b1df685d5a067afa5415288 posthog-rs = { git = "https://github.com/CapSoftware/posthog-rs", rev = "c7e9712be2f9a9122b1df685d5a067afa5415288" } diff --git a/apps/desktop/src-tauri/src/editor_window.rs b/apps/desktop/src-tauri/src/editor_window.rs index 33a2d56f8c..f47d3f43ad 100644 --- a/apps/desktop/src-tauri/src/editor_window.rs +++ b/apps/desktop/src-tauri/src/editor_window.rs @@ -3,6 +3,8 @@ use tauri::{AppHandle, Manager, Runtime, Window, ipc::CommandArg}; use tokio::sync::{RwLock, watch}; use tokio_util::sync::CancellationToken; +use cap_rendering::GpuOutputFormat; + use crate::{ create_editor_instance_impl, frame_ws::{WSFrame, WSFrameFormat, create_watch_frame_ws}, @@ -29,16 +31,22 @@ async fn do_prewarm(app: AppHandle, path: PathBuf) -> PendingResult { path, Box::new(move |output| { let ws_frame = match output { - cap_editor::EditorFrameOutput::Nv12(frame) => WSFrame { - data: frame.data, - width: frame.width, - height: frame.height, - stride: frame.y_stride, - frame_number: frame.frame_number, - target_time_ns: frame.target_time_ns, - format: WSFrameFormat::Nv12, - created_at: Instant::now(), - }, + cap_editor::EditorFrameOutput::Nv12(frame) => { + let ws_format = match frame.format { + GpuOutputFormat::Nv12 => WSFrameFormat::Nv12, + GpuOutputFormat::Rgba => WSFrameFormat::Rgba, + }; + WSFrame { + data: frame.data, + width: frame.width, + height: frame.height, + stride: frame.y_stride, + frame_number: frame.frame_number, + target_time_ns: frame.target_time_ns, + format: ws_format, + created_at: Instant::now(), + } + } cap_editor::EditorFrameOutput::Rgba(frame) => WSFrame { data: frame.data, width: frame.width, @@ -234,16 +242,22 @@ impl EditorInstances { path, Box::new(move |output| { let ws_frame = match output { - cap_editor::EditorFrameOutput::Nv12(frame) => WSFrame { - data: frame.data, - width: frame.width, - height: frame.height, - stride: frame.y_stride, - frame_number: frame.frame_number, - target_time_ns: frame.target_time_ns, - format: WSFrameFormat::Nv12, - created_at: Instant::now(), - }, + cap_editor::EditorFrameOutput::Nv12(frame) => { + let ws_format = match frame.format { + GpuOutputFormat::Nv12 => WSFrameFormat::Nv12, + GpuOutputFormat::Rgba => WSFrameFormat::Rgba, + }; + WSFrame { + data: frame.data, + width: frame.width, + height: frame.height, + stride: frame.y_stride, + frame_number: frame.frame_number, + target_time_ns: frame.target_time_ns, + format: ws_format, + created_at: Instant::now(), + } + } cap_editor::EditorFrameOutput::Rgba(frame) => WSFrame { data: frame.data, width: frame.width, diff --git a/apps/desktop/src-tauri/src/frame_ws.rs b/apps/desktop/src-tauri/src/frame_ws.rs index dc16a2bb1b..f60cd8caa5 100644 --- a/apps/desktop/src-tauri/src/frame_ws.rs +++ b/apps/desktop/src-tauri/src/frame_ws.rs @@ -30,10 +30,10 @@ fn pack_nv12_frame_ref( data: &[u8], width: u32, height: u32, + y_stride: u32, frame_number: u32, target_time_ns: u64, ) -> Vec { - let y_stride = width; let metadata_size = 28; let mut output = Vec::with_capacity(data.len() + metadata_size); output.extend_from_slice(data); @@ -90,6 +90,7 @@ fn pack_ws_frame_ref(frame: &WSFrame) -> Vec { &frame.data, frame.width, frame.height, + frame.stride, frame.frame_number, frame.target_time_ns, ), diff --git a/apps/desktop/src/routes/editor/context.ts b/apps/desktop/src/routes/editor/context.ts index 7e43e5abcf..be629d2bf3 100644 --- a/apps/desktop/src/routes/editor/context.ts +++ b/apps/desktop/src/routes/editor/context.ts @@ -80,7 +80,7 @@ export const getPreviewResolution = ( quality: EditorPreviewQuality, ): XY => { const scale = previewQualityScale[quality]; - const width = (Math.max(2, Math.round(OUTPUT_SIZE.x * scale)) + 1) & ~1; + const width = (Math.max(4, Math.round(OUTPUT_SIZE.x * scale)) + 3) & ~3; const height = (Math.max(2, Math.round(OUTPUT_SIZE.y * scale)) + 1) & ~1; return { x: width, y: height }; diff --git a/apps/desktop/src/routes/screenshot-editor/context.tsx b/apps/desktop/src/routes/screenshot-editor/context.tsx index 635ef6788d..ffbd5b9694 100644 --- a/apps/desktop/src/routes/screenshot-editor/context.tsx +++ b/apps/desktop/src/routes/screenshot-editor/context.tsx @@ -38,7 +38,7 @@ function convertNv12ToRgba( const ySize = yStride * height; const yPlane = nv12Data; const uvPlane = nv12Data.subarray(ySize); - const uvStride = width; + const uvStride = yStride; for (let row = 0; row < height; row++) { const yRowOffset = row * yStride; @@ -285,7 +285,7 @@ function createScreenshotEditorContext() { if (!width || !height) return; const ySize = yStride * height; - const uvSize = width * (height / 2); + const uvSize = yStride * (height / 2); const totalSize = ySize + uvSize; const nv12Data = new Uint8ClampedArray(buffer, 0, totalSize); diff --git a/apps/desktop/src/utils/frame-worker.ts b/apps/desktop/src/utils/frame-worker.ts index 0afb995088..8720428559 100644 --- a/apps/desktop/src/utils/frame-worker.ts +++ b/apps/desktop/src/utils/frame-worker.ts @@ -231,7 +231,7 @@ function parseFrameMetadata(bytes: Uint8Array): FrameMetadata | null { if (!width || !height) return null; const ySize = yStride * height; - const uvSize = width * (height / 2); + const uvSize = yStride * (height / 2); const totalSize = ySize + uvSize; if (bytes.byteLength - 28 < totalSize) { @@ -303,7 +303,7 @@ function convertNv12ToRgba( const ySize = yStride * height; const yPlane = nv12Data; const uvPlane = nv12Data.subarray(ySize); - const uvStride = width; + const uvStride = yStride; for (let row = 0; row < height; row++) { const yRowOffset = row * yStride; diff --git a/apps/desktop/src/utils/socket.ts b/apps/desktop/src/utils/socket.ts index 923f049cb0..ffff18e09a 100644 --- a/apps/desktop/src/utils/socket.ts +++ b/apps/desktop/src/utils/socket.ts @@ -62,7 +62,7 @@ function convertNv12ToRgbaMainThread( const ySize = yStride * height; const yPlane = nv12Data; const uvPlane = nv12Data.subarray(ySize); - const uvStride = width; + const uvStride = yStride; for (let row = 0; row < height; row++) { const yRowOffset = row * yStride; @@ -308,7 +308,7 @@ export function createImageDataWS( if (width > 0 && height > 0) { const ySize = yStride * height; - const uvSize = width * (height / 2); + const uvSize = yStride * (height / 2); const totalSize = ySize + uvSize; const frameData = new Uint8ClampedArray(buffer, 0, totalSize); @@ -351,7 +351,7 @@ export function createImageDataWS( if (width > 0 && height > 0) { const ySize = yStride * height; - const uvSize = width * (height / 2); + const uvSize = yStride * (height / 2); const totalSize = ySize + uvSize; const frameData = new Uint8ClampedArray(buffer, 0, totalSize); @@ -669,7 +669,7 @@ export function createImageDataWS( if (width > 0 && height > 0) { const ySize = yStride * height; - const uvSize = width * (height / 2); + const uvSize = yStride * (height / 2); const totalSize = ySize + uvSize; const frameData = new Uint8ClampedArray(buffer, 0, totalSize); @@ -735,7 +735,7 @@ export function createImageDataWS( if (width > 0 && height > 0) { const ySize = yStride * height; - const uvSize = width * (height / 2); + const uvSize = yStride * (height / 2); const totalSize = ySize + uvSize; const nv12Data = new Uint8ClampedArray(buffer, 0, totalSize); diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 0fb6d0b503..d32d6b2710 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -403,6 +403,7 @@ videoImportProgress: "video-import-progress" /** user-defined types **/ +export type AllGpusInfo = { gpus: GpuInfoDiag[]; primaryGpuIndex: number | null; isMultiGpuSystem: boolean; hasDiscreteGpu: boolean } export type Annotation = { id: string; type: AnnotationType; x: number; y: number; width: number; height: number; strokeColor: string; strokeWidth: number; fillColor: string; opacity: number; rotation: number; text: string | null; maskType?: MaskType | null; maskLevel?: number | null } export type AnnotationType = "arrow" | "circle" | "rectangle" | "text" | "mask" export type AppTheme = "system" | "light" | "dark" @@ -476,6 +477,7 @@ quality: number | null; */ fast: boolean | null } export type GlideDirection = "none" | "left" | "right" | "up" | "down" +export type GpuInfoDiag = { vendor: string; description: string; dedicatedVideoMemoryMb: number; adapterIndex: number; isSoftwareAdapter: boolean; isBasicRenderDriver: boolean; supportsHardwareEncoding: boolean } export type HapticPattern = "alignment" | "levelChange" | "generic" export type HapticPerformanceTime = "default" | "now" | "drawCompleted" export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean; shift: boolean } @@ -489,7 +491,6 @@ export type JsonValue = [T] export type LogicalBounds = { position: LogicalPosition; size: LogicalSize } export type LogicalPosition = { x: number; y: number } export type LogicalSize = { width: number; height: number } -export type MacOSVersionInfo = { major: number; minor: number; patch: number; displayName: string; buildNumber: string; isAppleSilicon: boolean } export type MainWindowRecordingStartBehaviour = "close" | "minimise" export type MaskKeyframes = { position?: MaskVectorKeyframe[]; size?: MaskVectorKeyframe[]; intensity?: MaskScalarKeyframe[] } export type MaskKind = "sensitive" | "highlight" @@ -533,6 +534,7 @@ export type RecordingStatus = "pending" | "recording" export type RecordingStopped = null export type RecordingTargetMode = "display" | "window" | "area" | "camera" export type RenderFrameEvent = { frame_number: number; fps: number; resolution_base: XY } +export type RenderingStatus = { isUsingSoftwareRendering: boolean; isUsingBasicRenderDriver: boolean; hardwareEncodingAvailable: boolean; warningMessage: string | null } export type RequestOpenRecordingPicker = { target_mode: RecordingTargetMode | null } export type RequestOpenSettings = { page: string } export type RequestScreenCapturePrewarm = { force?: boolean } @@ -555,7 +557,7 @@ export type StartRecordingInputs = { capture_target: ScreenCaptureTarget; captur export type StereoMode = "stereo" | "monoL" | "monoR" export type StudioRecordingMeta = { segment: SingleSegment } | { inner: MultipleSegments } export type StudioRecordingStatus = { status: "InProgress" } | { status: "NeedsRemux" } | { status: "Failed"; error: string } | { status: "Complete" } -export type SystemDiagnostics = { macosVersion: MacOSVersionInfo | null; availableEncoders: string[]; screenCaptureSupported: boolean; metalSupported: boolean; gpuName: string | null } +export type SystemDiagnostics = { windowsVersion: WindowsVersionInfo | null; gpuInfo: GpuInfoDiag | null; allGpus: AllGpusInfo | null; renderingStatus: RenderingStatus; availableEncoders: string[]; graphicsCaptureSupported: boolean; d3D11VideoProcessorAvailable: boolean } export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null } export type TextSegment = { start: number; end: number; enabled?: boolean; content?: string; center?: XY; size?: XY; fontFamily?: string; fontSize?: number; fontWeight?: number; italic?: boolean; color?: string; fadeDuration?: number } export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[]; sceneSegments?: SceneSegment[]; maskSegments?: MaskSegment[]; textSegments?: TextSegment[] } @@ -574,6 +576,7 @@ export type WindowExclusion = { bundleIdentifier?: string | null; ownerName?: st export type WindowId = string export type WindowPosition = { x: number; y: number; displayId?: DisplayId | null } export type WindowUnderCursor = { id: WindowId; app_name: string; bounds: LogicalBounds } +export type WindowsVersionInfo = { major: number; minor: number; build: number; displayName: string; meetsRequirements: boolean; isWindows11: boolean } export type XY = { x: T; y: T } export type ZoomMode = "auto" | { manual: { x: number; y: number } } export type ZoomSegment = { start: number; end: number; amount: number; mode: ZoomMode; glideDirection?: GlideDirection; glideSpeed?: number; instantAnimation?: boolean; edgeSnapRatio?: number } diff --git a/apps/desktop/src/utils/webgpu-renderer.ts b/apps/desktop/src/utils/webgpu-renderer.ts index 3ab05955aa..08ef45ad23 100644 --- a/apps/desktop/src/utils/webgpu-renderer.ts +++ b/apps/desktop/src/utils/webgpu-renderer.ts @@ -380,7 +380,7 @@ export function renderNv12FrameWebGPU( const ySize = yStride * height; const uvWidth = width / 2; const uvHeight = height / 2; - const uvStride = width; + const uvStride = yStride; const uvSize = uvStride * uvHeight; if (data.byteLength < ySize + uvSize) { diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 1d52c776f8..ebdd7c98b8 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -36,3 +36,6 @@ ringbuf = "0.4.8" lru = "0.12" tracing-subscriber = { version = "0.3", features = ["env-filter"] } workspace-hack = { version = "0.1", path = "../workspace-hack" } + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { workspace = true, features = ["Win32_Media"] } \ No newline at end of file diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f378538a08..716b7c0ef5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -148,7 +148,7 @@ impl Renderer { } } match frame_renderer - .render_immediate( + .render_immediate_nv12( current.segment_frames, current.uniforms, ¤t.cursor, @@ -157,7 +157,7 @@ impl Renderer { .await { Ok(frame) => { - (self.frame_cb)(EditorFrameOutput::Rgba(frame)); + (self.frame_cb)(EditorFrameOutput::Nv12(frame)); } Err(e) => { tracing::error!(error = %e, "Failed to render frame in editor"); @@ -170,11 +170,7 @@ impl Renderer { } impl RendererHandle { - async fn send(&self, msg: RendererMessage) { - let _ = self.tx.send(msg).await; - } - - pub async fn render_frame( + pub fn render_frame( &self, segment_frames: DecodedSegmentFrames, uniforms: ProjectUniforms, @@ -182,13 +178,12 @@ impl RendererHandle { ) { let (finished_tx, _finished_rx) = oneshot::channel(); - self.send(RendererMessage::RenderFrame { + let _ = self.tx.try_send(RendererMessage::RenderFrame { segment_frames, uniforms, finished: finished_tx, cursor, - }) - .await; + }); } pub async fn stop(&self) { diff --git a/crates/editor/src/editor_instance.rs b/crates/editor/src/editor_instance.rs index 4879fadc26..df50aac1bf 100644 --- a/crates/editor/src/editor_instance.rs +++ b/crates/editor/src/editor_instance.rs @@ -532,8 +532,7 @@ impl EditorInstance { &zoom_focus_interpolator, ); self.renderer - .render_frame(segment_frames, uniforms, segment_medias.cursor.clone()) - .await; + .render_frame(segment_frames, uniforms, segment_medias.cursor.clone()); } else { warn!("Preview renderer: no frames returned for frame {}", frame_number); } diff --git a/crates/editor/src/playback.rs b/crates/editor/src/playback.rs index 87a1776a3c..4f26dbeab4 100644 --- a/crates/editor/src/playback.rs +++ b/crates/editor/src/playback.rs @@ -35,13 +35,63 @@ use crate::{ }; const PREFETCH_BUFFER_SIZE: usize = 90; +#[cfg(not(target_os = "windows"))] const PARALLEL_DECODE_TASKS: usize = 6; +#[cfg(not(target_os = "windows"))] const INITIAL_PARALLEL_DECODE_TASKS: usize = 8; const MAX_PREFETCH_AHEAD: u32 = 90; +#[cfg(not(target_os = "windows"))] const PREFETCH_BEHIND: u32 = 10; const FRAME_CACHE_SIZE: usize = 90; const RAMP_UP_FRAME_COUNT: u32 = 15; +#[cfg(target_os = "windows")] +struct WindowsTimerResolution; + +#[cfg(target_os = "windows")] +impl WindowsTimerResolution { + fn set_high_precision() -> Self { + unsafe { + windows::Win32::Media::timeBeginPeriod(1); + } + Self + } +} + +#[cfg(target_os = "windows")] +impl Drop for WindowsTimerResolution { + fn drop(&mut self) { + unsafe { + windows::Win32::Media::timeEndPeriod(1); + } + } +} + +#[cfg(target_os = "windows")] +async fn precision_sleep_until(deadline: Instant, stop_rx: &mut watch::Receiver) -> bool { + let spin_threshold = Duration::from_millis(2); + + let now = Instant::now(); + if now >= deadline { + return false; + } + + let remaining = deadline - now; + if remaining > spin_threshold { + let sleep_target = deadline - spin_threshold; + tokio::select! { + _ = stop_rx.changed() => return true, + _ = tokio::time::sleep_until(sleep_target) => {} + } + } + + while Instant::now() < deadline { + tokio::task::yield_now().await; + } + + false +} + #[derive(Debug)] pub enum PlaybackStartError { InvalidFps, @@ -181,6 +231,12 @@ impl Playback { let mut in_flight: FuturesUnordered = FuturesUnordered::new(); let mut frames_decoded: u32 = 0; let mut prefetched_behind: HashSet = HashSet::new(); + #[cfg(target_os = "windows")] + let prefetch_behind = 0u32; + #[cfg(not(target_os = "windows"))] + let prefetch_behind = PREFETCH_BEHIND; + #[cfg(target_os = "windows")] + let prefetch_start = Instant::now(); let mut cached_project = prefetch_project.borrow().clone(); @@ -219,12 +275,29 @@ impl Playback { } let current_playback_frame = *playback_position_rx.borrow(); - let max_prefetch_frame = current_playback_frame + MAX_PREFETCH_AHEAD; + #[cfg(target_os = "windows")] + let max_prefetch_ahead = if prefetch_start.elapsed() < Duration::from_secs(6) { + 45u32 + } else { + MAX_PREFETCH_AHEAD + }; + #[cfg(not(target_os = "windows"))] + let max_prefetch_ahead = MAX_PREFETCH_AHEAD; + let max_prefetch_frame = current_playback_frame + max_prefetch_ahead; + + #[cfg(target_os = "windows")] + let initial_parallel_decode_tasks = 3usize; + #[cfg(not(target_os = "windows"))] + let initial_parallel_decode_tasks = INITIAL_PARALLEL_DECODE_TASKS; + #[cfg(target_os = "windows")] + let parallel_decode_tasks = 3usize; + #[cfg(not(target_os = "windows"))] + let parallel_decode_tasks = PARALLEL_DECODE_TASKS; let effective_parallel = if frames_decoded < RAMP_UP_FRAME_COUNT { - INITIAL_PARALLEL_DECODE_TASKS + initial_parallel_decode_tasks } else { - PARALLEL_DECODE_TASKS + parallel_decode_tasks }; while in_flight.len() < effective_parallel { @@ -292,7 +365,7 @@ impl Playback { } if in_flight.len() < effective_parallel { - for behind_offset in 1..=PREFETCH_BEHIND { + for behind_offset in 1..=prefetch_behind { if in_flight.len() >= effective_parallel { break; } @@ -400,7 +473,6 @@ impl Playback { let mut prefetch_buffer: VecDeque = VecDeque::with_capacity(PREFETCH_BUFFER_SIZE); let mut frame_cache = FrameCache::new(FRAME_CACHE_SIZE); - let aggressive_skip_threshold = 6u32; let mut total_frames_rendered = 0u64; let mut total_frames_skipped = 0u64; @@ -410,6 +482,9 @@ impl Playback { let mut last_stats_time = Instant::now(); let stats_interval = Duration::from_secs(2); + #[cfg(target_os = "windows")] + let warmup_target_frames = 45usize; + #[cfg(not(target_os = "windows"))] let warmup_target_frames = 10usize; let warmup_after_first_timeout = Duration::from_millis(500); let warmup_no_frames_timeout = Duration::from_secs(5); @@ -460,6 +535,9 @@ impl Playback { .make_contiguous() .sort_by_key(|p| p.frame_number); + #[cfg(target_os = "windows")] + let _timer_guard = WindowsTimerResolution::set_high_precision(); + let start = Instant::now(); let mut cached_project = self.project.borrow().clone(); @@ -491,9 +569,18 @@ impl Playback { let frame_offset = frame_number.saturating_sub(self.start_frame_number) as f64; let next_deadline = start + frame_duration.mul_f64(frame_offset); - tokio::select! { - _ = stop_rx.changed() => break 'playback, - _ = tokio::time::sleep_until(next_deadline) => {} + #[cfg(target_os = "windows")] + { + if precision_sleep_until(next_deadline, &mut stop_rx).await { + break 'playback; + } + } + #[cfg(not(target_os = "windows"))] + { + tokio::select! { + _ = stop_rx.changed() => break 'playback, + _ = tokio::time::sleep_until(next_deadline) => {} + } } if *stop_rx.borrow() { @@ -505,6 +592,8 @@ impl Playback { break; } + let decode_wait_timeout = Duration::from_millis(100); + let mut was_cached = false; let segment_frames_opt = if let Some(cached) = frame_cache.get(frame_number) { @@ -531,7 +620,7 @@ impl Playback { if is_in_flight { let wait_start = Instant::now(); - let max_wait = Duration::from_millis(100); + let max_wait = decode_wait_timeout; let mut found_frame = None; while wait_start.elapsed() < max_wait { @@ -629,7 +718,7 @@ impl Playback { guard.insert(frame_number); } - let max_wait = Duration::from_millis(100); + let max_wait = decode_wait_timeout; let data = tokio::select! { _ = stop_rx.changed() => { if let Ok(mut guard) = main_in_flight.write() { @@ -702,13 +791,11 @@ impl Playback { &zoom_focus_interpolator, ); - self.renderer - .render_frame( - Arc::unwrap_or_clone(segment_frames), - uniforms, - segment_media.cursor.clone(), - ) - .await; + self.renderer.render_frame( + Arc::unwrap_or_clone(segment_frames), + uniforms, + segment_media.cursor.clone(), + ); total_frames_rendered += 1; } @@ -748,26 +835,25 @@ impl Playback { if frame_number < expected_frame { let frames_behind = expected_frame - frame_number; - if frames_behind <= aggressive_skip_threshold { + if frames_behind <= 2 { continue; } - let skipped = frames_behind.saturating_sub(1); - if skipped > 0 { - frame_number += skipped; - total_frames_skipped += skipped as u64; - - prefetch_buffer.retain(|p| p.frame_number >= frame_number); - frame_cache.evict_far_from(frame_number, MAX_PREFETCH_AHEAD); - let _ = frame_request_tx.send(frame_number); - let _ = playback_position_tx.send(frame_number); - if has_audio - && audio_playhead_tx - .send(frame_number as f64 / fps_f64) - .is_err() - { - break 'playback; - } + let max_skip = 3u32; + let skipped = frames_behind.min(max_skip); + frame_number += skipped; + total_frames_skipped += skipped as u64; + + prefetch_buffer.retain(|p| p.frame_number >= frame_number); + frame_cache.evict_far_from(frame_number, MAX_PREFETCH_AHEAD); + let _ = frame_request_tx.send(frame_number); + let _ = playback_position_tx.send(frame_number); + if has_audio + && audio_playhead_tx + .send(frame_number as f64 / fps_f64) + .is_err() + { + break 'playback; } } } diff --git a/crates/rendering/src/decoder/media_foundation.rs b/crates/rendering/src/decoder/media_foundation.rs index bc5c3ad3ab..9d7ffb140f 100644 --- a/crates/rendering/src/decoder/media_foundation.rs +++ b/crates/rendering/src/decoder/media_foundation.rs @@ -18,6 +18,8 @@ struct DecoderHealthMonitor { total_frames_decoded: u64, total_errors: u64, last_successful_decode: Instant, + last_request_time: Instant, + last_unhealthy_warning: Option, frame_decode_times: [Duration; 32], frame_decode_index: usize, slow_frame_count: u32, @@ -31,12 +33,18 @@ impl DecoderHealthMonitor { total_frames_decoded: 0, total_errors: 0, last_successful_decode: Instant::now(), + last_request_time: Instant::now(), + last_unhealthy_warning: None, frame_decode_times: [Duration::ZERO; 32], frame_decode_index: 0, slow_frame_count: 0, } } + fn record_request(&mut self) { + self.last_request_time = Instant::now(); + } + fn record_success(&mut self, decode_time: Duration) { self.consecutive_errors = 0; self.consecutive_texture_read_failures = 0; @@ -66,9 +74,34 @@ impl DecoderHealthMonitor { const MAX_CONSECUTIVE_TEXTURE_FAILURES: u32 = 5; const MAX_TIME_SINCE_SUCCESS: Duration = Duration::from_secs(5); - self.consecutive_errors < MAX_CONSECUTIVE_ERRORS - && self.consecutive_texture_read_failures < MAX_CONSECUTIVE_TEXTURE_FAILURES - && self.last_successful_decode.elapsed() < MAX_TIME_SINCE_SUCCESS + if self.consecutive_errors >= MAX_CONSECUTIVE_ERRORS + || self.consecutive_texture_read_failures >= MAX_CONSECUTIVE_TEXTURE_FAILURES + { + return false; + } + + let idle_duration = self.last_request_time.elapsed(); + if idle_duration > Duration::from_secs(2) { + return true; + } + + self.last_successful_decode.elapsed() < MAX_TIME_SINCE_SUCCESS + } + + fn should_warn_unhealthy(&mut self) -> bool { + if self.is_healthy() { + return false; + } + + let should_warn = self + .last_unhealthy_warning + .is_none_or(|last| last.elapsed() > Duration::from_secs(5)); + + if should_warn { + self.last_unhealthy_warning = Some(Instant::now()); + } + + should_warn } #[allow(dead_code)] @@ -198,157 +231,227 @@ impl MFDecoder { }; let _ = ready_tx.send(Ok(init_result)); + struct PendingRequest { + frame: u32, + time: f32, + sender: oneshot::Sender, + } + while let Ok(r) = rx.recv() { - match r { - VideoDecoderMessage::GetFrame(requested_time, sender) => { + let mut pending_requests: Vec = Vec::with_capacity(8); + + let mut push_request = + |requested_time: f32, sender: oneshot::Sender| { if sender.is_closed() { - continue; + return; } + let frame = (requested_time * fps as f32).floor() as u32; + pending_requests.push(PendingRequest { + frame, + time: requested_time, + sender, + }); + }; - if !health.is_healthy() { - warn!( - name = name, - consecutive_errors = health.consecutive_errors, - texture_failures = health.consecutive_texture_read_failures, - total_decoded = health.total_frames_decoded, - "MediaFoundation decoder unhealthy, performance may degrade" - ); + match r { + VideoDecoderMessage::GetFrame(requested_time, sender) => { + push_request(requested_time, sender); + } + } + + while let Ok(msg) = rx.try_recv() { + match msg { + VideoDecoderMessage::GetFrame(requested_time, sender) => { + push_request(requested_time, sender); } + } + } - let requested_frame = (requested_time * fps as f32).floor() as u32; + let mut unfulfilled = Vec::with_capacity(pending_requests.len()); + for req in pending_requests.drain(..) { + if let Some(cached) = cache.get(&req.frame) { + let _ = req.sender.send(cached.to_decoded_frame()); + } else if !req.sender.is_closed() { + unfulfilled.push(req); + } + } + pending_requests = unfulfilled; - if let Some(cached) = cache.get(&requested_frame) { - let _ = sender.send(cached.to_decoded_frame()); - continue; - } + if pending_requests.is_empty() { + continue; + } - let cache_min = requested_frame.saturating_sub(FRAME_CACHE_SIZE as u32 / 2); - let cache_max = requested_frame + FRAME_CACHE_SIZE as u32 / 2; - - let needs_seek = last_decoded_frame - .map(|last| { - requested_frame < last - || requested_frame.saturating_sub(last) - > FRAME_CACHE_SIZE as u32 - }) - .unwrap_or(true); - - if needs_seek { - let time_100ns = frame_to_100ns(requested_frame, fps); - if let Err(e) = decoder.seek(time_100ns) { - warn!("MediaFoundation seek failed: {e}"); - } - cache.clear(); - last_decoded_frame = None; - } + pending_requests.sort_by_key(|r| r.frame); - let mut sender = Some(sender); - let mut last_valid_frame: Option = None; - - loop { - let decode_start = Instant::now(); - match decoder.read_sample() { - Ok(Some(mf_frame)) => { - let decode_time = decode_start.elapsed(); - let frame_number = pts_100ns_to_frame(mf_frame.pts, fps); - - let nv12_data = match decoder.read_texture_to_cpu( - &mf_frame.textures.nv12.texture, - mf_frame.width, - mf_frame.height, - ) { - Ok(data) => { - health.record_success(decode_time); - Some(Arc::new(data)) - } - Err(e) => { - health.record_texture_read_failure(); - warn!( - "Failed to read texture to CPU for frame {frame_number}: {e}" - ); - None - } - }; + let target_request = pending_requests.pop().unwrap(); + let deferred_requests = pending_requests; - let cached = CachedFrame { - number: frame_number, - _texture: mf_frame.textures.nv12.texture.clone(), - _shared_handle: Some(mf_frame.textures.nv12.handle), - _y_handle: Some(mf_frame.textures.y.handle), - _uv_handle: Some(mf_frame.textures.uv.handle), - nv12_data, - width: mf_frame.width, - height: mf_frame.height, - }; + let requested_frame = target_request.frame; + let mut sender = Some(target_request.sender); - last_decoded_frame = Some(frame_number); - - if frame_number >= cache_min && frame_number <= cache_max { - if cache.len() >= FRAME_CACHE_SIZE { - let key_to_remove = if frame_number > requested_frame { - *cache.keys().next().unwrap() - } else { - *cache.keys().next_back().unwrap() - }; - cache.remove(&key_to_remove); - } - cache.insert(frame_number, cached.clone()); - } + health.record_request(); - if frame_number <= requested_frame { - last_valid_frame = Some(cached); - } + if health.should_warn_unhealthy() { + warn!( + name = name, + consecutive_errors = health.consecutive_errors, + texture_failures = health.consecutive_texture_read_failures, + total_decoded = health.total_frames_decoded, + "MediaFoundation decoder unhealthy, performance may degrade" + ); + } - if frame_number >= requested_frame { - let frame_to_send = if frame_number == requested_frame { - cache.get(&requested_frame) - } else { - last_valid_frame - .as_ref() - .or_else(|| cache.get(&frame_number)) - }; - - if let Some(frame) = frame_to_send - && let Some(s) = sender.take() - { - let _ = s.send(frame.to_decoded_frame()); - } - } + let cache_min = requested_frame.saturating_sub(FRAME_CACHE_SIZE as u32 / 2); + let cache_max = requested_frame + FRAME_CACHE_SIZE as u32 / 2; + + let needs_seek = last_decoded_frame + .map(|last| { + requested_frame < last + || requested_frame.saturating_sub(last) > FRAME_CACHE_SIZE as u32 + }) + .unwrap_or(true); + + if needs_seek { + let time_100ns = frame_to_100ns(requested_frame, fps); + if let Err(e) = decoder.seek(time_100ns) { + warn!("MediaFoundation seek failed: {e}"); + } + cache.clear(); + last_decoded_frame = None; + } + + let mut last_valid_frame: Option = None; - let readahead_target = requested_frame + 30; - if frame_number >= readahead_target || frame_number > cache_max - { - break; + loop { + if sender.as_ref().is_some_and(|s| s.is_closed()) { + sender.take(); + break; + } + + let decode_start = Instant::now(); + match decoder.read_sample() { + Ok(Some(mf_frame)) => { + let decode_time = decode_start.elapsed(); + let frame_number = pts_100ns_to_frame(mf_frame.pts, fps); + + let has_valid_zero_copy_handles = { + let null_ptr = std::ptr::null_mut(); + mf_frame.textures.y.handle.0 != null_ptr + && mf_frame.textures.uv.handle.0 != null_ptr + }; + + let nv12_data = if has_valid_zero_copy_handles { + health.record_success(decode_time); + None + } else { + match decoder.read_texture_to_cpu( + &mf_frame.textures.nv12.texture, + mf_frame.width, + mf_frame.height, + ) { + Ok(data) => { + health.record_success(decode_time); + Some(Arc::new(data)) + } + Err(e) => { + health.record_texture_read_failure(); + warn!( + "Failed to read texture to CPU for frame {frame_number}: {e}" + ); + None } } - Ok(None) => { - break; + }; + + let cached = CachedFrame { + number: frame_number, + _texture: mf_frame.textures.nv12.texture.clone(), + _shared_handle: Some(mf_frame.textures.nv12.handle), + _y_handle: Some(mf_frame.textures.y.handle), + _uv_handle: Some(mf_frame.textures.uv.handle), + nv12_data, + width: mf_frame.width, + height: mf_frame.height, + }; + + last_decoded_frame = Some(frame_number); + + if frame_number >= cache_min && frame_number <= cache_max { + if cache.len() >= FRAME_CACHE_SIZE { + let key_to_remove = if frame_number > requested_frame { + *cache.keys().next().unwrap() + } else { + *cache.keys().next_back().unwrap() + }; + cache.remove(&key_to_remove); } - Err(e) => { - health.record_error(); - warn!( - consecutive_errors = health.consecutive_errors, - "MediaFoundation read_sample error: {e}" - ); - break; + cache.insert(frame_number, cached.clone()); + } + + if frame_number <= requested_frame { + last_valid_frame = Some(cached); + } + + if frame_number >= requested_frame { + let frame_to_send = if frame_number == requested_frame { + cache.get(&requested_frame) + } else { + last_valid_frame + .as_ref() + .or_else(|| cache.get(&frame_number)) + }; + + if let Some(frame) = frame_to_send + && let Some(s) = sender.take() + { + let _ = s.send(frame.to_decoded_frame()); } } - } - if let Some(s) = sender.take() { - if let Some(frame) = last_valid_frame - .or_else(|| cache.values().max_by_key(|f| f.number).cloned()) - { - let _ = s.send(frame.to_decoded_frame()); - } else { - let black_frame = DecodedFrame::new( - vec![0u8; (video_width * video_height * 4) as usize], - video_width, - video_height, - ); - let _ = s.send(black_frame); + #[cfg(target_os = "windows")] + let readahead_frames = 12u32; + #[cfg(not(target_os = "windows"))] + let readahead_frames = 30u32; + let readahead_target = requested_frame + readahead_frames; + if frame_number >= readahead_target || frame_number > cache_max { + break; } } + Ok(None) => { + break; + } + Err(e) => { + health.record_error(); + warn!( + consecutive_errors = health.consecutive_errors, + "MediaFoundation read_sample error: {e}" + ); + break; + } + } + } + + if let Some(s) = sender.take() { + if let Some(frame) = last_valid_frame + .or_else(|| cache.values().max_by_key(|f| f.number).cloned()) + { + let _ = s.send(frame.to_decoded_frame()); + } else { + let black_frame = DecodedFrame::new( + vec![0u8; (video_width * video_height * 4) as usize], + video_width, + video_height, + ); + let _ = s.send(black_frame); + } + } + + for req in deferred_requests { + if req.sender.is_closed() { + continue; + } + if let Some(cached) = cache.get(&req.frame) { + let _ = req.sender.send(cached.to_decoded_frame()); } } } diff --git a/crates/rendering/src/frame_pipeline.rs b/crates/rendering/src/frame_pipeline.rs index f8dac07e69..770dc83ed2 100644 --- a/crates/rendering/src/frame_pipeline.rs +++ b/crates/rendering/src/frame_pipeline.rs @@ -17,6 +17,7 @@ pub struct RgbaToNv12Converter { pending: Option, cached_width: u32, cached_height: u32, + cached_stride: u32, cached_bind_groups: Option<[wgpu::BindGroup; 2]>, cached_texture_view: Option, cached_texture_ptr: usize, @@ -108,20 +109,31 @@ impl RgbaToNv12Converter { pending: None, cached_width: 0, cached_height: 0, + cached_stride: 0, cached_bind_groups: None, cached_texture_view: None, cached_texture_ptr: 0, } } + fn aligned_stride(width: u32) -> u32 { + (width + 3) & !3 + } + fn nv12_size(width: u32, height: u32) -> u64 { - let y_size = (width as u64) * (height as u64); - let uv_size = (width as u64) * (height as u64 / 2); + let stride = Self::aligned_stride(width) as u64; + let aligned_height = ((height + 1) & !1) as u64; + let y_size = stride * aligned_height; + let uv_size = stride * (aligned_height / 2); y_size + uv_size } fn ensure_buffers(&mut self, device: &wgpu::Device, width: u32, height: u32) { - if self.cached_width == width && self.cached_height == height { + let stride = Self::aligned_stride(width); + if self.cached_width == width + && self.cached_height == height + && self.cached_stride == stride + { return; } @@ -148,6 +160,7 @@ impl RgbaToNv12Converter { self.current_readback = 0; self.cached_width = width; self.cached_height = height; + self.cached_stride = stride; self.cached_bind_groups = None; self.cached_texture_view = None; self.cached_texture_ptr = 0; @@ -165,7 +178,7 @@ impl RgbaToNv12Converter { frame_number: u32, frame_rate: u32, ) -> bool { - if width == 0 || height == 0 || !width.is_multiple_of(4) || !height.is_multiple_of(2) { + if width == 0 || height == 0 { return false; } @@ -182,8 +195,8 @@ impl RgbaToNv12Converter { }; self.current_readback = 1 - self.current_readback; - let y_stride = width; - let uv_stride = width; + let y_stride = Self::aligned_stride(width); + let uv_stride = Self::aligned_stride(width); let params = Nv12Params { width, diff --git a/crates/rendering/src/layers/camera.rs b/crates/rendering/src/layers/camera.rs index 01c130d5f0..d7c9944b1f 100644 --- a/crates/rendering/src/layers/camera.rs +++ b/crates/rendering/src/layers/camera.rs @@ -21,6 +21,7 @@ pub struct CameraLayer { } impl CameraLayer { + #[allow(dead_code)] pub fn new(device: &wgpu::Device) -> Self { Self::new_with_all_shared_pipelines( device, diff --git a/crates/rendering/src/layers/display.rs b/crates/rendering/src/layers/display.rs index fcca1100d9..797388fd3c 100644 --- a/crates/rendering/src/layers/display.rs +++ b/crates/rendering/src/layers/display.rs @@ -1,9 +1,11 @@ use cap_project::XY; +use std::sync::Arc; + use crate::{ DecodedSegmentFrames, PixelFormat, composite_frame::{CompositeVideoFramePipeline, CompositeVideoFrameUniforms}, - yuv_converter::YuvToRgbaConverter, + yuv_converter::{YuvConverterPipelines, YuvToRgbaConverter}, }; struct PendingTextureCopy { @@ -17,7 +19,7 @@ pub struct DisplayLayer { frame_texture_views: [wgpu::TextureView; 2], current_texture: usize, uniforms_buffer: wgpu::Buffer, - pipeline: CompositeVideoFramePipeline, + pipeline: std::sync::Arc, bind_groups: [Option; 2], last_recording_time: Option, yuv_converter: YuvToRgbaConverter, @@ -32,19 +34,32 @@ impl DisplayLayer { } pub fn new_with_options(device: &wgpu::Device, prefer_cpu_conversion: bool) -> Self { + Self::new_with_all_shared_pipelines( + device, + Arc::new(YuvConverterPipelines::new(device)), + Arc::new(CompositeVideoFramePipeline::new(device)), + prefer_cpu_conversion, + ) + } + + pub fn new_with_all_shared_pipelines( + device: &wgpu::Device, + yuv_pipelines: Arc, + composite_pipeline: Arc, + prefer_cpu_conversion: bool, + ) -> Self { let frame_texture_0 = CompositeVideoFramePipeline::create_frame_texture(device, 1920, 1080); let frame_texture_1 = CompositeVideoFramePipeline::create_frame_texture(device, 1920, 1080); let frame_texture_view_0 = frame_texture_0.create_view(&Default::default()); let frame_texture_view_1 = frame_texture_1.create_view(&Default::default()); let uniforms_buffer = CompositeVideoFrameUniforms::default().to_buffer(device); - let pipeline = CompositeVideoFramePipeline::new(device); let bind_group_0 = - Some(pipeline.bind_group(device, &uniforms_buffer, &frame_texture_view_0)); + Some(composite_pipeline.bind_group(device, &uniforms_buffer, &frame_texture_view_0)); let bind_group_1 = - Some(pipeline.bind_group(device, &uniforms_buffer, &frame_texture_view_1)); + Some(composite_pipeline.bind_group(device, &uniforms_buffer, &frame_texture_view_1)); - let yuv_converter = YuvToRgbaConverter::new(device); + let yuv_converter = YuvToRgbaConverter::new_with_shared_pipelines(device, yuv_pipelines); if prefer_cpu_conversion { tracing::info!("DisplayLayer initialized with CPU YUV conversion preference"); @@ -55,7 +70,7 @@ impl DisplayLayer { frame_texture_views: [frame_texture_view_0, frame_texture_view_1], current_texture: 0, uniforms_buffer, - pipeline, + pipeline: composite_pipeline, bind_groups: [bind_group_0, bind_group_1], last_recording_time: None, yuv_converter, @@ -64,6 +79,55 @@ impl DisplayLayer { } } + #[cfg(target_os = "windows")] + fn try_d3d11_staging_fallback( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + screen_frame: &crate::DecodedFrame, + actual_width: u32, + actual_height: u32, + next_texture: usize, + ) -> bool { + let Some(nv12_texture) = screen_frame.d3d11_texture_backing() else { + return false; + }; + + let Ok(d3d11_device) = (unsafe { nv12_texture.GetDevice() }) else { + return false; + }; + + let Ok(d3d11_context) = (unsafe { d3d11_device.GetImmediateContext() }) else { + return false; + }; + + if self + .yuv_converter + .convert_nv12_with_fallback( + device, + queue, + &d3d11_device, + &d3d11_context, + nv12_texture, + screen_frame.d3d11_y_handle(), + screen_frame.d3d11_uv_handle(), + actual_width, + actual_height, + ) + .is_ok() + && self.yuv_converter.output_texture().is_some() + { + self.pending_copy = Some(PendingTextureCopy { + width: actual_width, + height: actual_height, + dst_texture_index: next_texture, + }); + true + } else { + false + } + } + pub fn prepare( &mut self, device: &wgpu::Device, @@ -282,7 +346,14 @@ impl DisplayLayer { false } } else { - false + self.try_d3d11_staging_fallback( + device, + queue, + screen_frame, + actual_width, + actual_height, + next_texture, + ) } } @@ -541,7 +612,14 @@ impl DisplayLayer { Err(_) => false, } } else { - false + self.try_d3d11_staging_fallback( + device, + queue, + screen_frame, + actual_width, + actual_height, + next_texture, + ) } } else if let (Some(y_data), Some(uv_data)) = (screen_frame.y_plane(), screen_frame.uv_plane()) @@ -575,7 +653,14 @@ impl DisplayLayer { Err(_) => false, } } else { - false + self.try_d3d11_staging_fallback( + device, + queue, + screen_frame, + actual_width, + actual_height, + next_texture, + ) } #[cfg(not(target_os = "windows"))] diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index 14ce7b3d5d..e61b180115 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -1455,7 +1455,7 @@ impl ProjectUniforms { let height_scale = resolution_base.y as f32 / base_height as f32; let scale = width_scale.min(height_scale); - let scaled_width = ((base_width as f32 * scale) as u32 + 1) & !1; + let scaled_width = ((base_width as f32 * scale) as u32 + 3) & !3; let scaled_height = ((base_height as f32 * scale) as u32 + 1) & !1; (scaled_width, scaled_height) } @@ -2320,6 +2320,24 @@ impl<'a> FrameRenderer<'a> { } } + pub async fn render_immediate_nv12( + &mut self, + segment_frames: DecodedSegmentFrames, + uniforms: ProjectUniforms, + cursor: &CursorEvents, + layers: &mut RendererLayers, + ) -> Result { + if let Some(frame) = self + .render_nv12(segment_frames, uniforms, cursor, layers) + .await? + { + return Ok(frame); + } + self.flush_pipeline_nv12() + .await + .unwrap_or(Err(RenderingError::BufferMapWaitingFailed)) + } + pub async fn flush_pipeline_nv12( &mut self, ) -> Option> { @@ -2443,13 +2461,30 @@ impl RendererLayers { queue: &wgpu::Queue, prefer_cpu_conversion: bool, ) -> Self { + let shared_yuv_pipelines = Arc::new(yuv_converter::YuvConverterPipelines::new(device)); + let shared_composite_pipeline = + Arc::new(composite_frame::CompositeVideoFramePipeline::new(device)); + Self { background: BackgroundLayer::new(device), background_blur: BlurLayer::new(device), - display: DisplayLayer::new_with_options(device, prefer_cpu_conversion), + display: DisplayLayer::new_with_all_shared_pipelines( + device, + shared_yuv_pipelines.clone(), + shared_composite_pipeline.clone(), + prefer_cpu_conversion, + ), cursor: CursorLayer::new(device), - camera: CameraLayer::new(device), - camera_only: CameraLayer::new(device), + camera: CameraLayer::new_with_all_shared_pipelines( + device, + shared_yuv_pipelines.clone(), + shared_composite_pipeline.clone(), + ), + camera_only: CameraLayer::new_with_all_shared_pipelines( + device, + shared_yuv_pipelines, + shared_composite_pipeline, + ), mask: MaskLayer::new(device), text: TextLayer::new(device, queue), captions: CaptionsLayer::new(device, queue), diff --git a/vendor/tao/.cargo-ok b/vendor/tao/.cargo-ok new file mode 100644 index 0000000000..5f8b795830 --- /dev/null +++ b/vendor/tao/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/vendor/tao/.cargo_vcs_info.json b/vendor/tao/.cargo_vcs_info.json new file mode 100644 index 0000000000..d201940fec --- /dev/null +++ b/vendor/tao/.cargo_vcs_info.json @@ -0,0 +1,7 @@ +{ + "git": { + "sha1": "fda48e80ef0600079666c19146eb01bdfc4dd3c4", + "dirty": true + }, + "path_in_vcs": "" +} diff --git a/vendor/tao/Cargo.lock b/vendor/tao/Cargo.lock new file mode 100644 index 0000000000..e134e3112c --- /dev/null +++ b/vendor/tao/Cargo.lock @@ -0,0 +1,3267 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av1-grain" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2 0.6.2", +] + +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytemuck" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags", + "core-foundation", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "ctor-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags", + "objc2 0.6.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "dlopen2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "drm" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" +dependencies = [ + "bitflags", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" +dependencies = [ + "drm-sys", + "rustix 0.38.44", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "gethostname" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" +dependencies = [ + "rustix 1.0.8", + "windows-targets 0.52.6", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.3+wasi-0.2.4", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c6a3ce16143778e24df6f95365f12ed105425b22abefd289dd88a64bab59605" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle 0.6.2", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +dependencies = [ + "proc-macro-crate 2.0.2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "bitflags", + "objc2 0.6.2", + "objc2-core-foundation", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags", + "dispatch2", + "objc2 0.6.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +dependencies = [ + "bitflags", + "objc2 0.6.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pxfm" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e790881194f6f6e86945f0a42a6981977323669aeb6c40e9c7ec253133b96f8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41" +dependencies = [ + "cty", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases", + "core-graphics", + "drm", + "fastrand", + "foreign-types", + "js-sys", + "log", + "memmap2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core", + "raw-window-handle 0.6.2", + "redox_syscall", + "rustix 0.38.44", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.59.0", + "x11rb", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.3" +dependencies = [ + "bitflags", + "block2 0.6.1", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "env_logger", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "image", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2 0.6.2", + "objc2-app-kit", + "objc2-foundation 0.3.1", + "once_cell", + "parking_lot", + "raw-window-handle 0.4.3", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "scopeguard", + "serde", + "softbuffer", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.3+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.0.8", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags", + "rustix 1.0.8", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-version" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.0.8", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" +dependencies = [ + "zune-core", +] diff --git a/vendor/tao/Cargo.toml b/vendor/tao/Cargo.toml new file mode 100644 index 0000000000..13c0c01c60 --- /dev/null +++ b/vendor/tao/Cargo.toml @@ -0,0 +1,390 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.74" +name = "tao" +version = "0.34.3" +authors = [ + "Tauri Programme within The Commons Conservancy", + "The winit contributors", +] +build = false +include = [ + "/README.md", + "src/**/*.rs", + "examples/**/*.rs", + "LICENSE*", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Cross-platform window manager library." +documentation = "https://docs.rs/tao" +readme = "README.md" +keywords = ["windowing"] +categories = ["gui"] +license = "Apache-2.0" +repository = "https://github.com/tauri-apps/tao" + +[package.metadata.docs.rs] +default-target = "x86_64-unknown-linux-gnu" +features = [ + "rwh_04", + "rwh_05", + "rwh_06", + "serde", + "x11", +] +targets = [ + "i686-pc-windows-msvc", + "x86_64-pc-windows-msvc", + "i686-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", +] + +[features] +default = [ + "rwh_06", + "x11", +] +rwh_04 = ["dep:rwh_04"] +rwh_05 = ["dep:rwh_05"] +rwh_06 = ["dep:rwh_06"] +serde = [ + "dep:serde", + "dpi/serde", +] +x11 = [ + "dep:gdkx11-sys", + "dep:x11-dl", +] + +[lib] +name = "tao" +path = "src/lib.rs" + +[[example]] +name = "control_flow" +path = "examples/control_flow.rs" + +[[example]] +name = "cursor" +path = "examples/cursor.rs" + +[[example]] +name = "cursor_grab" +path = "examples/cursor_grab.rs" + +[[example]] +name = "custom_events" +path = "examples/custom_events.rs" + +[[example]] +name = "decorations" +path = "examples/decorations.rs" + +[[example]] +name = "drag_window" +path = "examples/drag_window.rs" + +[[example]] +name = "fullscreen" +path = "examples/fullscreen.rs" + +[[example]] +name = "handling_close" +path = "examples/handling_close.rs" + +[[example]] +name = "min_max_size" +path = "examples/min_max_size.rs" + +[[example]] +name = "minimize" +path = "examples/minimize.rs" + +[[example]] +name = "monitor_list" +path = "examples/monitor_list.rs" + +[[example]] +name = "mouse_wheel" +path = "examples/mouse_wheel.rs" + +[[example]] +name = "multithreaded" +path = "examples/multithreaded.rs" + +[[example]] +name = "multiwindow" +path = "examples/multiwindow.rs" + +[[example]] +name = "overlay" +path = "examples/overlay.rs" + +[[example]] +name = "parentwindow" +path = "examples/parentwindow.rs" + +[[example]] +name = "progress_bar" +path = "examples/progress_bar.rs" + +[[example]] +name = "reopen_event" +path = "examples/reopen_event.rs" + +[[example]] +name = "request_redraw" +path = "examples/request_redraw.rs" + +[[example]] +name = "request_redraw_threaded" +path = "examples/request_redraw_threaded.rs" + +[[example]] +name = "resizable" +path = "examples/resizable.rs" + +[[example]] +name = "set_ime_position" +path = "examples/set_ime_position.rs" + +[[example]] +name = "theme" +path = "examples/theme.rs" + +[[example]] +name = "timer" +path = "examples/timer.rs" + +[[example]] +name = "transparent" +path = "examples/transparent.rs" + +[[example]] +name = "video_modes" +path = "examples/video_modes.rs" + +[[example]] +name = "window" +path = "examples/window.rs" + +[[example]] +name = "window_debug" +path = "examples/window_debug.rs" + +[[example]] +name = "window_icon" +path = "examples/window_icon.rs" + +[[example]] +name = "window_run_return" +path = "examples/window_run_return.rs" + +[dependencies.bitflags] +version = "2" + +[dependencies.crossbeam-channel] +version = "0.5" + +[dependencies.dpi] +version = "0.1" + +[dependencies.lazy_static] +version = "1" + +[dependencies.libc] +version = "0.2" + +[dependencies.log] +version = "0.4" + +[dependencies.rwh_04] +version = "0.4" +optional = true +package = "raw-window-handle" + +[dependencies.rwh_05] +version = "0.5" +features = ["std"] +optional = true +package = "raw-window-handle" + +[dependencies.rwh_06] +version = "0.6" +features = ["std"] +optional = true +package = "raw-window-handle" + +[dependencies.serde] +version = "1" +features = ["serde_derive"] +optional = true + +[dependencies.url] +version = "2" + +[dev-dependencies.env_logger] +version = "0.11" + +[dev-dependencies.image] +version = "0.25" + +[target.'cfg(any(target_os = "android", target_os = "windows"))'.dependencies.once_cell] +version = "1" + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies.block2] +version = "0.6" + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies.objc2] +version = "0.6" + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.dlopen2] +version = "0.8.0" + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gdkwayland-sys] +version = "0.18.0" + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gdkx11-sys] +version = "0.18" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.gtk] +version = "0.18" + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.parking_lot] +version = "0.12" + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies.x11-dl] +version = "2.21" +optional = true + +[target.'cfg(target_os = "android")'.dependencies.jni] +version = "0.21" + +[target.'cfg(target_os = "android")'.dependencies.ndk] +version = "0.9" + +[target.'cfg(target_os = "android")'.dependencies.ndk-context] +version = "0.1" + +[target.'cfg(target_os = "android")'.dependencies.ndk-sys] +version = "0.6" + +[target.'cfg(target_os = "android")'.dependencies.tao-macros] +version = "0.1.0" + +[target.'cfg(target_os = "macos")'.dependencies.core-foundation] +version = "0.10" + +[target.'cfg(target_os = "macos")'.dependencies.core-graphics] +version = "0.24" + +[target.'cfg(target_os = "macos")'.dependencies.dispatch] +version = "0.2" + +[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit] +version = "0.3" +features = [ + "std", + "objc2-core-foundation", + "NSApplication", + "NSButton", + "NSColor", + "NSControl", + "NSEvent", + "NSGraphics", + "NSImage", + "NSOpenGLView", + "NSPasteboard", + "NSResponder", + "NSRunningApplication", + "NSScreen", + "NSView", + "NSWindow", + "NSUserActivity", +] +default-features = false + +[target.'cfg(target_os = "macos")'.dependencies.objc2-foundation] +version = "0.3" +features = [ + "std", + "NSArray", + "NSAttributedString", + "NSAutoreleasePool", + "NSDate", + "NSDictionary", + "NSEnumerator", + "NSGeometry", + "NSObjCRuntime", + "NSRange", + "NSString", + "NSThread", + "NSURL", +] +default-features = false + +[target.'cfg(target_os = "macos")'.dependencies.scopeguard] +version = "1.2" + +[target.'cfg(target_os = "windows")'.dependencies.parking_lot] +version = "0.12" + +[target.'cfg(target_os = "windows")'.dependencies.unicode-segmentation] +version = "1.11" + +[target.'cfg(target_os = "windows")'.dependencies.windows] +version = "0.61" +features = [ + "Win32_Devices_HumanInterfaceDevice", + "Win32_Foundation", + "Win32_Globalization", + "Win32_Graphics_Dwm", + "Win32_Graphics_Gdi", + "Win32_System_Com", + "Win32_System_Com_StructuredStorage", + "Win32_System_DataExchange", + "Win32_System_Diagnostics_Debug", + "Win32_System_LibraryLoader", + "Win32_System_Memory", + "Win32_System_Ole", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", + "Win32_System_SystemInformation", + "Win32_UI_Accessibility", + "Win32_UI_Controls", + "Win32_UI_HiDpi", + "Win32_UI_Input_Ime", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Input_Pointer", + "Win32_UI_Input_Touch", + "Win32_UI_Shell", + "Win32_UI_TextServices", + "Win32_UI_WindowsAndMessaging", +] + +[target.'cfg(target_os = "windows")'.dependencies.windows-core] +version = "0.61" + +[target.'cfg(target_os = "windows")'.dependencies.windows-version] +version = "0.1" + +[target.'cfg(target_os = "windows")'.dev-dependencies.softbuffer] +version = "0.4" diff --git a/vendor/tao/Cargo.toml.orig b/vendor/tao/Cargo.toml.orig new file mode 100644 index 0000000000..5b0f5d2c44 --- /dev/null +++ b/vendor/tao/Cargo.toml.orig @@ -0,0 +1,158 @@ +[package] +name = "tao" +version = "0.34.3" +description = "Cross-platform window manager library." +authors = [ + "Tauri Programme within The Commons Conservancy", + "The winit contributors" +] +edition = "2021" +rust-version = "1.74" +keywords = [ "windowing" ] +license = "Apache-2.0" +readme = "README.md" +repository = "https://github.com/tauri-apps/tao" +documentation = "https://docs.rs/tao" +categories = [ "gui" ] +include = ["/README.md", "src/**/*.rs", "examples/**/*.rs", "LICENSE*"] + +[package.metadata.docs.rs] +features = [ "rwh_04", "rwh_05", "rwh_06", "serde", "x11" ] +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "i686-pc-windows-msvc", + "x86_64-pc-windows-msvc", + "i686-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin" +] + +[features] +default = [ "rwh_06", "x11" ] +serde = [ "dep:serde", "dpi/serde" ] +rwh_04 = [ "dep:rwh_04" ] +rwh_05 = [ "dep:rwh_05" ] +rwh_06 = [ "dep:rwh_06" ] +x11 = [ "dep:gdkx11-sys", "dep:x11-dl" ] + +[workspace] +members = [ "tao-macros" ] + +[dependencies] +lazy_static = "1" +libc = "0.2" +log = "0.4" +serde = { version = "1", optional = true, features = [ "serde_derive" ] } +rwh_04 = { package = "raw-window-handle", version = "0.4", optional = true } +rwh_05 = { package = "raw-window-handle", version = "0.5", features = [ "std" ], optional = true } +rwh_06 = { package = "raw-window-handle", version = "0.6", features = [ "std" ], optional = true } +bitflags = "2" +crossbeam-channel = "0.5" +url = "2" +dpi = "0.1" + +[dev-dependencies] +image = "0.25" +env_logger = "0.11" + +[target."cfg(target_os = \"windows\")".dev-dependencies] +softbuffer = "0.4" + +[target."cfg(target_os = \"windows\")".dependencies] +parking_lot = "0.12" +unicode-segmentation = "1.11" +windows-version = "0.1" +windows-core = "0.61" + + [target."cfg(target_os = \"windows\")".dependencies.windows] + version = "0.61" + features = [ + "Win32_Devices_HumanInterfaceDevice", + "Win32_Foundation", + "Win32_Globalization", + "Win32_Graphics_Dwm", + "Win32_Graphics_Gdi", + "Win32_System_Com", + "Win32_System_Com_StructuredStorage", + "Win32_System_DataExchange", + "Win32_System_Diagnostics_Debug", + "Win32_System_LibraryLoader", + "Win32_System_Memory", + "Win32_System_Ole", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", + "Win32_System_SystemInformation", + "Win32_UI_Accessibility", + "Win32_UI_Controls", + "Win32_UI_HiDpi", + "Win32_UI_Input_Ime", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Input_Pointer", + "Win32_UI_Input_Touch", + "Win32_UI_Shell", + "Win32_UI_TextServices", + "Win32_UI_WindowsAndMessaging" +] + +[target."cfg(any(target_os = \"android\", target_os = \"windows\"))".dependencies] +once_cell = "1" + +[target."cfg(target_os = \"android\")".dependencies] +jni = "0.21" +ndk = "0.9" +ndk-sys = "0.6" +ndk-context = "0.1" +tao-macros = { version = "0.1.0", path = "./tao-macros" } + +[target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies] +objc2 = "0.6" +block2 = "0.6" + +[target."cfg(target_os = \"macos\")".dependencies] +objc2-foundation = { version = "0.3", default-features = false, features = [ + "std", + "NSArray", + "NSAttributedString", + "NSAutoreleasePool", + "NSDate", + "NSDictionary", + "NSEnumerator", + "NSGeometry", + "NSObjCRuntime", + "NSRange", + "NSString", + "NSThread", + "NSURL", +] } +objc2-app-kit = { version = "0.3", default-features = false, features = [ + "std", + "objc2-core-foundation", + "NSApplication", + "NSButton", + "NSColor", + "NSControl", + "NSEvent", + "NSGraphics", + "NSImage", + "NSOpenGLView", + "NSPasteboard", + "NSResponder", + "NSRunningApplication", + "NSScreen", + "NSView", + "NSWindow", + "NSUserActivity" +] } +core-foundation = "0.10" +core-graphics = "0.24" +dispatch = "0.2" +scopeguard = "1.2" + +[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] +gtk = "0.18" +gdkx11-sys = { version = "0.18", optional = true } +gdkwayland-sys = "0.18.0" +x11-dl = { version = "2.21", optional = true } +parking_lot = "0.12" +dlopen2 = "0.8.0" diff --git a/vendor/tao/LICENSE b/vendor/tao/LICENSE new file mode 100644 index 0000000000..ad410e1130 --- /dev/null +++ b/vendor/tao/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/vendor/tao/LICENSE.spdx b/vendor/tao/LICENSE.spdx new file mode 100644 index 0000000000..ac886c6d67 --- /dev/null +++ b/vendor/tao/LICENSE.spdx @@ -0,0 +1,18 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tao +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageCopyrightText: 2021-2023, The Tauri Programme in the Commons Conservancy +PackageSummary: Tao is the official, rust-based window manager for Wry. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2020-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tao +PackageDownloadLocation: git+https://github.com/tauri-apps/tao.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tao.git +Creator: Person: Daniel Thompson-Yvetot diff --git a/vendor/tao/README.md b/vendor/tao/README.md new file mode 100644 index 0000000000..a7d5046194 --- /dev/null +++ b/vendor/tao/README.md @@ -0,0 +1,80 @@ +TAO - Window Creation Library + +[![](https://img.shields.io/crates/v/tao?style=flat-square)](https://crates.io/crates/tao) +[![](https://img.shields.io/docsrs/tao?style=flat-square)](https://docs.rs/tao/) +[![License](https://img.shields.io/badge/License-Apache%202-green.svg)](https://opencollective.com/tauri) +[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S) +[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app) +[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation) +[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri) + +Cross-platform application window creation library in Rust that supports all major platforms like +Windows, macOS, Linux, iOS and Android. Built for you, maintained for Tauri. + +### Cargo Features + +TAO provides the following features, which can be enabled in your `Cargo.toml` file: + +- `serde`: Enables serialization/deserialization of certain types with [Serde](https://crates.io/crates/serde). + +## Platform-specific notes + +### Android + +This library makes use of the [ndk-rs](https://github.com/rust-windowing/android-ndk-rs) crates, refer to that repo for more documentation. + +Running on an Android device needs a dynamic system library, add this to Cargo.toml: + +```toml +[[example]] +name = "request_redraw_threaded" +crate-type = ["cdylib"] +``` + +And add this to the example file to add the native activity glue: + +```rust +#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] +fn main() { + ... +} +``` + +And run the application with `cargo apk run --example request_redraw_threaded` + +### Linux + +Gtk and its related libraries are used to build the support of Linux. Be sure to install following packages before building: + +#### Arch Linux / Manjaro: + +```bash +sudo pacman -S gtk3 +``` + +#### Debian / Ubuntu: + +```bash +sudo apt install libgtk-3-dev +``` + +### Acknowledgement + +This is a fork of [winit](https://crates.io/crates/winit) which replaces Linux's port to Gtk. +In the future, we want to make these features more modular as separate crates. So we can switch back to winit and also benefit the whole community. + +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). diff --git a/vendor/tao/examples/control_flow.rs b/vendor/tao/examples/control_flow.rs new file mode 100644 index 0000000000..45148bfd23 --- /dev/null +++ b/vendor/tao/examples/control_flow.rs @@ -0,0 +1,120 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{thread, time}; + +use tao::{ + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::Key, + window::WindowBuilder, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Mode { + Wait, + WaitUntil, + Poll, +} + +const WAIT_TIME: time::Duration = time::Duration::from_millis(100); +const POLL_SLEEP_TIME: time::Duration = time::Duration::from_millis(100); + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + + println!("Press '1' to switch to Wait mode."); + println!("Press '2' to switch to WaitUntil mode."); + println!("Press '3' to switch to Poll mode."); + println!("Press 'R' to toggle request_redraw() calls."); + println!("Press 'Esc' to close the window."); + + let event_loop = EventLoop::new(); + let window = WindowBuilder::new() + .with_title("Press 1, 2, 3 to change control flow mode. Press R to toggle redraw requests.") + .build(&event_loop) + .unwrap(); + + let mut mode = Mode::Wait; + let mut request_redraw = false; + let mut wait_cancelled = false; + let mut close_requested = false; + + event_loop.run(move |event, _, control_flow| { + use tao::event::StartCause; + println!("{event:?}"); + match event { + Event::NewEvents(start_cause) => { + wait_cancelled = match start_cause { + StartCause::WaitCancelled { .. } => mode == Mode::WaitUntil, + _ => false, + } + } + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => { + close_requested = true; + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key, + state: ElementState::Pressed, + .. + }, + .. + } => { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + if Key::Character("1") == logical_key { + mode = Mode::Wait; + println!("\nmode: {mode:?}\n"); + } + if Key::Character("2") == logical_key { + mode = Mode::WaitUntil; + println!("\nmode: {mode:?}\n"); + } + if Key::Character("3") == logical_key { + mode = Mode::Poll; + println!("\nmode: {mode:?}\n"); + } + if Key::Character("r") == logical_key { + request_redraw = !request_redraw; + println!("\nrequest_redraw: {request_redraw}\n"); + } + if Key::Escape == logical_key { + close_requested = true; + } + } + _ => {} + }, + Event::MainEventsCleared => { + if request_redraw && !wait_cancelled && !close_requested { + window.request_redraw(); + } + if close_requested { + *control_flow = ControlFlow::Exit; + } + } + Event::RedrawRequested(_window_id) => {} + Event::RedrawEventsCleared => { + *control_flow = match mode { + Mode::Wait => ControlFlow::Wait, + Mode::WaitUntil => { + if wait_cancelled { + *control_flow + } else { + ControlFlow::WaitUntil(time::Instant::now() + WAIT_TIME) + } + } + Mode::Poll => { + thread::sleep(POLL_SLEEP_TIME); + ControlFlow::Poll + } + }; + } + _ => (), + } + }); +} diff --git a/vendor/tao/examples/cursor.rs b/vendor/tao/examples/cursor.rs new file mode 100644 index 0000000000..48a91d6a9e --- /dev/null +++ b/vendor/tao/examples/cursor.rs @@ -0,0 +1,89 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::{CursorIcon, WindowBuilder}, +}; + +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new().build(&event_loop).unwrap(); + window.set_title("A fantastic window!"); + + let mut cursor_idx = 0; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: + WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Pressed, + .. + }, + .. + }, + .. + } => { + println!("Setting cursor to \"{:?}\"", CURSORS[cursor_idx]); + window.set_cursor_icon(CURSORS[cursor_idx]); + if cursor_idx < CURSORS.len() - 1 { + cursor_idx += 1; + } else { + cursor_idx = 0; + } + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } + }); +} + +const CURSORS: &[CursorIcon] = &[ + CursorIcon::Default, + CursorIcon::Crosshair, + CursorIcon::Hand, + CursorIcon::Arrow, + CursorIcon::Move, + CursorIcon::Text, + CursorIcon::Wait, + CursorIcon::Help, + CursorIcon::Progress, + CursorIcon::NotAllowed, + CursorIcon::ContextMenu, + CursorIcon::Cell, + CursorIcon::VerticalText, + CursorIcon::Alias, + CursorIcon::Copy, + CursorIcon::NoDrop, + CursorIcon::Grab, + CursorIcon::Grabbing, + CursorIcon::AllScroll, + CursorIcon::ZoomIn, + CursorIcon::ZoomOut, + CursorIcon::EResize, + CursorIcon::NResize, + CursorIcon::NeResize, + CursorIcon::NwResize, + CursorIcon::SResize, + CursorIcon::SeResize, + CursorIcon::SwResize, + CursorIcon::WResize, + CursorIcon::EwResize, + CursorIcon::NsResize, + CursorIcon::NeswResize, + CursorIcon::NwseResize, + CursorIcon::ColResize, + CursorIcon::RowResize, +]; diff --git a/vendor/tao/examples/cursor_grab.rs b/vendor/tao/examples/cursor_grab.rs new file mode 100644 index 0000000000..a939d82010 --- /dev/null +++ b/vendor/tao/examples/cursor_grab.rs @@ -0,0 +1,66 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + event::{DeviceEvent, ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, ModifiersState}, + window::WindowBuilder, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("Super Cursor Grab'n'Hide Simulator 9000") + .build(&event_loop) + .unwrap(); + + let mut modifiers = ModifiersState::default(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: key, + state: ElementState::Released, + .. + }, + .. + } => { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + match key { + Key::Escape => *control_flow = ControlFlow::Exit, + Key::Character(ch) => match ch.to_lowercase().as_str() { + "g" => window.set_cursor_grab(!modifiers.shift_key()).unwrap(), + "h" => window.set_cursor_visible(modifiers.shift_key()), + _ => (), + }, + _ => (), + } + } + WindowEvent::ModifiersChanged(m) => modifiers = m, + _ => (), + }, + Event::DeviceEvent { event, .. } => match event { + DeviceEvent::MouseMotion { delta, .. } => println!("mouse moved: {delta:?}"), + DeviceEvent::Button { button, state, .. } => match state { + ElementState::Pressed => println!("mouse button {button} pressed"), + ElementState::Released => println!("mouse button {button} released"), + _ => (), + }, + _ => (), + }, + _ => (), + } + }); +} diff --git a/vendor/tao/examples/custom_events.rs b/vendor/tao/examples/custom_events.rs new file mode 100644 index 0000000000..f94d001135 --- /dev/null +++ b/vendor/tao/examples/custom_events.rs @@ -0,0 +1,51 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoopBuilder}, + window::WindowBuilder, + }; + + #[derive(Debug, Clone, Copy)] + enum CustomEvent { + Timer, + } + + let event_loop = EventLoopBuilder::::with_user_event().build(); + + let _window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + // `EventLoopProxy` allows you to dispatch custom events to the main Tao event + // loop from any thread. + let event_loop_proxy = event_loop.create_proxy(); + + std::thread::spawn(move || { + // Wake up the `event_loop` once every second and dispatch a custom event + // from a different thread. + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + event_loop_proxy.send_event(CustomEvent::Timer).ok(); + } + }); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::UserEvent(event) => println!("user event: {event:?}"), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } + }); +} diff --git a/vendor/tao/examples/decorations.rs b/vendor/tao/examples/decorations.rs new file mode 100644 index 0000000000..d9025e2b46 --- /dev/null +++ b/vendor/tao/examples/decorations.rs @@ -0,0 +1,51 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + dpi::LogicalSize, + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::KeyCode, + window::WindowBuilder, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let mut decorations = true; + + let window = WindowBuilder::new() + .with_title("Hit space to toggle decorations.") + .with_inner_size(LogicalSize::new(400.0, 200.0)) + .with_decorations(decorations) + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::KeyboardInput { + event: + KeyEvent { + physical_key: KeyCode::Space, + state: ElementState::Released, + .. + }, + .. + } => { + decorations = !decorations; + println!("Decorations: {decorations}"); + window.set_decorations(decorations); + } + _ => (), + }, + _ => (), + }; + }); +} diff --git a/vendor/tao/examples/drag_window.rs b/vendor/tao/examples/drag_window.rs new file mode 100644 index 0000000000..895c360d39 --- /dev/null +++ b/vendor/tao/examples/drag_window.rs @@ -0,0 +1,78 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + event::{ElementState, Event, KeyEvent, MouseButton, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::Key, + window::{Window, WindowBuilder, WindowId}, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window_1 = WindowBuilder::new().build(&event_loop).unwrap(); + let window_2 = WindowBuilder::new().build(&event_loop).unwrap(); + + let mut switched = false; + let mut entered_id = window_2.id(); + + event_loop.run(move |event, _, control_flow| match event { + Event::NewEvents(StartCause::Init) => { + eprintln!("Switch which window is to be dragged by pressing \"x\".") + } + Event::WindowEvent { + event, window_id, .. + } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + .. + } => { + let window = if (window_id == window_1.id() && switched) + || (window_id == window_2.id() && !switched) + { + &window_2 + } else { + &window_1 + }; + + window.drag_window().unwrap() + } + WindowEvent::CursorEntered { .. } => { + entered_id = window_id; + name_windows(entered_id, switched, &window_1, &window_2) + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Released, + logical_key: Key::Character("x"), + .. + }, + .. + } => { + switched = !switched; + name_windows(entered_id, switched, &window_1, &window_2); + println!("Switched!") + } + _ => (), + }, + _ => (), + }); +} + +fn name_windows(window_id: WindowId, switched: bool, window_1: &Window, window_2: &Window) { + let (drag_target, other) = + if (window_id == window_1.id() && switched) || (window_id == window_2.id() && !switched) { + (&window_2, &window_1) + } else { + (&window_1, &window_2) + }; + drag_target.set_title("drag target"); + other.set_title("tao window"); +} diff --git a/vendor/tao/examples/fullscreen.rs b/vendor/tao/examples/fullscreen.rs new file mode 100644 index 0000000000..c5d8bfacd7 --- /dev/null +++ b/vendor/tao/examples/fullscreen.rs @@ -0,0 +1,129 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::io::{stdin, stdout, Write}; + +use tao::{ + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::Key, + monitor::{MonitorHandle, VideoMode}, + window::{Fullscreen, WindowBuilder}, +}; +#[allow(clippy::single_match)] +#[allow(clippy::ok_expect)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + print!("Please choose the fullscreen mode: (1) exclusive, (2) borderless, (3) borderless on current monitor: "); + stdout().flush().unwrap(); + + let mut num = String::new(); + stdin().read_line(&mut num).unwrap(); + let num = num.trim().parse().ok().expect("Please enter a number"); + + let fullscreen = Some(match num { + 1 => Fullscreen::Exclusive(prompt_for_video_mode(&prompt_for_monitor(&event_loop))), + 2 => Fullscreen::Borderless(Some(prompt_for_monitor(&event_loop))), + 3 => Fullscreen::Borderless(None), + _ => panic!("Please enter a valid number"), + }); + + let mut decorations = true; + + let window = WindowBuilder::new() + .with_title("Hello world!") + .with_fullscreen(fullscreen.clone()) + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key, + state: ElementState::Pressed, + .. + }, + .. + } => { + if Key::Escape == logical_key { + *control_flow = ControlFlow::Exit + } + + if Key::Character("f") == logical_key { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + window.set_fullscreen(fullscreen.clone()); + } + } + + if Key::Character("s") == logical_key { + println!("window.fullscreen {:?}", window.fullscreen()); + } + if Key::Character("m") == logical_key { + let is_maximized = window.is_maximized(); + window.set_maximized(!is_maximized); + } + if Key::Character("d") == logical_key { + decorations = !decorations; + window.set_decorations(decorations); + } + } + _ => (), + }, + _ => {} + } + }); +} + +// Enumerate monitors and prompt user to choose one +fn prompt_for_monitor(event_loop: &EventLoop<()>) -> MonitorHandle { + for (num, monitor) in event_loop.available_monitors().enumerate() { + println!("Monitor #{}: {:?}", num, monitor.name()); + } + + print!("Please write the number of the monitor to use: "); + stdout().flush().unwrap(); + + let mut num = String::new(); + stdin().read_line(&mut num).unwrap(); + let num = num.trim().parse().expect("Please enter a number"); + let monitor = event_loop + .available_monitors() + .nth(num) + .expect("Please enter a valid ID"); + + println!("Using {:?}", monitor.name()); + + monitor +} + +fn prompt_for_video_mode(monitor: &MonitorHandle) -> VideoMode { + for (i, video_mode) in monitor.video_modes().enumerate() { + println!("Video mode #{i}: {video_mode}"); + } + + print!("Please write the number of the video mode to use: "); + stdout().flush().unwrap(); + + let mut num = String::new(); + stdin().read_line(&mut num).unwrap(); + let num = num.trim().parse().expect("Please enter a number"); + let video_mode = monitor + .video_modes() + .nth(num) + .expect("Please enter a valid ID"); + + println!("Using {video_mode}"); + + video_mode +} diff --git a/vendor/tao/examples/handling_close.rs b/vendor/tao/examples/handling_close.rs new file mode 100644 index 0000000000..278a646fc7 --- /dev/null +++ b/vendor/tao/examples/handling_close.rs @@ -0,0 +1,87 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::Key, + window::WindowBuilder, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let _window = WindowBuilder::new() + .with_title("Your faithful window") + .build(&event_loop) + .unwrap(); + + let mut close_requested = false; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::CloseRequested => { + // `CloseRequested` is sent when the close button on the window is pressed (or + // through whatever other mechanisms the window manager provides for closing a + // window). If you don't handle this event, the close button won't actually do + // anything. + + // A common thing to do here is prompt the user if they have unsaved work. + // Creating a proper dialog box for that is far beyond the scope of this + // example, so here we'll just respond to the Y and N keys. + println!("Are you ready to bid your window farewell? [Y/N]"); + close_requested = true; + + // In applications where you can safely close the window without further + // action from the user, this is generally where you'd handle cleanup before + // closing the window. How to close the window is detailed in the handler for + // the Y key. + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: Key::Character(char), + state: ElementState::Released, + .. + }, + .. + } => { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + match char { + "y" => { + if close_requested { + // This is where you'll want to do any cleanup you need. + println!("Buh-bye!"); + + // For a single-window application like this, you'd normally just + // break out of the event loop here. If you wanted to keep running the + // event loop (i.e. if it's a multi-window application), you need to + // drop the window. That closes it, and results in `Destroyed` being + // sent. + *control_flow = ControlFlow::Exit; + } + } + "n" => { + if close_requested { + println!("Your window will continue to stay by your side."); + close_requested = false; + } + } + _ => (), + } + } + _ => (), + } + } + _ => (), + } + }); +} diff --git a/vendor/tao/examples/min_max_size.rs b/vendor/tao/examples/min_max_size.rs new file mode 100644 index 0000000000..6ad631faa8 --- /dev/null +++ b/vendor/tao/examples/min_max_size.rs @@ -0,0 +1,87 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + dpi::LogicalUnit, + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::Key, + window::{WindowBuilder, WindowSizeConstraints}, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let min_width = 400.0; + let max_width = 800.0; + let min_height = 200.0; + let max_height = 400.0; + let mut size_constraints = WindowSizeConstraints::default(); + + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + eprintln!("constraint keys:"); + eprintln!(" (E) Toggle the min width"); + eprintln!(" (F) Toggle the max width"); + eprintln!(" (P) Toggle the min height"); + eprintln!(" (V) Toggle the max height"); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + + Event::WindowEvent { + event: + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: Key::Character(key_str), + state: ElementState::Released, + .. + }, + .. + }, + .. + } => match key_str { + "e" => { + size_constraints.min_width = size_constraints + .min_width + .is_none() + .then_some(LogicalUnit::new(min_width).into()); + window.set_inner_size_constraints(size_constraints); + } + "f" => { + size_constraints.max_width = size_constraints + .max_width + .is_none() + .then_some(LogicalUnit::new(max_width).into()); + window.set_inner_size_constraints(size_constraints); + } + "p" => { + size_constraints.min_height = size_constraints + .min_height + .is_none() + .then_some(LogicalUnit::new(min_height).into()); + window.set_inner_size_constraints(size_constraints); + } + "v" => { + size_constraints.max_height = size_constraints + .max_height + .is_none() + .then_some(LogicalUnit::new(max_height).into()); + window.set_inner_size_constraints(size_constraints); + } + _ => {} + }, + _ => (), + } + }); +} diff --git a/vendor/tao/examples/minimize.rs b/vendor/tao/examples/minimize.rs new file mode 100644 index 0000000000..76ceeb7b51 --- /dev/null +++ b/vendor/tao/examples/minimize.rs @@ -0,0 +1,47 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +extern crate tao; + +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::Key, + window::WindowBuilder, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + + // Keyboard input event to handle minimize via a hotkey + Event::WindowEvent { + event: WindowEvent::KeyboardInput { event, .. }, + window_id, + .. + } if window_id == window.id() && Key::Character("m") == event.logical_key => { + // Pressing the 'm' key will minimize the window + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + window.set_minimized(true); + } + _ => (), + } + }); +} diff --git a/vendor/tao/examples/monitor_list.rs b/vendor/tao/examples/monitor_list.rs new file mode 100644 index 0000000000..02ea917d65 --- /dev/null +++ b/vendor/tao/examples/monitor_list.rs @@ -0,0 +1,14 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{event_loop::EventLoop, window::WindowBuilder}; + +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + dbg!(window.available_monitors().collect::>()); + dbg!(window.primary_monitor()); +} diff --git a/vendor/tao/examples/mouse_wheel.rs b/vendor/tao/examples/mouse_wheel.rs new file mode 100644 index 0000000000..8997a5e6f0 --- /dev/null +++ b/vendor/tao/examples/mouse_wheel.rs @@ -0,0 +1,54 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + event::{DeviceEvent, Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +#[allow(clippy::collapsible_match)] +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("Mouse Wheel events") + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + _ => (), + }, + Event::DeviceEvent { event, .. } => match event { + DeviceEvent::MouseWheel { delta, .. } => match delta { + tao::event::MouseScrollDelta::LineDelta(x, y) => { + println!("mouse wheel Line Delta: ({x},{y})"); + let pixels_per_line = 120.0; + let mut pos = window.outer_position().unwrap(); + pos.x -= (x * pixels_per_line) as i32; + pos.y -= (y * pixels_per_line) as i32; + window.set_outer_position(pos) + } + tao::event::MouseScrollDelta::PixelDelta(p) => { + println!("mouse wheel Pixel Delta: ({},{})", p.x, p.y); + let mut pos = window.outer_position().unwrap(); + pos.x -= p.x as i32; + pos.y -= p.y as i32; + window.set_outer_position(pos) + } + _ => (), + }, + _ => (), + }, + _ => (), + } + }); +} diff --git a/vendor/tao/examples/multithreaded.rs b/vendor/tao/examples/multithreaded.rs new file mode 100644 index 0000000000..7dfe996325 --- /dev/null +++ b/vendor/tao/examples/multithreaded.rs @@ -0,0 +1,187 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#[allow(clippy::single_match)] +#[allow(clippy::iter_nth)] +fn main() { + use std::{collections::HashMap, sync::mpsc, thread, time::Duration}; + + use tao::{ + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, ModifiersState}, + window::{CursorIcon, Fullscreen, WindowBuilder}, + }; + + const WINDOW_COUNT: usize = 3; + const WINDOW_SIZE: PhysicalSize = PhysicalSize::new(600, 400); + + env_logger::init(); + let event_loop = EventLoop::new(); + let mut window_senders = HashMap::with_capacity(WINDOW_COUNT); + for _ in 0..WINDOW_COUNT { + let window = WindowBuilder::new() + .with_inner_size(WINDOW_SIZE) + .build(&event_loop) + .unwrap(); + + let mut video_modes: Vec<_> = window.current_monitor().unwrap().video_modes().collect(); + let mut video_mode_id = 0usize; + + let (tx, rx) = mpsc::channel(); + window_senders.insert(window.id(), tx); + let mut modifiers = ModifiersState::default(); + thread::spawn(move || { + while let Ok(event) = rx.recv() { + match event { + WindowEvent::Moved { .. } => { + // We need to update our chosen video mode if the window + // was moved to an another monitor, so that the window + // appears on this monitor instead when we go fullscreen + let previous_video_mode = video_modes.iter().nth(video_mode_id).cloned(); + video_modes = window.current_monitor().unwrap().video_modes().collect(); + video_mode_id = video_mode_id.min(video_modes.len()); + let video_mode = video_modes.iter().nth(video_mode_id); + + // Different monitors may support different video modes, + // and the index we chose previously may now point to a + // completely different video mode, so notify the user + if video_mode != previous_video_mode.as_ref() { + println!( + "Window moved to another monitor, picked video mode: {}", + video_modes.iter().nth(video_mode_id).unwrap() + ); + } + } + WindowEvent::ModifiersChanged(mod_state) => { + modifiers = mod_state; + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Released, + logical_key: key, + .. + }, + .. + } => { + use Key::{ArrowLeft, ArrowRight, Character}; + window.set_title(&format!("{key:?}")); + let state = !modifiers.shift_key(); + match &key { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + Character(string) => match string.to_lowercase().as_str() { + "a" => window.set_always_on_top(state), + "c" => window.set_cursor_icon(match state { + true => CursorIcon::Progress, + false => CursorIcon::Default, + }), + "d" => window.set_decorations(!state), + "f" => window.set_fullscreen(match (state, modifiers.alt_key()) { + (true, false) => Some(Fullscreen::Borderless(None)), + (true, true) => Some(Fullscreen::Exclusive( + video_modes.iter().nth(video_mode_id).unwrap().clone(), + )), + (false, _) => None, + }), + "g" => window.set_cursor_grab(state).unwrap(), + "h" => window.set_cursor_visible(!state), + "i" => { + println!("Info:"); + println!("-> outer_position : {:?}", window.outer_position()); + println!("-> inner_position : {:?}", window.inner_position()); + println!("-> outer_size : {:?}", window.outer_size()); + println!("-> inner_size : {:?}", window.inner_size()); + println!("-> fullscreen : {:?}", window.fullscreen()); + } + "l" => window.set_min_inner_size(match state { + true => Some(WINDOW_SIZE), + false => None, + }), + "m" => window.set_maximized(state), + "p" => window.set_outer_position({ + let mut position = window.outer_position().unwrap(); + let sign = if state { 1 } else { -1 }; + position.x += 10 * sign; + position.y += 10 * sign; + position + }), + "q" => window.request_redraw(), + "r" => window.set_resizable(state), + "s" => window.set_inner_size(match state { + true => PhysicalSize::new(WINDOW_SIZE.width + 100, WINDOW_SIZE.height + 100), + false => WINDOW_SIZE, + }), + "w" => { + if let Size::Physical(size) = WINDOW_SIZE.into() { + window + .set_cursor_position(Position::Physical(PhysicalPosition::new( + size.width as i32 / 2, + size.height as i32 / 2, + ))) + .unwrap() + } + } + "z" => { + window.set_visible(false); + thread::sleep(Duration::from_secs(1)); + window.set_visible(true); + } + _ => (), + }, + ArrowRight | ArrowLeft => { + video_mode_id = match &key { + ArrowLeft => video_mode_id.saturating_sub(1), + ArrowRight => (video_modes.len() - 1).min(video_mode_id + 1), + _ => unreachable!(), + }; + println!( + "Picking video mode: {}", + video_modes.iter().nth(video_mode_id).unwrap() + ); + } + _ => (), + } + } + _ => (), + } + } + }); + } + event_loop.run(move |event, _event_loop, control_flow| { + *control_flow = match !window_senders.is_empty() { + true => ControlFlow::Wait, + false => ControlFlow::Exit, + }; + match event { + Event::WindowEvent { + event, window_id, .. + } => match event { + WindowEvent::CloseRequested + | WindowEvent::Destroyed + | WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Released, + logical_key: Key::Escape, + .. + }, + .. + } => { + window_senders.remove(&window_id); + } + _ => { + if let Some(tx) = window_senders.get(&window_id) { + if let Some(event) = event.to_static() { + tx.send(event).unwrap(); + } + } + } + }, + _ => (), + } + }) +} diff --git a/vendor/tao/examples/multiwindow.rs b/vendor/tao/examples/multiwindow.rs new file mode 100644 index 0000000000..32973057a5 --- /dev/null +++ b/vendor/tao/examples/multiwindow.rs @@ -0,0 +1,55 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; + +use tao::{ + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::Window, +}; + +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let mut windows = HashMap::new(); + for _ in 0..3 { + let window = Window::new(&event_loop).unwrap(); + windows.insert(window.id(), window); + } + + event_loop.run(move |event, event_loop, control_flow| { + *control_flow = ControlFlow::Wait; + + if let Event::WindowEvent { + event, window_id, .. + } = event + { + match event { + WindowEvent::CloseRequested => { + println!("Window {window_id:?} has received the signal to close"); + + // This drops the window, causing it to close. + windows.remove(&window_id); + + if windows.is_empty() { + *control_flow = ControlFlow::Exit; + } + } + WindowEvent::KeyboardInput { + event: KeyEvent { + state: ElementState::Pressed, + .. + }, + .. + } => { + let window = Window::new(event_loop).unwrap(); + windows.insert(window.id(), window); + } + _ => (), + } + } + }) +} diff --git a/vendor/tao/examples/overlay.rs b/vendor/tao/examples/overlay.rs new file mode 100644 index 0000000000..03f290c61a --- /dev/null +++ b/vendor/tao/examples/overlay.rs @@ -0,0 +1,128 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, ModifiersState}, + window::WindowBuilder, +}; + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +use tao::platform::unix::WindowExtUnix; + +#[cfg(target_os = "macos")] +use tao::platform::macos::WindowExtMacOS; + +#[cfg(target_os = "ios")] +use tao::platform::ios::WindowExtIOS; + +#[cfg(windows)] +use tao::{ + dpi::PhysicalSize, platform::windows::IconExtWindows, platform::windows::WindowExtWindows, + window::Icon, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + let mut modifiers = ModifiersState::default(); + + eprintln!("Key mappings:"); + #[cfg(windows)] + eprintln!(" [any key]: Show the Overlay Icon"); + #[cfg(not(windows))] + eprintln!(" [1-5]: Show a Badge count"); + eprintln!(" Ctrl+1: Clear"); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::WindowEvent { event, .. } => match event { + WindowEvent::ModifiersChanged(new_state) => { + modifiers = new_state; + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: Key::Character(key_str), + state: ElementState::Released, + .. + }, + .. + } => { + let _count = match key_str { + "1" => 1, + "2" => 2, + "3" => 3, + "4" => 4, + "5" => 5, + _ => 20, + }; + + if modifiers.is_empty() { + #[cfg(windows)] + { + let mut path = std::env::current_dir().unwrap(); + path.push("./examples/icon.ico"); + let icon = Icon::from_path(path, Some(PhysicalSize::new(32, 32))).unwrap(); + + window.set_overlay_icon(Some(&icon)); + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + window.set_badge_count(Some(_count), None); + + #[cfg(target_os = "macos")] + window.set_badge_label(_count.to_string().into()); + + #[cfg(target_os = "ios")] + window.set_badge_count(_count); + } else if modifiers.control_key() && key_str == "1" { + #[cfg(windows)] + window.set_overlay_icon(None); + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + window.set_badge_count(None, None); + + #[cfg(target_os = "macos")] + window.set_badge_label(None); + + #[cfg(target_os = "ios")] + window.set_badge_count(0); + } + } + _ => {} + }, + _ => {} + } + }); +} diff --git a/vendor/tao/examples/parentwindow.rs b/vendor/tao/examples/parentwindow.rs new file mode 100644 index 0000000000..5425651059 --- /dev/null +++ b/vendor/tao/examples/parentwindow.rs @@ -0,0 +1,70 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +fn main() { + use std::collections::HashMap; + #[cfg(target_os = "macos")] + use tao::platform::macos::{WindowBuilderExtMacOS, WindowExtMacOS}; + #[cfg(target_os = "linux")] + use tao::platform::unix::{WindowBuilderExtUnix, WindowExtUnix}; + #[cfg(target_os = "windows")] + use tao::platform::windows::{WindowBuilderExtWindows, WindowExtWindows}; + use tao::{ + dpi::LogicalSize, + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }; + env_logger::init(); + let event_loop = EventLoop::new(); + let mut windows = HashMap::new(); + let main_window = WindowBuilder::new().build(&event_loop).unwrap(); + + #[cfg(target_os = "macos")] + let parent_window = main_window.ns_window(); + #[cfg(target_os = "windows")] + let parent_window = main_window.hwnd(); + #[cfg(target_os = "linux")] + let parent_window = main_window.gtk_window(); + + let child_window_builder = WindowBuilder::new().with_inner_size(LogicalSize::new(200, 200)); + + #[cfg(any(target_os = "windows", target_os = "macos"))] + let child_window_builder = child_window_builder.with_parent_window(parent_window); + + #[cfg(target_os = "linux")] + let child_window_builder = child_window_builder.with_transient_for(parent_window); + + let child_window = child_window_builder.build(&event_loop).unwrap(); + + windows.insert(child_window.id(), child_window); + windows.insert(main_window.id(), main_window); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::NewEvents(StartCause::Init) => println!("TAO application started!"), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + .. + } => { + println!("Window {window_id:?} has received the signal to close"); + // This drop the window, causing it to close. + windows.remove(&window_id); + if windows.is_empty() { + *control_flow = ControlFlow::Exit; + } + } + _ => (), + }; + }) +} + +#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] +fn main() { + println!("This platform doesn't have the parent window support."); +} diff --git a/vendor/tao/examples/progress_bar.rs b/vendor/tao/examples/progress_bar.rs new file mode 100644 index 0000000000..c526a74cc8 --- /dev/null +++ b/vendor/tao/examples/progress_bar.rs @@ -0,0 +1,89 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, ModifiersState}, + window::{ProgressBarState, ProgressState, WindowBuilder}, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + let mut modifiers = ModifiersState::default(); + + eprintln!("Key mappings:"); + eprintln!(" [1-5]: Set progress to [0%, 25%, 50%, 75%, 100%]"); + eprintln!(" Ctrl+1: Set state to None"); + eprintln!(" Ctrl+2: Set state to Normal"); + eprintln!(" Ctrl+3: Set state to Indeterminate"); + eprintln!(" Ctrl+4: Set state to Paused"); + eprintln!(" Ctrl+5: Set state to Error"); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::WindowEvent { event, .. } => match event { + WindowEvent::ModifiersChanged(new_state) => { + modifiers = new_state; + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: Key::Character(key_str), + state: ElementState::Released, + .. + }, + .. + } => { + if modifiers.is_empty() { + let mut progress: u64 = 0; + match key_str { + "1" => progress = 0, + "2" => progress = 25, + "3" => progress = 50, + "4" => progress = 75, + "5" => progress = 100, + _ => {} + } + + window.set_progress_bar(ProgressBarState { + progress: Some(progress), + state: Some(ProgressState::Normal), + desktop_filename: None, + }); + } else if modifiers.control_key() { + let mut state = ProgressState::None; + match key_str { + "1" => state = ProgressState::None, + "2" => state = ProgressState::Normal, + "3" => state = ProgressState::Indeterminate, + "4" => state = ProgressState::Paused, + "5" => state = ProgressState::Error, + _ => {} + } + + window.set_progress_bar(ProgressBarState { + progress: None, + state: Some(state), + desktop_filename: None, + }); + } + } + _ => {} + }, + _ => {} + } + }); +} diff --git a/vendor/tao/examples/reopen_event.rs b/vendor/tao/examples/reopen_event.rs new file mode 100644 index 0000000000..5b14ef4a2a --- /dev/null +++ b/vendor/tao/examples/reopen_event.rs @@ -0,0 +1,45 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::Window, +}; + +#[allow(clippy::single_match)] +fn main() { + let event_loop = EventLoop::new(); + + let mut window = Some(Window::new(&event_loop).unwrap()); + + event_loop.run(move |event, event_loop, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + // drop the window + window = None; + } + Event::Reopen { + has_visible_windows, + .. + } => { + println!("on reopen, has visible windows: {has_visible_windows}"); + if !has_visible_windows { + window = Some(Window::new(event_loop).unwrap()) + } + } + Event::MainEventsCleared => { + if let Some(w) = &window { + w.request_redraw(); + } + } + _ => (), + } + }); +} diff --git a/vendor/tao/examples/request_redraw.rs b/vendor/tao/examples/request_redraw.rs new file mode 100644 index 0000000000..f3f563c6fc --- /dev/null +++ b/vendor/tao/examples/request_redraw.rs @@ -0,0 +1,43 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + event::{ElementState, Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + println!("{event:?}"); + + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::MouseInput { + state: ElementState::Released, + .. + } => { + window.request_redraw(); + } + _ => (), + }, + Event::RedrawRequested(_) => { + println!("\nredrawing!\n"); + } + _ => (), + } + }); +} diff --git a/vendor/tao/examples/request_redraw_threaded.rs b/vendor/tao/examples/request_redraw_threaded.rs new file mode 100644 index 0000000000..069d65d52a --- /dev/null +++ b/vendor/tao/examples/request_redraw_threaded.rs @@ -0,0 +1,45 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{thread, time}; + +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +#[allow(clippy::single_match)] +#[allow(clippy::collapsible_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + thread::spawn(move || loop { + thread::sleep(time::Duration::from_secs(1)); + window.request_redraw(); + }); + + event_loop.run(move |event, _, control_flow| { + println!("{event:?}"); + + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + _ => (), + }, + Event::RedrawRequested(_) => { + println!("\nredrawing!\n"); + } + _ => (), + } + }); +} diff --git a/vendor/tao/examples/resizable.rs b/vendor/tao/examples/resizable.rs new file mode 100644 index 0000000000..e0d8c16f24 --- /dev/null +++ b/vendor/tao/examples/resizable.rs @@ -0,0 +1,51 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + dpi::LogicalSize, + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::KeyCode, + window::WindowBuilder, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let mut resizable = false; + + let window = WindowBuilder::new() + .with_title("Hit space to toggle resizability.") + .with_inner_size(LogicalSize::new(400.0, 200.0)) + .with_resizable(resizable) + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::KeyboardInput { + event: + KeyEvent { + physical_key: KeyCode::Space, + state: ElementState::Released, + .. + }, + .. + } => { + resizable = !resizable; + println!("Resizable: {resizable}"); + window.set_resizable(resizable); + } + _ => (), + }, + _ => (), + }; + }); +} diff --git a/vendor/tao/examples/set_ime_position.rs b/vendor/tao/examples/set_ime_position.rs new file mode 100644 index 0000000000..2310f5c5a0 --- /dev/null +++ b/vendor/tao/examples/set_ime_position.rs @@ -0,0 +1,56 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + dpi::PhysicalPosition, + event::{ElementState, Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new().build(&event_loop).unwrap(); + window.set_title("A fantastic window!"); + + println!("Ime position will system default"); + println!("Click to set ime position to cursor's"); + + let mut cursor_position = PhysicalPosition::new(0.0, 0.0); + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CursorMoved { position, .. }, + .. + } => { + cursor_position = position; + } + Event::WindowEvent { + event: + WindowEvent::MouseInput { + state: ElementState::Released, + .. + }, + .. + } => { + println!( + "Setting ime position to {}, {}", + cursor_position.x, cursor_position.y + ); + window.set_ime_position(cursor_position); + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + *control_flow = ControlFlow::Exit; + } + _ => (), + } + }); +} diff --git a/vendor/tao/examples/theme.rs b/vendor/tao/examples/theme.rs new file mode 100644 index 0000000000..c0e3284b9a --- /dev/null +++ b/vendor/tao/examples/theme.rs @@ -0,0 +1,50 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{event::KeyEvent, keyboard::KeyCode}; + +fn main() { + use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::{Theme, WindowBuilder}, + }; + + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + // .with_theme(Some(tao::window::Theme::Light)) + .build(&event_loop) + .unwrap(); + + println!("Initial theme: {:?}", window.theme()); + println!("Press D for Dark Mode"); + println!("Press L for Light Mode"); + println!("Press A for Auto Mode"); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::KeyboardInput { + event: KeyEvent { physical_key, .. }, + .. + } => match physical_key { + KeyCode::KeyD => window.set_theme(Some(Theme::Dark)), + KeyCode::KeyL => window.set_theme(Some(Theme::Light)), + KeyCode::KeyA => window.set_theme(None), + _ => {} + }, + WindowEvent::ThemeChanged(theme) => { + println!("Theme is changed: {theme:?}") + } + _ => (), + } + } + }); +} diff --git a/vendor/tao/examples/timer.rs b/vendor/tao/examples/timer.rs new file mode 100644 index 0000000000..eb3ce6f38d --- /dev/null +++ b/vendor/tao/examples/timer.rs @@ -0,0 +1,43 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::time::{Duration, Instant}; + +use tao::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let _window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + let timer_length = Duration::new(1, 0); + + event_loop.run(move |event, _, control_flow| { + println!("{event:?}"); + + match event { + Event::NewEvents(StartCause::Init) => { + *control_flow = ControlFlow::WaitUntil(Instant::now() + timer_length) + } + Event::NewEvents(StartCause::ResumeTimeReached { .. }) => { + *control_flow = ControlFlow::WaitUntil(Instant::now() + timer_length); + println!("\nTimer\n"); + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } + }); +} diff --git a/vendor/tao/examples/transparent.rs b/vendor/tao/examples/transparent.rs new file mode 100644 index 0000000000..22c99c9a19 --- /dev/null +++ b/vendor/tao/examples/transparent.rs @@ -0,0 +1,66 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(windows)] +use std::{num::NonZeroU32, rc::Rc}; + +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_decorations(false) + .with_transparent(true) + .build(&event_loop) + .unwrap(); + + #[cfg(windows)] + let (window, _context, mut surface) = { + let window = Rc::new(window); + let context = softbuffer::Context::new(window.clone()).unwrap(); + let surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); + (window, context, surface) + }; + + window.set_title("A fantastic window!"); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + println!("{event:?}"); + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + + #[cfg(windows)] + Event::RedrawRequested(_) => { + let (width, height) = { + let size = window.inner_size(); + (size.width, size.height) + }; + surface + .resize( + NonZeroU32::new(width).unwrap(), + NonZeroU32::new(height).unwrap(), + ) + .unwrap(); + + let mut buffer = surface.buffer_mut().unwrap(); + buffer.fill(0); + buffer.present().unwrap(); + } + + _ => (), + } + }); +} diff --git a/vendor/tao/examples/video_modes.rs b/vendor/tao/examples/video_modes.rs new file mode 100644 index 0000000000..d40b3f6e3b --- /dev/null +++ b/vendor/tao/examples/video_modes.rs @@ -0,0 +1,24 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::event_loop::EventLoop; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + let monitor = match event_loop.primary_monitor() { + Some(monitor) => monitor, + None => { + println!("No primary monitor detected."); + return; + } + }; + + println!("Listing available video modes:"); + + for mode in monitor.video_modes() { + println!("{mode}"); + } +} diff --git a/vendor/tao/examples/window.rs b/vendor/tao/examples/window.rs new file mode 100644 index 0000000000..c4804fc4c0 --- /dev/null +++ b/vendor/tao/examples/window.rs @@ -0,0 +1,52 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +#[allow(clippy::single_match)] +fn main() { + let event_loop = EventLoop::new(); + + let mut window = Some( + WindowBuilder::new() + .with_title("A fantastic window!") + .with_inner_size(tao::dpi::LogicalSize::new(300.0, 300.0)) + .with_min_inner_size(tao::dpi::LogicalSize::new(200.0, 200.0)) + .build(&event_loop) + .unwrap(), + ); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + println!("{event:?}"); + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id: _, + .. + } => { + // drop the window to fire the `Destroyed` event + window = None; + } + Event::WindowEvent { + event: WindowEvent::Destroyed, + window_id: _, + .. + } => { + *control_flow = ControlFlow::Exit; + } + Event::MainEventsCleared => { + if let Some(w) = &window { + w.request_redraw(); + } + } + _ => (), + } + }); +} diff --git a/vendor/tao/examples/window_debug.rs b/vendor/tao/examples/window_debug.rs new file mode 100644 index 0000000000..1bb04c4782 --- /dev/null +++ b/vendor/tao/examples/window_debug.rs @@ -0,0 +1,176 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +// This example is used by developers to test various window functions. + +use tao::{ + dpi::{LogicalSize, PhysicalSize}, + event::{DeviceEvent, ElementState, Event, KeyEvent, RawKeyEvent, WindowEvent}, + event_loop::{ControlFlow, DeviceEventFilter, EventLoop}, + keyboard::{Key, KeyCode}, + window::{Fullscreen, WindowBuilder}, +}; + +#[allow(clippy::single_match)] +#[allow(clippy::collapsible_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + event_loop.set_device_event_filter(DeviceEventFilter::Never); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .with_inner_size(LogicalSize::new(100.0, 100.0)) + .build(&event_loop) + .unwrap(); + + eprintln!("debugging keys:"); + eprintln!(" (E) Enter exclusive fullscreen"); + eprintln!(" (F) Toggle borderless fullscreen"); + eprintln!(" (P) Toggle borderless fullscreen on system's preferred monitor"); + eprintln!(" (V) Toggle visibility"); + eprintln!(" (T) Toggle always on top"); + eprintln!(" (B) Toggle always on bottom"); + eprintln!(" (C) Toggle content protection"); + eprintln!(" (R) Toggle resizable"); + eprintln!(" (M) Toggle minimized"); + eprintln!(" (X) Toggle maximized"); + eprintln!(" (Q) Quit event loop"); + eprintln!(" (Shift + M) Toggle minimizable"); + eprintln!(" (Shift + X) Toggle maximizable"); + eprintln!(" (Shift + Q) Toggle closable"); + + let mut always_on_bottom = false; + let mut always_on_top = false; + let mut visible = true; + let mut content_protection = false; + let mut resizable = false; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + // This used to use the virtual key, but the new API + // only provides the `physical_key` (`Code`). + Event::DeviceEvent { + event: + DeviceEvent::Key(RawKeyEvent { + physical_key, + state: ElementState::Released, + .. + }), + .. + } => match physical_key { + KeyCode::KeyM => { + if window.is_minimized() { + window.set_minimized(false); + window.set_focus() + } + } + KeyCode::KeyV => { + if !visible { + visible = !visible; + window.set_visible(visible); + } + } + _ => (), + }, + Event::WindowEvent { + event: + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: Key::Character(key_str), + state: ElementState::Released, + .. + }, + .. + }, + .. + } => match key_str { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + "e" => { + fn area(size: PhysicalSize) -> u32 { + size.width * size.height + } + + let monitor = window.current_monitor().unwrap(); + if let Some(mode) = monitor + .video_modes() + .max_by(|a, b| area(a.size()).cmp(&area(b.size()))) + { + window.set_fullscreen(Some(Fullscreen::Exclusive(mode))); + } else { + eprintln!("no video modes available"); + } + } + "f" => { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + let monitor = window.current_monitor(); + window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); + } + } + "p" => { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + window.set_fullscreen(Some(Fullscreen::Borderless(None))); + } + } + "r" => { + resizable = !resizable; + window.set_resizable(resizable); + println!("Resizable: {resizable}"); + } + "m" => { + window.set_minimized(!window.is_minimized()); + } + "q" => { + *control_flow = ControlFlow::Exit; + } + "v" => { + visible = !visible; + window.set_visible(visible); + } + "x" => { + window.set_maximized(!window.is_maximized()); + } + "t" => { + always_on_top = !always_on_top; + window.set_always_on_top(always_on_top); + } + "b" => { + always_on_bottom = !always_on_bottom; + window.set_always_on_bottom(always_on_bottom); + } + "c" => { + content_protection = !content_protection; + window.set_content_protection(content_protection); + } + "M" => { + let minimizable = !window.is_minimizable(); + window.set_minimizable(minimizable); + } + "X" => { + let maximizable = !window.is_maximizable(); + window.set_maximizable(maximizable); + } + "Q" => { + let closable = !window.is_closable(); + window.set_closable(closable); + } + _ => (), + }, + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + .. + } if window_id == window.id() => *control_flow = ControlFlow::Exit, + _ => (), + } + }); +} diff --git a/vendor/tao/examples/window_icon.rs b/vendor/tao/examples/window_icon.rs new file mode 100644 index 0000000000..bc5c4c6100 --- /dev/null +++ b/vendor/tao/examples/window_icon.rs @@ -0,0 +1,64 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +extern crate image; +use std::path::Path; + +use tao::{ + event::Event, + event_loop::{ControlFlow, EventLoop}, + window::{Icon, WindowBuilder}, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + + // You'll have to choose an icon size at your own discretion. On Linux, the icon should be + // provided in whatever size it was naturally drawn; that is, don’t scale the image before passing + // it to Tao. But on Windows, you will have to account for screen scaling. Here we use 32px, + // since it seems to work well enough in most cases. Be careful about going too high, or + // you'll be bitten by the low-quality downscaling built into the WM. + let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png"); + + let icon = load_icon(Path::new(path)); + + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("An iconic window!") + // At present, this only does anything on Windows and Linux, so if you want to save load + // time, you can put icon loading behind a function that returns `None` on other platforms. + .with_window_icon(Some(icon)) + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + if let Event::WindowEvent { event, .. } = event { + use tao::event::WindowEvent::*; + match event { + CloseRequested => *control_flow = ControlFlow::Exit, + DroppedFile(path) => { + window.set_window_icon(Some(load_icon(&path))); + } + _ => (), + } + } + }); +} + +fn load_icon(path: &Path) -> Icon { + let (icon_rgba, icon_width, icon_height) = { + // alternatively, you can embed the icon in the binary through `include_bytes!` macro and use `image::load_from_memory` + let image = image::open(path) + .expect("Failed to open icon path") + .into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") +} diff --git a/vendor/tao/examples/window_run_return.rs b/vendor/tao/examples/window_run_return.rs new file mode 100644 index 0000000000..ebae8ac8b9 --- /dev/null +++ b/vendor/tao/examples/window_run_return.rs @@ -0,0 +1,59 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +// Limit this example to only compatible platforms. +#[cfg(not(target_os = "ios"))] +#[allow(clippy::single_match)] +fn main() { + use std::{thread::sleep, time::Duration}; + + use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + platform::run_return::EventLoopExtRunReturn, + window::WindowBuilder, + }; + let mut event_loop = EventLoop::new(); + + env_logger::init(); + let _window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + let mut quit = false; + + while !quit { + event_loop.run_return(|event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + if let Event::WindowEvent { event, .. } = &event { + // Print only Window events to reduce noise + println!("{:?}", event); + } + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + quit = true; + } + Event::MainEventsCleared => { + *control_flow = ControlFlow::Exit; + } + _ => (), + } + }); + + // Sleep for 1/60 second to simulate rendering + println!("rendering"); + sleep(Duration::from_millis(16)); + } +} + +#[cfg(target_os = "ios")] +fn main() { + println!("This platform doesn't support run_return."); +} diff --git a/vendor/tao/src/error.rs b/vendor/tao/src/error.rs new file mode 100644 index 0000000000..3bc21cedcc --- /dev/null +++ b/vendor/tao/src/error.rs @@ -0,0 +1,88 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +//! The `Error` struct and associated types. +use std::{error, fmt}; + +use crate::platform_impl; + +/// An error whose cause it outside Tao's control. +#[non_exhaustive] +#[derive(Debug)] +pub enum ExternalError { + /// The operation is not supported by the backend. + NotSupported(NotSupportedError), + /// The OS cannot perform the operation. + Os(OsError), +} + +/// The error type for when the requested operation is not supported by the backend. +#[derive(Clone)] +pub struct NotSupportedError { + _marker: (), +} + +/// The error type for when the OS cannot perform the requested operation. +#[derive(Debug)] +pub struct OsError { + line: u32, + file: &'static str, + error: platform_impl::OsError, +} + +impl NotSupportedError { + #[inline] + #[allow(dead_code)] + pub(crate) fn new() -> NotSupportedError { + NotSupportedError { _marker: () } + } +} + +impl OsError { + #[allow(dead_code)] + pub(crate) fn new(line: u32, file: &'static str, error: platform_impl::OsError) -> OsError { + OsError { line, file, error } + } +} + +#[allow(unused_macros)] +macro_rules! os_error { + ($error:expr) => {{ + crate::error::OsError::new(line!(), file!(), $error) + }}; +} + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.pad(&format!( + "os error at {}:{}: {}", + self.file, self.line, self.error + )) + } +} + +impl fmt::Display for ExternalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + ExternalError::NotSupported(e) => e.fmt(f), + ExternalError::Os(e) => e.fmt(f), + } + } +} + +impl fmt::Debug for NotSupportedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.debug_struct("NotSupportedError").finish() + } +} + +impl fmt::Display for NotSupportedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.pad("the requested operation is not supported by Tao") + } +} + +impl error::Error for OsError {} +impl error::Error for ExternalError {} +impl error::Error for NotSupportedError {} diff --git a/vendor/tao/src/event.rs b/vendor/tao/src/event.rs new file mode 100644 index 0000000000..54b77ce7d3 --- /dev/null +++ b/vendor/tao/src/event.rs @@ -0,0 +1,928 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +//! The `Event` enum and assorted supporting types. +//! +//! These are sent to the closure given to [`EventLoop::run(...)`][event_loop_run], where they get +//! processed and used to modify the program state. For more details, see the root-level documentation. +//! +//! Some of these events represent different "parts" of a traditional event-handling loop. You could +//! approximate the basic ordering loop of [`EventLoop::run(...)`][event_loop_run] like this: +//! +//! ```rust,ignore +//! let mut control_flow = ControlFlow::Poll; +//! let mut start_cause = StartCause::Init; +//! +//! while control_flow != ControlFlow::Exit { +//! event_handler(NewEvents(start_cause), ..., &mut control_flow); +//! +//! for e in (window events, user events, device events) { +//! event_handler(e, ..., &mut control_flow); +//! } +//! event_handler(MainEventsCleared, ..., &mut control_flow); +//! +//! for w in (redraw windows) { +//! event_handler(RedrawRequested(w), ..., &mut control_flow); +//! } +//! event_handler(RedrawEventsCleared, ..., &mut control_flow); +//! +//! start_cause = wait_if_necessary(control_flow); +//! } +//! +//! event_handler(LoopDestroyed, ..., &mut control_flow); +//! ``` +//! +//! This leaves out timing details like `ControlFlow::WaitUntil` but hopefully +//! describes what happens in what order. +//! +//! [event_loop_run]: crate::event_loop::EventLoop::run +use std::{path::PathBuf, time::Instant}; + +use crate::{ + dpi::{PhysicalPosition, PhysicalSize}, + keyboard::{self, ModifiersState}, + platform_impl, + window::{Theme, WindowId}, +}; + +/// Describes a generic event. +/// +/// See the module-level docs for more information on the event loop manages each event. +#[non_exhaustive] +#[derive(Debug, PartialEq)] +pub enum Event<'a, T: 'static> { + /// Emitted when new events arrive from the OS to be processed. + /// + /// This event type is useful as a place to put code that should be done before you start + /// processing events, such as updating frame timing information for benchmarking or checking + /// the [`StartCause`][crate::event::StartCause] to see if a timer set by + /// [`ControlFlow::WaitUntil`](crate::event_loop::ControlFlow::WaitUntil) has elapsed. + NewEvents(StartCause), + + /// Emitted when the OS sends an event to a tao window. + #[non_exhaustive] + WindowEvent { + window_id: WindowId, + event: WindowEvent<'a>, + }, + + /// Emitted when the OS sends an event to a device. + #[non_exhaustive] + DeviceEvent { + device_id: DeviceId, + event: DeviceEvent, + }, + + /// Emitted when an event is sent from [`EventLoopProxy::send_event`](crate::event_loop::EventLoopProxy::send_event) + UserEvent(T), + + /// Emitted when the application has been suspended. + Suspended, + + /// Emitted when the application has been resumed. + Resumed, + + /// Emitted when all of the event loop's input events have been processed and redraw processing + /// is about to begin. + /// + /// This event is useful as a place to put your code that should be run after all + /// state-changing events have been handled and you want to do stuff (updating state, performing + /// calculations, etc) that happens as the "main body" of your event loop. If your program only draws + /// graphics when something changes, it's usually better to do it in response to + /// [`Event::RedrawRequested`](crate::event::Event::RedrawRequested), which gets emitted + /// immediately after this event. Programs that draw graphics continuously, like most games, + /// can render here unconditionally for simplicity. + MainEventsCleared, + + /// Emitted after `MainEventsCleared` when a window should be redrawn. + /// + /// This gets triggered in two scenarios: + /// - The OS has performed an operation that's invalidated the window's contents (such as + /// resizing the window). + /// - The application has explicitly requested a redraw via + /// [`Window::request_redraw`](crate::window::Window::request_redraw). + /// + /// During each iteration of the event loop, Tao will aggregate duplicate redraw requests + /// into a single event, to help avoid duplicating rendering work. + /// + /// Mainly of interest to applications with mostly-static graphics that avoid redrawing unless + /// something changes, like most non-game GUIs. + /// + /// ## Platform-specific + /// + /// - **Linux: This is triggered by `draw` signal of the gtk window. It can be used to detect if + /// the window is requested to redraw. But widgets it contains are usually not tied to its signal. + /// So if you really want to draw each component, please consider using `connect_draw` method + /// from [`WidgetExt`] directly.** + /// + /// [`WidgetExt`]: https://gtk-rs.org/gtk3-rs/stable/latest/docs/gtk/prelude/trait.WidgetExt.html + RedrawRequested(WindowId), + + /// Emitted after all `RedrawRequested` events have been processed and control flow is about to + /// be taken away from the program. If there are no `RedrawRequested` events, it is emitted + /// immediately after `MainEventsCleared`. + /// + /// This event is useful for doing any cleanup or bookkeeping work after all the rendering + /// tasks have been completed. + RedrawEventsCleared, + + /// Emitted when the event loop is being shut down. + /// + /// This is irreversable - if this event is emitted, it is guaranteed to be the last event that + /// gets emitted. You generally want to treat this as an "do on quit" event. + LoopDestroyed, + + /// Emitted when the app is open by external resources, like opening a file or deeplink. + Opened { urls: Vec }, + + /// ## Platform-specific + /// + /// - **macOS**: https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428638-applicationshouldhandlereopen with return value same as hasVisibleWindows + /// - **Other**: Unsupported. + #[non_exhaustive] + Reopen { has_visible_windows: bool }, +} + +impl Clone for Event<'static, T> { + fn clone(&self) -> Self { + use self::Event::*; + match self { + WindowEvent { window_id, event } => WindowEvent { + window_id: *window_id, + event: event.clone(), + }, + UserEvent(event) => UserEvent(event.clone()), + DeviceEvent { device_id, event } => DeviceEvent { + device_id: *device_id, + event: event.clone(), + }, + NewEvents(cause) => NewEvents(*cause), + MainEventsCleared => MainEventsCleared, + RedrawRequested(wid) => RedrawRequested(*wid), + RedrawEventsCleared => RedrawEventsCleared, + LoopDestroyed => LoopDestroyed, + Suspended => Suspended, + Resumed => Resumed, + Opened { urls } => Opened { urls: urls.clone() }, + Reopen { + has_visible_windows, + } => Reopen { + has_visible_windows: *has_visible_windows, + }, + } + } +} + +impl<'a, T> Event<'a, T> { + pub fn map_nonuser_event(self) -> Result, Event<'a, T>> { + use self::Event::*; + match self { + UserEvent(_) => Err(self), + WindowEvent { window_id, event } => Ok(WindowEvent { window_id, event }), + DeviceEvent { device_id, event } => Ok(DeviceEvent { device_id, event }), + NewEvents(cause) => Ok(NewEvents(cause)), + MainEventsCleared => Ok(MainEventsCleared), + RedrawRequested(wid) => Ok(RedrawRequested(wid)), + RedrawEventsCleared => Ok(RedrawEventsCleared), + LoopDestroyed => Ok(LoopDestroyed), + Suspended => Ok(Suspended), + Resumed => Ok(Resumed), + Opened { urls } => Ok(Opened { urls }), + Reopen { + has_visible_windows, + } => Ok(Reopen { + has_visible_windows, + }), + } + } + + /// If the event doesn't contain a reference, turn it into an event with a `'static` lifetime. + /// Otherwise, return `None`. + pub fn to_static(self) -> Option> { + use self::Event::*; + match self { + WindowEvent { window_id, event } => event + .to_static() + .map(|event| WindowEvent { window_id, event }), + UserEvent(event) => Some(UserEvent(event)), + DeviceEvent { device_id, event } => Some(DeviceEvent { device_id, event }), + NewEvents(cause) => Some(NewEvents(cause)), + MainEventsCleared => Some(MainEventsCleared), + RedrawRequested(wid) => Some(RedrawRequested(wid)), + RedrawEventsCleared => Some(RedrawEventsCleared), + LoopDestroyed => Some(LoopDestroyed), + Suspended => Some(Suspended), + Resumed => Some(Resumed), + Opened { urls } => Some(Opened { urls }), + Reopen { + has_visible_windows, + } => Some(Reopen { + has_visible_windows, + }), + } + } +} + +/// Describes the reason the event loop is resuming. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum StartCause { + /// Sent if the time specified by `ControlFlow::WaitUntil` has been reached. Contains the + /// moment the timeout was requested and the requested resume time. The actual resume time is + /// guaranteed to be equal to or after the requested resume time. + #[non_exhaustive] + ResumeTimeReached { + start: Instant, + requested_resume: Instant, + }, + + /// Sent if the OS has new events to send to the window, after a wait was requested. Contains + /// the moment the wait was requested and the resume time, if requested. + #[non_exhaustive] + WaitCancelled { + start: Instant, + requested_resume: Option, + }, + + /// Sent if the event loop is being resumed after the loop's control flow was set to + /// `ControlFlow::Poll`. + Poll, + + /// Sent once, immediately after `run` is called. Indicates that the loop was just initialized. + Init, +} + +/// Describes an event from a `Window`. +#[non_exhaustive] +#[derive(Debug, PartialEq)] +pub enum WindowEvent<'a> { + /// The size of the window has changed. Contains the client area's new dimensions. + Resized(PhysicalSize), + + /// The position of the window has changed. Contains the window's new position. + /// + /// ## Platform-specific + /// + /// - **Linux(Wayland)**: will always be (0, 0) since Wayland doesn't support a global cordinate system. + Moved(PhysicalPosition), + + /// The window has been requested to close. + CloseRequested, + + /// The window has been destroyed. + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Only fired if the [`crate::window::Window`] is dropped. + /// - **macOS:** Fired if the [`crate::window::Window`] is dropped or the dock `Quit` item is clicked. + Destroyed, + + /// A file has been dropped into the window. + /// + /// When the user drops multiple files at once, this event will be emitted for each file + /// separately. + DroppedFile(PathBuf), + + /// A file is being hovered over the window. + /// + /// When the user hovers multiple files at once, this event will be emitted for each file + /// separately. + HoveredFile(PathBuf), + + /// A file was hovered, but has exited the window. + /// + /// There will be a single `HoveredFileCancelled` event triggered even if multiple files were + /// hovered. + HoveredFileCancelled, + + /// The window received a unicode character. + ReceivedImeText(String), + + /// The window gained or lost focus. + /// + /// The parameter is true if the window has gained focus, and false if it has lost focus. + Focused(bool), + + /// An event from the keyboard has been received. + /// + /// ## Platform-specific + /// - **Windows:** The shift key overrides NumLock. In other words, while shift is held down, + /// numpad keys act as if NumLock wasn't active. When this is used, the OS sends fake key + /// events which are not marked as `is_synthetic`. + #[non_exhaustive] + KeyboardInput { + device_id: DeviceId, + event: KeyEvent, + + /// If `true`, the event was generated synthetically by tao + /// in one of the following circumstances: + /// + /// * Synthetic key press events are generated for all keys pressed + /// when a window gains focus. Likewise, synthetic key release events + /// are generated for all keys pressed when a window goes out of focus. + /// ***Currently, this is only functional on Linux and Windows*** + /// + /// Otherwise, this value is always `false`. + is_synthetic: bool, + }, + + /// The keyboard modifiers have changed. + ModifiersChanged(ModifiersState), + + /// The cursor has moved on the window. + CursorMoved { + device_id: DeviceId, + + /// (x,y) coords in pixels relative to the top-left corner of the window. Because the range of this data is + /// limited by the display area and it may have been transformed by the OS to implement effects such as cursor + /// acceleration, it should not be used to implement non-cursor-like interactions such as 3D camera control. + position: PhysicalPosition, + #[deprecated = "Deprecated in favor of WindowEvent::ModifiersChanged"] + modifiers: ModifiersState, + }, + + /// The cursor has entered the window. + CursorEntered { device_id: DeviceId }, + + /// The cursor has left the window. + CursorLeft { device_id: DeviceId }, + + /// A mouse wheel movement or touchpad scroll occurred. + MouseWheel { + device_id: DeviceId, + delta: MouseScrollDelta, + phase: TouchPhase, + #[deprecated = "Deprecated in favor of WindowEvent::ModifiersChanged"] + modifiers: ModifiersState, + }, + + /// An mouse button press has been received. + MouseInput { + device_id: DeviceId, + state: ElementState, + button: MouseButton, + #[deprecated = "Deprecated in favor of WindowEvent::ModifiersChanged"] + modifiers: ModifiersState, + }, + + /// Touchpad pressure event. + /// + /// At the moment, only supported on Apple forcetouch-capable macbooks. + /// The parameters are: pressure level (value between 0 and 1 representing how hard the touchpad + /// is being pressed) and stage (integer representing the click level). + TouchpadPressure { + device_id: DeviceId, + pressure: f32, + stage: i64, + }, + + /// Motion on some analog axis. May report data redundant to other, more specific events. + AxisMotion { + device_id: DeviceId, + axis: AxisId, + value: f64, + }, + + /// Touch event has been received + Touch(Touch), + + /// The window's scale factor has changed. + /// + /// The following user actions can cause DPI changes: + /// + /// * Changing the display's resolution. + /// * Changing the display's scale factor (e.g. in Control Panel on Windows). + /// * Moving the window to a display with a different scale factor. + /// + /// After this event callback has been processed, the window will be resized to whatever value + /// is pointed to by the `new_inner_size` reference. By default, this will contain the size suggested + /// by the OS, but it can be changed to any value. + /// + /// For more information about DPI in general, see the [`dpi`](crate::dpi) module. + ScaleFactorChanged { + scale_factor: f64, + new_inner_size: &'a mut PhysicalSize, + }, + + /// The system window theme has changed. + /// + /// Applications might wish to react to this to change the theme of the content of the window + /// when the system changes the window theme. + /// + /// ## Platform-specific + /// + /// - **Linux / Android / iOS:** Unsupported + ThemeChanged(Theme), + + /// The window decorations has been clicked. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS / Android / iOS:** Unsupported + DecorationsClick, +} + +impl Clone for WindowEvent<'static> { + fn clone(&self) -> Self { + use self::WindowEvent::*; + match self { + Resized(size) => Resized(*size), + Moved(pos) => Moved(*pos), + CloseRequested => CloseRequested, + Destroyed => Destroyed, + DroppedFile(file) => DroppedFile(file.clone()), + HoveredFile(file) => HoveredFile(file.clone()), + HoveredFileCancelled => HoveredFileCancelled, + ReceivedImeText(c) => ReceivedImeText(c.clone()), + Focused(f) => Focused(*f), + KeyboardInput { + device_id, + event, + is_synthetic, + } => KeyboardInput { + device_id: *device_id, + event: event.clone(), + is_synthetic: *is_synthetic, + }, + + ModifiersChanged(modifiers) => ModifiersChanged(*modifiers), + #[allow(deprecated)] + CursorMoved { + device_id, + position, + modifiers, + } => CursorMoved { + device_id: *device_id, + position: *position, + modifiers: *modifiers, + }, + CursorEntered { device_id } => CursorEntered { + device_id: *device_id, + }, + CursorLeft { device_id } => CursorLeft { + device_id: *device_id, + }, + #[allow(deprecated)] + MouseWheel { + device_id, + delta, + phase, + modifiers, + } => MouseWheel { + device_id: *device_id, + delta: *delta, + phase: *phase, + modifiers: *modifiers, + }, + #[allow(deprecated)] + MouseInput { + device_id, + state, + button, + modifiers, + } => MouseInput { + device_id: *device_id, + state: *state, + button: *button, + modifiers: *modifiers, + }, + TouchpadPressure { + device_id, + pressure, + stage, + } => TouchpadPressure { + device_id: *device_id, + pressure: *pressure, + stage: *stage, + }, + AxisMotion { + device_id, + axis, + value, + } => AxisMotion { + device_id: *device_id, + axis: *axis, + value: *value, + }, + Touch(touch) => Touch(*touch), + ThemeChanged(theme) => ThemeChanged(*theme), + ScaleFactorChanged { .. } => { + unreachable!("Static event can't be about scale factor changing") + } + DecorationsClick => DecorationsClick, + } + } +} + +impl<'a> WindowEvent<'a> { + pub fn to_static(self) -> Option> { + use self::WindowEvent::*; + match self { + Resized(size) => Some(Resized(size)), + Moved(position) => Some(Moved(position)), + CloseRequested => Some(CloseRequested), + Destroyed => Some(Destroyed), + DroppedFile(file) => Some(DroppedFile(file)), + HoveredFile(file) => Some(HoveredFile(file)), + HoveredFileCancelled => Some(HoveredFileCancelled), + ReceivedImeText(c) => Some(ReceivedImeText(c)), + Focused(focused) => Some(Focused(focused)), + KeyboardInput { + device_id, + event, + is_synthetic, + } => Some(KeyboardInput { + device_id, + event, + is_synthetic, + }), + ModifiersChanged(modifiers) => Some(ModifiersChanged(modifiers)), + #[allow(deprecated)] + CursorMoved { + device_id, + position, + modifiers, + } => Some(CursorMoved { + device_id, + position, + modifiers, + }), + CursorEntered { device_id } => Some(CursorEntered { device_id }), + CursorLeft { device_id } => Some(CursorLeft { device_id }), + #[allow(deprecated)] + MouseWheel { + device_id, + delta, + phase, + modifiers, + } => Some(MouseWheel { + device_id, + delta, + phase, + modifiers, + }), + #[allow(deprecated)] + MouseInput { + device_id, + state, + button, + modifiers, + } => Some(MouseInput { + device_id, + state, + button, + modifiers, + }), + TouchpadPressure { + device_id, + pressure, + stage, + } => Some(TouchpadPressure { + device_id, + pressure, + stage, + }), + AxisMotion { + device_id, + axis, + value, + } => Some(AxisMotion { + device_id, + axis, + value, + }), + Touch(touch) => Some(Touch(touch)), + ThemeChanged(theme) => Some(ThemeChanged(theme)), + ScaleFactorChanged { .. } => None, + DecorationsClick => Some(DecorationsClick), + } + } +} + +/// Identifier of an input device. +/// +/// Whenever you receive an event arising from a particular input device, this event contains a `DeviceId` which +/// identifies its origin. Note that devices may be virtual (representing an on-screen cursor and keyboard focus) or +/// physical. Virtual devices typically aggregate inputs from multiple physical devices. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(pub(crate) platform_impl::DeviceId); + +impl DeviceId { + /// # Safety + /// Returns a dummy `DeviceId`, useful for unit testing. The only guarantee made about the return + /// value of this function is that it will always be equal to itself and to future values returned + /// by this function. No other guarantees are made. This may be equal to a real `DeviceId`. + /// + /// **Passing this into a tao function will result in undefined behavior.** + pub unsafe fn dummy() -> Self { + DeviceId(platform_impl::DeviceId::dummy()) + } +} + +/// Represents raw hardware events that are not associated with any particular window. +/// +/// Useful for interactions that diverge significantly from a conventional 2D GUI, such as 3D camera or first-person +/// game controls. Many physical actions, such as mouse movement, can produce both device and window events. Because +/// window events typically arise from virtual devices (corresponding to GUI cursors and keyboard focus) the device IDs +/// may not match. +/// +/// Note that these events are delivered regardless of input focus. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq)] +pub enum DeviceEvent { + Added, + Removed, + + /// Change in physical position of a pointing device. + /// + /// This represents raw, unfiltered physical motion. Not to be confused with `WindowEvent::CursorMoved`. + #[non_exhaustive] + MouseMotion { + /// (x, y) change in position in unspecified units. + /// + /// Different devices may use different units. + delta: (f64, f64), + }, + + /// Physical scroll event + #[non_exhaustive] + MouseWheel { + delta: MouseScrollDelta, + }, + + /// Motion on some analog axis. This event will be reported for all arbitrary input devices + /// that tao supports on this platform, including mouse devices. If the device is a mouse + /// device then this will be reported alongside the MouseMotion event. + #[non_exhaustive] + Motion { + axis: AxisId, + value: f64, + }, + + #[non_exhaustive] + Button { + button: ButtonId, + state: ElementState, + }, + + Key(RawKeyEvent), + + #[non_exhaustive] + Text { + codepoint: char, + }, +} + +/// Describes a keyboard input as a raw device event. +/// +/// Note that holding down a key may produce repeated `RawKeyEvent`s. The +/// operating system doesn't provide information whether such an event is a +/// repeat or the initial keypress. An application may emulate this by, for +/// example keeping a Map/Set of pressed keys and determining whether a keypress +/// corresponds to an already pressed key. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RawKeyEvent { + pub physical_key: keyboard::KeyCode, + pub state: ElementState, +} + +/// Describes a keyboard input targeting a window. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEvent { + /// Represents the position of a key independent of the currently active layout. + /// + /// It also uniquely identifies the physical key (i.e. it's mostly synonymous with a scancode). + /// The most prevalent use case for this is games. For example the default keys for the player + /// to move around might be the W, A, S, and D keys on a US layout. The position of these keys + /// is more important than their label, so they should map to Z, Q, S, and D on an "AZERTY" + /// layout. (This value is `KeyCode::KeyW` for the Z key on an AZERTY layout.) + /// + /// Note that `Fn` and `FnLock` key events are not guaranteed to be emitted by `tao`. These + /// keys are usually handled at the hardware or OS level. + pub physical_key: keyboard::KeyCode, + + /// This value is affected by all modifiers except Ctrl. + /// + /// This has two use cases: + /// - Allows querying whether the current input is a Dead key. + /// - Allows handling key-bindings on platforms which don't + /// support `key_without_modifiers`. + /// + /// ## Platform-specific + /// - **Web:** Dead keys might be reported as the real key instead + /// of `Dead` depending on the browser/OS. + pub logical_key: keyboard::Key<'static>, + + /// Contains the text produced by this keypress. + /// + /// In most cases this is identical to the content + /// of the `Character` variant of `logical_key`. + /// However, on Windows when a dead key was pressed earlier + /// but cannot be combined with the character from this + /// keypress, the produced text will consist of two characters: + /// the dead-key-character followed by the character resulting + /// from this keypress. + /// + /// An additional difference from `logical_key` is that + /// this field stores the text representation of any key + /// that has such a representation. For example when + /// `logical_key` is `Key::Enter`, this field is `Some("\r")`. + /// + /// This is `None` if the current keypress cannot + /// be interpreted as text. + /// + /// See also: `text_with_all_modifiers()` + pub text: Option<&'static str>, + + pub location: keyboard::KeyLocation, + pub state: ElementState, + pub repeat: bool, + + pub(crate) platform_specific: platform_impl::KeyEventExtra, +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl KeyEvent { + /// Identical to `KeyEvent::text` but this is affected by Ctrl. + /// + /// For example, pressing Ctrl+a produces `Some("\x01")`. + pub fn text_with_all_modifiers(&self) -> Option<&str> { + self.platform_specific.text_with_all_modifiers + } + + /// This value ignores all modifiers including, + /// but not limited to Shift, Caps Lock, + /// and Ctrl. In most cases this means that the + /// unicode character in the resulting string is lowercase. + /// + /// This is useful for key-bindings / shortcut key combinations. + /// + /// In case `logical_key` reports `Dead`, this will still report the + /// key as `Character` according to the current keyboard layout. This value + /// cannot be `Dead`. + pub fn key_without_modifiers(&self) -> keyboard::Key<'static> { + self.platform_specific.key_without_modifiers.clone() + } +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +impl KeyEvent { + /// Identical to `KeyEvent::text`. + pub fn text_with_all_modifiers(&self) -> Option<&str> { + self.text + } + + /// Identical to `KeyEvent::logical_key`. + pub fn key_without_modifiers(&self) -> keyboard::Key<'static> { + self.logical_key.clone() + } +} + +/// Describes touch-screen input state. +#[non_exhaustive] +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum TouchPhase { + Started, + Moved, + Ended, + Cancelled, +} + +/// Represents a touch event +/// +/// Every time the user touches the screen, a new `Start` event with an unique +/// identifier for the finger is generated. When the finger is lifted, an `End` +/// event is generated with the same finger id. +/// +/// After a `Start` event has been emitted, there may be zero or more `Move` +/// events when the finger is moved or the touch pressure changes. +/// +/// The finger id may be reused by the system after an `End` event. The user +/// should assume that a new `Start` event received with the same id has nothing +/// to do with the old finger and is a new finger. +/// +/// A `Cancelled` event is emitted when the system has canceled tracking this +/// touch, such as when the window loses focus, or on iOS if the user moves the +/// device against their face. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Touch { + pub device_id: DeviceId, + pub phase: TouchPhase, + pub location: PhysicalPosition, + /// Describes how hard the screen was pressed. May be `None` if the platform + /// does not support pressure sensitivity. + /// + /// ## Platform-specific + /// + /// - Only available on **iOS** 9.0+ and **Windows** 8+. + pub force: Option, + /// Unique identifier of a finger. + pub id: u64, +} + +/// Describes the force of a touch event +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Force { + /// On iOS, the force is calibrated so that the same number corresponds to + /// roughly the same amount of pressure on the screen regardless of the + /// device. + #[non_exhaustive] + Calibrated { + /// The force of the touch, where a value of 1.0 represents the force of + /// an average touch (predetermined by the system, not user-specific). + /// + /// The force reported by Apple Pencil is measured along the axis of the + /// pencil. If you want a force perpendicular to the device, you need to + /// calculate this value using the `altitude_angle` value. + force: f64, + /// The maximum possible force for a touch. + /// + /// The value of this field is sufficiently high to provide a wide + /// dynamic range for values of the `force` field. + max_possible_force: f64, + /// The altitude (in radians) of the stylus. + /// + /// A value of 0 radians indicates that the stylus is parallel to the + /// surface. The value of this property is Pi/2 when the stylus is + /// perpendicular to the surface. + altitude_angle: Option, + }, + /// If the platform reports the force as normalized, we have no way of + /// knowing how much pressure 1.0 corresponds to - we know it's the maximum + /// amount of force, but as to how much force, you might either have to + /// press really really hard, or not hard at all, depending on the device. + Normalized(f64), +} + +impl Force { + /// Returns the force normalized to the range between 0.0 and 1.0 inclusive. + /// Instead of normalizing the force, you should prefer to handle + /// `Force::Calibrated` so that the amount of force the user has to apply is + /// consistent across devices. + pub fn normalized(&self) -> f64 { + match self { + Force::Calibrated { + force, + max_possible_force, + altitude_angle, + } => { + let force = match altitude_angle { + Some(altitude_angle) => force / altitude_angle.sin(), + None => *force, + }; + force / max_possible_force + } + Force::Normalized(force) => *force, + } + } +} + +/// Identifier for a specific analog axis on some device. +pub type AxisId = u32; + +/// Identifier for a specific button on some device. +pub type ButtonId = u32; + +/// Describes the input state of a key. +#[non_exhaustive] +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ElementState { + Pressed, + Released, +} + +/// Describes a button of a mouse controller. +#[non_exhaustive] +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum MouseButton { + Left, + Right, + Middle, + Other(u16), +} + +/// Describes a difference in the mouse scroll wheel state. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum MouseScrollDelta { + /// Amount in lines or rows to scroll in the horizontal + /// and vertical directions. + /// + /// Positive values indicate movement forward + /// (away from the user) or rightwards. + LineDelta(f32, f32), + /// Amount in pixels to scroll in the horizontal and + /// vertical direction. + /// + /// Scroll events are expressed as a PixelDelta if + /// supported by the device (eg. a touchpad) and + /// platform. + PixelDelta(PhysicalPosition), +} diff --git a/vendor/tao/src/event_loop.rs b/vendor/tao/src/event_loop.rs new file mode 100644 index 0000000000..8b01f3232d --- /dev/null +++ b/vendor/tao/src/event_loop.rs @@ -0,0 +1,411 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +//! The `EventLoop` struct and assorted supporting types, including `ControlFlow`. +//! +//! If you want to send custom events to the event loop, use [`EventLoop::create_proxy()`][create_proxy] +//! to acquire an [`EventLoopProxy`][event_loop_proxy] and call its [`send_event`][send_event] method. +//! +//! See the root-level documentation for information on how to create and use an event loop to +//! handle events. +//! +//! [create_proxy]: crate::event_loop::EventLoop::create_proxy +//! [event_loop_proxy]: crate::event_loop::EventLoopProxy +//! [send_event]: crate::event_loop::EventLoopProxy::send_event +use std::{error, fmt, marker::PhantomData, ops::Deref, time::Instant}; + +use crate::{ + dpi::PhysicalPosition, + error::ExternalError, + event::Event, + monitor::MonitorHandle, + platform_impl, + window::{ProgressBarState, Theme}, +}; + +/// Provides a way to retrieve events from the system and from the windows that were registered to +/// the events loop. +/// +/// An `EventLoop` can be seen more or less as a "context". Calling `EventLoop::new()` +/// initializes everything that will be required to create windows. +/// +/// To wake up an `EventLoop` from a another thread, see the `EventLoopProxy` docs. +/// +/// Note that the `EventLoop` cannot be shared across threads (due to platform-dependant logic +/// forbidding it), as such it is neither `Send` nor `Sync`. If you need cross-thread access, the +/// `Window` created from this `EventLoop` _can_ be sent to an other thread, and the +/// `EventLoopProxy` allows you to wake up an `EventLoop` from another thread. +/// +pub struct EventLoop { + pub(crate) event_loop: platform_impl::EventLoop, + pub(crate) _marker: ::std::marker::PhantomData<*mut ()>, // Not Send nor Sync +} + +/// Target that associates windows with an `EventLoop`. +/// +/// This type exists to allow you to create new windows while Tao executes +/// your callback. `EventLoop` will coerce into this type (`impl Deref for +/// EventLoop`), so functions that take this as a parameter can also take +/// `&EventLoop`. +#[derive(Clone)] +pub struct EventLoopWindowTarget { + pub(crate) p: platform_impl::EventLoopWindowTarget, + pub(crate) _marker: ::std::marker::PhantomData<*mut ()>, // Not Send nor Sync +} + +impl fmt::Debug for EventLoop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("EventLoop { .. }") + } +} + +impl fmt::Debug for EventLoopWindowTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("EventLoopWindowTarget { .. }") + } +} + +/// Object that allows building the event loop. +/// +/// This is used to make specifying options that affect the whole application +/// easier. But note that constructing multiple event loops is not supported. +#[derive(Default)] +pub struct EventLoopBuilder { + pub(crate) platform_specific: platform_impl::PlatformSpecificEventLoopAttributes, + _p: PhantomData, +} +impl EventLoopBuilder<()> { + /// Start building a new event loop. + #[inline] + pub fn new() -> Self { + Self::with_user_event() + } +} +impl EventLoopBuilder { + /// Start building a new event loop, with the given type as the user event + /// type. + #[inline] + pub fn with_user_event() -> Self { + Self { + platform_specific: Default::default(), + _p: PhantomData, + } + } + /// Builds a new event loop. + /// + /// ***For cross-platform compatibility, the `EventLoop` must be created on the main thread.*** + /// Attempting to create the event loop on a different thread will panic. This restriction isn't + /// strictly necessary on all platforms, but is imposed to eliminate any nasty surprises when + /// porting to platforms that require it. `EventLoopBuilderExt::any_thread` functions are exposed + /// in the relevant `platform` module if the target platform supports creating an event loop on + /// any thread. + /// + /// Usage will result in display backend initialisation, this can be controlled on linux + /// using an environment variable `WINIT_UNIX_BACKEND`. Legal values are `x11` and `wayland`. + /// If it is not set, winit will try to connect to a wayland connection, and if it fails will + /// fallback on x11. If this variable is set with any other value, winit will panic. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. + #[inline] + pub fn build(&mut self) -> EventLoop { + EventLoop { + event_loop: platform_impl::EventLoop::new(&mut self.platform_specific), + _marker: PhantomData, + } + } +} + +/// Set by the user callback given to the `EventLoop::run` method. +/// +/// Indicates the desired behavior of the event loop after [`Event::RedrawEventsCleared`][events_cleared] +/// is emitted. Defaults to `Poll`. +/// +/// ## Persistency +/// Almost every change is persistent between multiple calls to the event loop closure within a +/// given run loop. The only exception to this is `ExitWithCode` which, once set, cannot be unset. +/// Changes are **not** persistent between multiple calls to `run_return` - issuing a new call will +/// reset the control flow to `Poll`. +/// +/// [events_cleared]: crate::event::Event::RedrawEventsCleared +#[non_exhaustive] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ControlFlow { + /// When the current loop iteration finishes, immediately begin a new iteration regardless of + /// whether or not new events are available to process. + Poll, + /// When the current loop iteration finishes, suspend the thread until another event arrives. + Wait, + /// When the current loop iteration finishes, suspend the thread until either another event + /// arrives or the given time is reached. + WaitUntil(Instant), + /// Send a `LoopDestroyed` event and stop the event loop. This variant is *sticky* - once set, + /// `control_flow` cannot be changed from `ExitWithCode`, and any future attempts to do so will + /// result in the `control_flow` parameter being reset to `ExitWithCode`. + /// + /// The contained number will be used as exit code. The [`Exit`] constant is a shortcut for this + /// with exit code 0. + /// + /// ## Platform-specific + /// + /// - **Android / iOS / WASM**: The supplied exit code is unused. + /// - **Unix**: On most Unix-like platforms, only the 8 least significant bits will be used, + /// which can cause surprises with negative exit values (`-42` would end up as `214`). See + /// [`std::process::exit`]. + /// + /// [`Exit`]: ControlFlow::Exit + ExitWithCode(i32), +} + +impl ControlFlow { + /// Alias for [`ExitWithCode`]`(0)`. + /// + /// [`ExitWithCode`]: ControlFlow::ExitWithCode + #[allow(non_upper_case_globals)] + pub const Exit: Self = Self::ExitWithCode(0); +} + +impl Default for ControlFlow { + #[inline(always)] + fn default() -> ControlFlow { + ControlFlow::Poll + } +} + +impl EventLoop<()> { + /// Alias for [`EventLoopBuilder::new().build()`]. + /// + /// [`EventLoopBuilder::new().build()`]: EventLoopBuilder::build + #[inline] + pub fn new() -> EventLoop<()> { + EventLoopBuilder::new().build() + } +} + +impl Default for EventLoop<()> { + fn default() -> Self { + Self::new() + } +} + +impl EventLoop { + /// Hijacks the calling thread and initializes the tao event loop with the provided + /// closure. Since the closure is `'static`, it must be a `move` closure if it needs to + /// access any data from the calling context. + /// + /// See the [`ControlFlow`] docs for information on how changes to `&mut ControlFlow` impact the + /// event loop's behavior. + /// + /// Any values not passed to this function will *not* be dropped. + /// + /// ## Platform-specific + /// + /// - **Unix**: The program terminates with exit code 1 if the display server + /// disconnects. + /// + /// [`ControlFlow`]: crate::event_loop::ControlFlow + #[inline] + pub fn run(self, event_handler: F) -> ! + where + F: 'static + FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), + { + self.event_loop.run(event_handler) + } + + /// Creates an `EventLoopProxy` that can be used to dispatch user events to the main event loop. + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + event_loop_proxy: self.event_loop.create_proxy(), + } + } +} + +impl Deref for EventLoop { + type Target = EventLoopWindowTarget; + fn deref(&self) -> &EventLoopWindowTarget { + self.event_loop.window_target() + } +} + +impl EventLoopWindowTarget { + /// Returns the list of all the monitors available on the system. + #[inline] + pub fn available_monitors(&self) -> impl Iterator { + self + .p + .available_monitors() + .into_iter() + .map(|inner| MonitorHandle { inner }) + } + + /// Returns the primary monitor of the system. + /// + /// Returns `None` if it can't identify any monitor as a primary one. + #[inline] + pub fn primary_monitor(&self) -> Option { + self.p.primary_monitor() + } + + /// Returns the monitor that contains the given point. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS:** Unsupported. + #[inline] + pub fn monitor_from_point(&self, x: f64, y: f64) -> Option { + self + .p + .monitor_from_point(x, y) + .map(|inner| MonitorHandle { inner }) + } + + /// Change [`DeviceEvent`] filter mode. + /// + /// Since the [`DeviceEvent`] capture can lead to high CPU usage for unfocused windows, tao + /// will ignore them by default for unfocused windows. This method allows changing + /// this filter at runtime to explicitly capture them again. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS / iOS / Android:** Unsupported. + /// + /// [`DeviceEvent`]: crate::event::DeviceEvent + pub fn set_device_event_filter(&self, _filter: DeviceEventFilter) { + #[cfg(target_os = "windows")] + self.p.set_device_event_filter(_filter); + } + + /// Returns the current cursor position + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Linux(Wayland)**: Unsupported, returns `0,0`. + #[inline] + pub fn cursor_position(&self) -> Result, ExternalError> { + self.p.cursor_position() + } + + /// Sets the progress bar state + /// + /// ## Platform-specific + /// + /// - **Windows:** Unsupported. Use the Progress Bar Function Available in Window (Windows can have different progress bars for different window) + /// - **Linux:** Only supported desktop environments with `libunity` (e.g. GNOME). + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_progress_bar(&self, _progress: ProgressBarState) { + #[cfg(any(target_os = "linux", target_os = "macos"))] + self.p.set_progress_bar(_progress) + } + + /// Sets the theme for the application. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_theme(&self, _theme: Option) { + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "macos", + ))] + self.p.set_theme(_theme) + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for EventLoop { + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + rwh_05::HasRawDisplayHandle::raw_display_handle(&**self) + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for EventLoop { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + rwh_06::HasDisplayHandle::display_handle(&**self) + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for EventLoopWindowTarget { + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + self.p.raw_display_handle_rwh_05() + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for EventLoopWindowTarget { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.p.raw_display_handle_rwh_06()?; + // SAFETY: The display will never be deallocated while the event loop is alive. + Ok(unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }) + } +} + +/// Used to send custom events to `EventLoop`. +pub struct EventLoopProxy { + event_loop_proxy: platform_impl::EventLoopProxy, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { + event_loop_proxy: self.event_loop_proxy.clone(), + } + } +} + +impl EventLoopProxy { + /// Send an event to the `EventLoop` from which this proxy was created. This emits a + /// `UserEvent(event)` event in the event loop, where `event` is the value passed to this + /// function. + /// + /// Returns an `Err` if the associated `EventLoop` no longer exists. + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.event_loop_proxy.send_event(event) + } +} + +impl fmt::Debug for EventLoopProxy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("EventLoopProxy { .. }") + } +} + +/// The error that is returned when an `EventLoopProxy` attempts to wake up an `EventLoop` that +/// no longer exists. Contains the original event given to `send_event`. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct EventLoopClosed(pub T); + +impl fmt::Display for EventLoopClosed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Tried to wake up a closed `EventLoop`") + } +} + +impl error::Error for EventLoopClosed {} + +/// Fiter controlling the propagation of device events. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum DeviceEventFilter { + /// Always filter out device events. + Always, + /// Filter out device events while the window is not focused. + Unfocused, + /// Report all device events regardless of window focus. + Never, +} + +impl Default for DeviceEventFilter { + fn default() -> Self { + Self::Unfocused + } +} diff --git a/vendor/tao/src/icon.rs b/vendor/tao/src/icon.rs new file mode 100644 index 0000000000..8827e77bf0 --- /dev/null +++ b/vendor/tao/src/icon.rs @@ -0,0 +1,169 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use crate::platform_impl::PlatformIcon; +use std::{error::Error, fmt, io, mem}; + +#[repr(C)] +#[derive(Debug)] +pub(crate) struct Pixel { + pub(crate) r: u8, + pub(crate) g: u8, + pub(crate) b: u8, + pub(crate) a: u8, +} + +pub(crate) const PIXEL_SIZE: usize = mem::size_of::(); + +#[non_exhaustive] +#[derive(Debug)] +/// An error produced when using `Icon::from_rgba` with invalid arguments. +pub enum BadIcon { + /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be + /// safely interpreted as 32bpp RGBA pixels. + #[non_exhaustive] + ByteCountNotDivisibleBy4 { byte_count: usize }, + /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`. + /// At least one of your arguments is incorrect. + #[non_exhaustive] + DimensionsVsPixelCount { + width: u32, + height: u32, + width_x_height: usize, + pixel_count: usize, + }, + /// Produced when the provided icon width or height is equal to zero. + #[non_exhaustive] + DimensionsZero { width: u32, height: u32 }, + /// Produced when the provided icon width or height is equal to zero. + #[non_exhaustive] + DimensionsMultiplyOverflow { width: u32, height: u32 }, + /// Produced when underlying OS functionality failed to create the icon + OsError(io::Error), +} + +impl fmt::Display for BadIcon { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BadIcon::ByteCountNotDivisibleBy4 { byte_count } => write!(f, + "The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making it impossible to interpret as 32bpp RGBA pixels.", + ), + BadIcon::DimensionsVsPixelCount { + width, + height, + width_x_height, + pixel_count, + } => write!(f, + "The specified dimensions ({width:?}x{height:?}) don't match the number of pixels supplied by the `rgba` argument ({pixel_count:?}). For those dimensions, the expected pixel count is {width_x_height:?}.", + ), + BadIcon::DimensionsZero { + width, + height, + } => write!(f, + "The specified dimensions ({width:?}x{height:?}) must be greater than zero." + ), + BadIcon::DimensionsMultiplyOverflow { + width, + height, + } => write!(f, + "The specified dimensions multiplication has overflowed ({width:?}x{height:?})." + ), + BadIcon::OsError(e) => write!(f, "OS error when instantiating the icon: {e:?}"), + } + } +} + +impl Error for BadIcon { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(self) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct RgbaIcon { + pub(crate) rgba: Vec, + pub(crate) width: u32, + pub(crate) height: u32, +} + +/// For platforms which don't have window icons (e.g. web) +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NoIcon; + +#[allow(dead_code)] // These are not used on every platform +mod constructors { + use super::*; + + impl RgbaIcon { + /// Creates an `Icon` from 32bpp RGBA data. + /// + /// The length of `rgba` must be divisible by 4, and `width * height` must equal + /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error. + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + if width == 0 || height == 0 { + return Err(BadIcon::DimensionsZero { width, height }); + } + + if rgba.len() % PIXEL_SIZE != 0 { + return Err(BadIcon::ByteCountNotDivisibleBy4 { + byte_count: rgba.len(), + }); + } + let width_usize = width as usize; + let height_usize = height as usize; + let width_x_height = match width_usize.checked_mul(height_usize) { + Some(v) => v, + None => return Err(BadIcon::DimensionsMultiplyOverflow { width, height }), + }; + + let pixel_count = rgba.len() / PIXEL_SIZE; + if pixel_count != width_x_height { + Err(BadIcon::DimensionsVsPixelCount { + width, + height, + width_x_height, + pixel_count, + }) + } else { + Ok(RgbaIcon { + rgba, + width, + height, + }) + } + } + } + + impl NoIcon { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + // Create the rgba icon anyway to validate the input + let _ = RgbaIcon::from_rgba(rgba, width, height)?; + Ok(NoIcon) + } + } +} + +/// An icon used for the window titlebar, taskbar, etc. +#[derive(Clone)] +pub struct Icon { + pub(crate) inner: PlatformIcon, +} + +impl fmt::Debug for Icon { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + fmt::Debug::fmt(&self.inner, formatter) + } +} + +impl Icon { + /// Creates an `Icon` from 32bpp RGBA data. + /// + /// The length of `rgba` must be divisible by 4, and `width * height` must equal + /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error. + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + Ok(Icon { + inner: PlatformIcon::from_rgba(rgba, width, height)?, + }) + } +} diff --git a/vendor/tao/src/keyboard.rs b/vendor/tao/src/keyboard.rs new file mode 100644 index 0000000000..ac50d42c53 --- /dev/null +++ b/vendor/tao/src/keyboard.rs @@ -0,0 +1,1596 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +//! **UNSTABLE** -- Types related to the keyboard. + +// This file contains a substantial portion of the UI Events Specification by the W3C. In +// particular, the variant names within `Key` and `KeyCode` and their documentation are modified +// versions of contents of the aforementioned specification. +// +// The original documents are: +// +// ### For `Key` +// UI Events KeyboardEvent key Values +// https://www.w3.org/TR/2017/CR-uievents-key-20170601/ +// Copyright © 2017 W3C® (MIT, ERCIM, Keio, Beihang). +// +// ### For `KeyCode` +// UI Events KeyboardEvent code Values +// https://www.w3.org/TR/2017/CR-uievents-code-20170601/ +// Copyright © 2017 W3C® (MIT, ERCIM, Keio, Beihang). +// +// These documents were used under the terms of the following license. This W3C license as well as +// the W3C short notice apply to the `Key` and `KeyCode` enums and their variants and the +// documentation attached to their variants. + +// --------- BEGGINING OF W3C LICENSE -------------------------------------------------------------- +// +// License +// +// By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, +// and will comply with the following terms and conditions. +// +// Permission to copy, modify, and distribute this work, with or without modification, for any +// purpose and without fee or royalty is hereby granted, provided that you include the following on +// ALL copies of the work or portions thereof, including modifications: +// +// - The full text of this NOTICE in a location viewable to users of the redistributed or derivative +// work. +// - Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none +// exist, the W3C Software and Document Short Notice should be included. +// - Notice of any changes or modifications, through a copyright statement on the new code or +// document such as "This software or document includes material copied from or derived from +// [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." +// +// Disclaimers +// +// THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR +// ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD +// PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +// +// COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES +// ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. +// +// The name and trademarks of copyright holders may NOT be used in advertising or publicity +// pertaining to the work without specific, written prior permission. Title to copyright in this +// work will at all times remain with copyright holders. +// +// --------- END OF W3C LICENSE -------------------------------------------------------------------- + +// --------- BEGGINING OF W3C SHORT NOTICE --------------------------------------------------------- +// +// tao: https://github.com/tauri-apps/tao +// +// Copyright © 2021 World Wide Web Consortium, (Massachusetts Institute of Technology, European +// Research Consortium for Informatics and Mathematics, Keio University, Beihang). All Rights +// Reserved. This work is distributed under the W3C® Software License [1] in the hope that it will +// be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// [1] http://www.w3.org/Consortium/Legal/copyright-software +// +// --------- END OF W3C SHORT NOTICE --------------------------------------------------------------- + +use std::{fmt, str::FromStr}; + +use crate::{ + error::OsError, + platform_impl::{ + keycode_from_scancode as platform_keycode_from_scancode, + keycode_to_scancode as platform_keycode_to_scancode, + }, +}; + +impl ModifiersState { + /// Returns `true` if the shift key is pressed. + pub fn shift_key(&self) -> bool { + self.intersects(Self::SHIFT) + } + /// Returns `true` if the control key is pressed. + pub fn control_key(&self) -> bool { + self.intersects(Self::CONTROL) + } + /// Returns `true` if the alt key is pressed. + pub fn alt_key(&self) -> bool { + self.intersects(Self::ALT) + } + /// Returns `true` if the super key is pressed. + pub fn super_key(&self) -> bool { + self.intersects(Self::SUPER) + } +} + +bitflags! { + /// Represents the current state of the keyboard modifiers + /// + /// Each flag represents a modifier and is set if this modifier is active. + #[derive(Clone, Copy, Default, Debug, PartialEq)] + pub struct ModifiersState: u32 { + // left and right modifiers are currently commented out, but we should be able to support + // them in a future release + /// The "shift" key. + const SHIFT = 0b100 << 0; + // const LSHIFT = 0b010 << 0; + // const RSHIFT = 0b001 << 0; + /// The "control" key. + const CONTROL = 0b100 << 3; + // const LCTRL = 0b010 << 3; + // const RCTRL = 0b001 << 3; + /// The "alt" key. + const ALT = 0b100 << 6; + // const LALT = 0b010 << 6; + // const RALT = 0b001 << 6; + /// This is the "windows" key on PC and "command" key on Mac. + const SUPER = 0b100 << 9; + // const LSUPER = 0b010 << 9; + // const RSUPER = 0b001 << 9; + } +} + +#[cfg(feature = "serde")] +mod modifiers_serde { + use super::ModifiersState; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + #[derive(Default, Serialize, Deserialize)] + #[serde(default)] + #[serde(rename = "ModifiersState")] + pub struct ModifiersStateSerialize { + pub shift_key: bool, + pub control_key: bool, + pub alt_key: bool, + pub super_key: bool, + } + + impl Serialize for ModifiersState { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let s = ModifiersStateSerialize { + shift_key: self.shift_key(), + control_key: self.control_key(), + alt_key: self.alt_key(), + super_key: self.super_key(), + }; + s.serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for ModifiersState { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ModifiersStateSerialize { + shift_key, + control_key, + alt_key, + super_key, + } = ModifiersStateSerialize::deserialize(deserializer)?; + let mut m = ModifiersState::empty(); + m.set(ModifiersState::SHIFT, shift_key); + m.set(ModifiersState::CONTROL, control_key); + m.set(ModifiersState::ALT, alt_key); + m.set(ModifiersState::SUPER, super_key); + Ok(m) + } + } +} + +/// Contains the platform-native physical key identifier (aka scancode) +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum NativeKeyCode { + Unidentified, + Windows(u16), + MacOS(u16), + Gtk(u16), + + /// This is the android "key code" of the event as returned by + /// `KeyEvent.getKeyCode()` + Android(i32), +} + +/// Represents the code of a physical key. +/// +/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few +/// exceptions: +/// - The keys that the specification calls "MetaLeft" and "MetaRight" are named "SuperLeft" and +/// "SuperRight" here. +/// - The key that the specification calls "Super" is reported as `Unidentified` here. +/// - The `Unidentified` variant here, can still identifiy a key through it's `NativeKeyCode`. +/// +/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum KeyCode { + /// This variant is used when the key cannot be translated to any + /// other variant. + /// + /// The native scancode is provided (if available) in order + /// to allow the user to specify keybindings for keys which + /// are not defined by this API. + Unidentified(NativeKeyCode), + /// ` on a US keyboard. This is also called a backtick or grave. + /// This is the 半角/全角/漢字 + /// (hankaku/zenkaku/kanji) key on Japanese keyboards + Backquote, + /// Used for both the US \\ (on the 101-key layout) and also for the key + /// located between the " and Enter keys on row C of the 102-, + /// 104- and 106-key layouts. + /// Labeled # on a UK (102) keyboard. + Backslash, + /// [ on a US keyboard. + BracketLeft, + /// ] on a US keyboard. + BracketRight, + /// , on a US keyboard. + Comma, + /// 0 on a US keyboard. + Digit0, + /// 1 on a US keyboard. + Digit1, + /// 2 on a US keyboard. + Digit2, + /// 3 on a US keyboard. + Digit3, + /// 4 on a US keyboard. + Digit4, + /// 5 on a US keyboard. + Digit5, + /// 6 on a US keyboard. + Digit6, + /// 7 on a US keyboard. + Digit7, + /// 8 on a US keyboard. + Digit8, + /// 9 on a US keyboard. + Digit9, + /// = on a US keyboard. + Equal, + /// Located between the left Shift and Z keys. + /// Labeled \\ on a UK keyboard. + IntlBackslash, + /// Located between the / and right Shift keys. + /// Labeled \\ (ro) on a Japanese keyboard. + IntlRo, + /// Located between the = and Backspace keys. + /// Labeled ¥ (yen) on a Japanese keyboard. \\ on a + /// Russian keyboard. + IntlYen, + /// a on a US keyboard. + /// Labeled q on an AZERTY (e.g., French) keyboard. + KeyA, + /// b on a US keyboard. + KeyB, + /// c on a US keyboard. + KeyC, + /// d on a US keyboard. + KeyD, + /// e on a US keyboard. + KeyE, + /// f on a US keyboard. + KeyF, + /// g on a US keyboard. + KeyG, + /// h on a US keyboard. + KeyH, + /// i on a US keyboard. + KeyI, + /// j on a US keyboard. + KeyJ, + /// k on a US keyboard. + KeyK, + /// l on a US keyboard. + KeyL, + /// m on a US keyboard. + KeyM, + /// n on a US keyboard. + KeyN, + /// o on a US keyboard. + KeyO, + /// p on a US keyboard. + KeyP, + /// q on a US keyboard. + /// Labeled a on an AZERTY (e.g., French) keyboard. + KeyQ, + /// r on a US keyboard. + KeyR, + /// s on a US keyboard. + KeyS, + /// t on a US keyboard. + KeyT, + /// u on a US keyboard. + KeyU, + /// v on a US keyboard. + KeyV, + /// w on a US keyboard. + /// Labeled z on an AZERTY (e.g., French) keyboard. + KeyW, + /// x on a US keyboard. + KeyX, + /// y on a US keyboard. + /// Labeled z on a QWERTZ (e.g., German) keyboard. + KeyY, + /// z on a US keyboard. + /// Labeled w on an AZERTY (e.g., French) keyboard, and y on a + /// QWERTZ (e.g., German) keyboard. + KeyZ, + /// - on a US keyboard. + Minus, + /// Shift+= on a US keyboard. + Plus, + /// . on a US keyboard. + Period, + /// ' on a US keyboard. + Quote, + /// ; on a US keyboard. + Semicolon, + /// / on a US keyboard. + Slash, + /// Alt, Option, or . + AltLeft, + /// Alt, Option, or . + /// This is labeled AltGr on many keyboard layouts. + AltRight, + /// Backspace or . + /// Labeled Delete on Apple keyboards. + Backspace, + /// CapsLock or + CapsLock, + /// The application context menu key, which is typically found between the right + /// Super key and the right Control key. + ContextMenu, + /// Control or + ControlLeft, + /// Control or + ControlRight, + /// Enter or . Labeled Return on Apple keyboards. + Enter, + /// The Windows, , Command, or other OS symbol key. + SuperLeft, + /// The Windows, , Command, or other OS symbol key. + SuperRight, + /// Shift or + ShiftLeft, + /// Shift or + ShiftRight, + /// (space) + Space, + /// Tab or + Tab, + /// Japanese: (henkan) + Convert, + /// Japanese: カタカナ/ひらがな/ローマ字 (katakana/hiragana/romaji) + KanaMode, + /// Korean: HangulMode 한/영 (han/yeong) + /// + /// Japanese (Mac keyboard): (kana) + Lang1, + /// Korean: Hanja (hanja) + /// + /// Japanese (Mac keyboard): (eisu) + Lang2, + /// Japanese (word-processing keyboard): Katakana + Lang3, + /// Japanese (word-processing keyboard): Hiragana + Lang4, + /// Japanese (word-processing keyboard): Zenkaku/Hankaku + Lang5, + /// Japanese: 無変換 (muhenkan) + NonConvert, + /// . The forward delete key. + /// Note that on Apple keyboards, the key labelled Delete on the main part of + /// the keyboard is encoded as [`Backspace`]. + /// + /// [`Backspace`]: Self::Backspace + Delete, + /// Page Down, End, or + End, + /// Help. Not present on standard PC keyboards. + Help, + /// Home or + Home, + /// Insert or Ins. Not present on Apple keyboards. + Insert, + /// Page Down, PgDn, or + PageDown, + /// Page Up, PgUp, or + PageUp, + /// + ArrowDown, + /// + ArrowLeft, + /// + ArrowRight, + /// + ArrowUp, + /// On the Mac, this is used for the numpad Clear key. + NumLock, + /// 0 Ins on a keyboard. 0 on a phone or remote control + Numpad0, + /// 1 End on a keyboard. 1 or 1 QZ on a phone or remote control + Numpad1, + /// 2 ↓ on a keyboard. 2 ABC on a phone or remote control + Numpad2, + /// 3 PgDn on a keyboard. 3 DEF on a phone or remote control + Numpad3, + /// 4 ← on a keyboard. 4 GHI on a phone or remote control + Numpad4, + /// 5 on a keyboard. 5 JKL on a phone or remote control + Numpad5, + /// 6 → on a keyboard. 6 MNO on a phone or remote control + Numpad6, + /// 7 Home on a keyboard. 7 PQRS or 7 PRS on a phone + /// or remote control + Numpad7, + /// 8 ↑ on a keyboard. 8 TUV on a phone or remote control + Numpad8, + /// 9 PgUp on a keyboard. 9 WXYZ or 9 WXY on a phone + /// or remote control + Numpad9, + /// + + NumpadAdd, + /// Found on the Microsoft Natural Keyboard. + NumpadBackspace, + /// C or A (All Clear). Also for use with numpads that have a + /// Clear key that is separate from the NumLock key. On the Mac, the + /// numpad Clear key is encoded as [`NumLock`]. + /// + /// [`NumLock`]: Self::NumLock + NumpadClear, + /// C (Clear Entry) + NumpadClearEntry, + /// , (thousands separator). For locales where the thousands separator + /// is a "." (e.g., Brazil), this key may generate a .. + NumpadComma, + /// . Del. For locales where the decimal separator is "," (e.g., + /// Brazil), this key may generate a ,. + NumpadDecimal, + /// / + NumpadDivide, + NumpadEnter, + /// = + NumpadEqual, + /// # on a phone or remote control device. This key is typically found + /// below the 9 key and to the right of the 0 key. + NumpadHash, + /// M Add current entry to the value stored in memory. + NumpadMemoryAdd, + /// M Clear the value stored in memory. + NumpadMemoryClear, + /// M Replace the current entry with the value stored in memory. + NumpadMemoryRecall, + /// M Replace the value stored in memory with the current entry. + NumpadMemoryStore, + /// M Subtract current entry from the value stored in memory. + NumpadMemorySubtract, + /// * on a keyboard. For use with numpads that provide mathematical + /// operations (+, - * and /). + /// + /// Use `NumpadStar` for the * key on phones and remote controls. + NumpadMultiply, + /// ( Found on the Microsoft Natural Keyboard. + NumpadParenLeft, + /// ) Found on the Microsoft Natural Keyboard. + NumpadParenRight, + /// * on a phone or remote control device. + /// + /// This key is typically found below the 7 key and to the left of + /// the 0 key. + /// + /// Use "NumpadMultiply" for the * key on + /// numeric keypads. + NumpadStar, + /// - + NumpadSubtract, + /// Esc or + Escape, + /// Fn This is typically a hardware key that does not generate a separate code. + Fn, + /// FLock or FnLock. Function Lock key. Found on the Microsoft + /// Natural Keyboard. + FnLock, + /// PrtScr SysRq or Print Screen + PrintScreen, + /// Scroll Lock + ScrollLock, + /// Pause Break + Pause, + /// Some laptops place this key to the left of the key. + /// + /// This also the "back" button (triangle) on Android. + BrowserBack, + BrowserFavorites, + /// Some laptops place this key to the right of the key. + BrowserForward, + /// The "home" button on Android. + BrowserHome, + BrowserRefresh, + BrowserSearch, + BrowserStop, + /// Eject or . This key is placed in the function section on some Apple + /// keyboards. + Eject, + /// Sometimes labelled My Computer on the keyboard + LaunchApp1, + /// Sometimes labelled Calculator on the keyboard + LaunchApp2, + LaunchMail, + MediaPlayPause, + MediaSelect, + MediaStop, + MediaTrackNext, + MediaTrackPrevious, + /// This key is placed in the function section on some Apple keyboards, replacing the + /// Eject key. + Power, + Sleep, + AudioVolumeDown, + AudioVolumeMute, + AudioVolumeUp, + WakeUp, + Hyper, + Turbo, + Abort, + Resume, + Suspend, + /// Found on Sun’s USB keyboard. + Again, + /// Found on Sun’s USB keyboard. + Copy, + /// Found on Sun’s USB keyboard. + Cut, + /// Found on Sun’s USB keyboard. + Find, + /// Found on Sun’s USB keyboard. + Open, + /// Found on Sun’s USB keyboard. + Paste, + /// Found on Sun’s USB keyboard. + Props, + /// Found on Sun’s USB keyboard. + Select, + /// Found on Sun’s USB keyboard. + Undo, + /// Use for dedicated ひらがな key found on some Japanese word processing keyboards. + Hiragana, + /// Use for dedicated カタカナ key found on some Japanese word processing keyboards. + Katakana, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} + +impl KeyCode { + /// Return platform specific scancode. + pub fn to_scancode(self) -> Option { + platform_keycode_to_scancode(self) + } + /// Return `KeyCode` from platform scancode. + pub fn from_scancode(scancode: u32) -> KeyCode { + platform_keycode_from_scancode(scancode) + } +} + +impl FromStr for KeyCode { + type Err = OsError; + fn from_str(accelerator_string: &str) -> Result { + let keycode = match accelerator_string.to_uppercase().as_str() { + "`" | "BACKQUOTE" => KeyCode::Backquote, + "BACKSLASH" => KeyCode::Backslash, + "[" | "BRACKETLEFT" => KeyCode::BracketLeft, + "]" | "BRACKETRIGHT" => KeyCode::BracketRight, + "," | "COMMA" => KeyCode::Comma, + "0" => KeyCode::Digit0, + "1" => KeyCode::Digit1, + "2" => KeyCode::Digit2, + "3" => KeyCode::Digit3, + "4" => KeyCode::Digit4, + "5" => KeyCode::Digit5, + "6" => KeyCode::Digit6, + "7" => KeyCode::Digit7, + "8" => KeyCode::Digit8, + "9" => KeyCode::Digit9, + "NUM0" | "NUMPAD0" => KeyCode::Numpad0, + "NUM1" | "NUMPAD1" => KeyCode::Numpad1, + "NUM2" | "NUMPAD2" => KeyCode::Numpad2, + "NUM3" | "NUMPAD3" => KeyCode::Numpad3, + "NUM4" | "NUMPAD4" => KeyCode::Numpad4, + "NUM5" | "NUMPAD5" => KeyCode::Numpad5, + "NUM6" | "NUMPAD6" => KeyCode::Numpad6, + "NUM7" | "NUMPAD7" => KeyCode::Numpad7, + "NUM8" | "NUMPAD8" => KeyCode::Numpad8, + "NUM9" | "NUMPAD9" => KeyCode::Numpad9, + "=" => KeyCode::Equal, + "-" => KeyCode::Minus, + "PLUS" => KeyCode::Plus, + "." | "PERIOD" => KeyCode::Period, + "'" | "QUOTE" => KeyCode::Quote, + "\\" => KeyCode::IntlBackslash, + "A" => KeyCode::KeyA, + "B" => KeyCode::KeyB, + "C" => KeyCode::KeyC, + "D" => KeyCode::KeyD, + "E" => KeyCode::KeyE, + "F" => KeyCode::KeyF, + "G" => KeyCode::KeyG, + "H" => KeyCode::KeyH, + "I" => KeyCode::KeyI, + "J" => KeyCode::KeyJ, + "K" => KeyCode::KeyK, + "L" => KeyCode::KeyL, + "M" => KeyCode::KeyM, + "N" => KeyCode::KeyN, + "O" => KeyCode::KeyO, + "P" => KeyCode::KeyP, + "Q" => KeyCode::KeyQ, + "R" => KeyCode::KeyR, + "S" => KeyCode::KeyS, + "T" => KeyCode::KeyT, + "U" => KeyCode::KeyU, + "V" => KeyCode::KeyV, + "W" => KeyCode::KeyW, + "X" => KeyCode::KeyX, + "Y" => KeyCode::KeyY, + "Z" => KeyCode::KeyZ, + + ";" | "SEMICOLON" => KeyCode::Semicolon, + "/" | "SLASH" => KeyCode::Slash, + "BACKSPACE" => KeyCode::Backspace, + "CAPSLOCK" => KeyCode::CapsLock, + "CONTEXTMENU" => KeyCode::ContextMenu, + "ENTER" => KeyCode::Enter, + "SPACE" => KeyCode::Space, + "TAB" => KeyCode::Tab, + "CONVERT" => KeyCode::Convert, + + "DELETE" => KeyCode::Delete, + "END" => KeyCode::End, + "HELP" => KeyCode::Help, + "HOME" => KeyCode::Home, + "PAGEDOWN" => KeyCode::PageDown, + "PAGEUP" => KeyCode::PageUp, + + "DOWN" | "ARROWDOWN" => KeyCode::ArrowDown, + "UP" | "ARROWUP" => KeyCode::ArrowUp, + "LEFT" | "ARROWLEFT" => KeyCode::ArrowLeft, + "RIGHT" | "ARROWRIGHT" => KeyCode::ArrowRight, + + "NUMLOCK" => KeyCode::NumLock, + "NUMADD" | "NUMPADADD" => KeyCode::NumpadAdd, + "NUMBACKSPACE" | "NUMPADBACKSPACE" => KeyCode::NumpadBackspace, + "NUMCLEAR" | "NUMPADCLEAR" => KeyCode::NumpadClear, + "NUMCOMMA" | "NUMPADCOMMA" => KeyCode::NumpadComma, + "NUMDIVIDE" | "NUMPADDIVIDE" => KeyCode::NumpadDivide, + "NUMSUBSTRACT" | "NUMPADSUBSTRACT" => KeyCode::NumpadSubtract, + "NUMENTER" | "NUMPADENTER" => KeyCode::NumpadEnter, + + "ESC" | "ESCAPE" => KeyCode::Escape, + "FN" => KeyCode::Fn, + "FNLOCK" => KeyCode::FnLock, + "PRINTSCREEN" => KeyCode::PrintScreen, + "SCROLLLOCK" => KeyCode::ScrollLock, + + "PAUSE" => KeyCode::Pause, + + "VOLUMEMUTE" => KeyCode::AudioVolumeMute, + "VOLUMEDOWN" => KeyCode::AudioVolumeDown, + "VOLUMEUP" => KeyCode::AudioVolumeUp, + "MEDIANEXTTRACK" => KeyCode::MediaTrackNext, + "MEDIAPREVIOUSTRACK" => KeyCode::MediaTrackPrevious, + "MEDIAPLAYPAUSE" => KeyCode::MediaPlayPause, + "LAUNCHMAIL" => KeyCode::LaunchMail, + + "SUSPEND" => KeyCode::Suspend, + "F1" => KeyCode::F1, + "F2" => KeyCode::F2, + "F3" => KeyCode::F3, + "F4" => KeyCode::F4, + "F5" => KeyCode::F5, + "F6" => KeyCode::F6, + "F7" => KeyCode::F7, + "F8" => KeyCode::F8, + "F9" => KeyCode::F9, + "F10" => KeyCode::F10, + "F11" => KeyCode::F11, + "F12" => KeyCode::F12, + "F13" => KeyCode::F13, + "F14" => KeyCode::F14, + "F15" => KeyCode::F15, + "F16" => KeyCode::F16, + "F17" => KeyCode::F17, + "F18" => KeyCode::F18, + "F19" => KeyCode::F19, + "F20" => KeyCode::F20, + "F21" => KeyCode::F21, + "F22" => KeyCode::F22, + "F23" => KeyCode::F23, + "F24" => KeyCode::F24, + "F25" => KeyCode::F25, + "F26" => KeyCode::F26, + "F27" => KeyCode::F27, + "F28" => KeyCode::F28, + "F29" => KeyCode::F29, + "F30" => KeyCode::F30, + "F31" => KeyCode::F31, + "F32" => KeyCode::F32, + "F33" => KeyCode::F33, + "F34" => KeyCode::F34, + "F35" => KeyCode::F35, + _ => KeyCode::Unidentified(NativeKeyCode::Unidentified), + }; + + Ok(keycode) + } +} + +impl fmt::Display for KeyCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + &KeyCode::Unidentified(_) => write!(f, "{:?}", "Unidentified"), + val => write!(f, "{val:?}"), + } + } +} + +/// Key represents the meaning of a keypress. +/// +/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.key`] with a few +/// exceptions: +/// - The `Super` variant here, is named `Meta` in the aforementioned specification. (There's +/// another key which the specification calls `Super`. That does not exist here.) +/// - The `Space` variant here, can be identified by the character it generates in the +/// specificaiton. +/// - The `Unidentified` variant here, can still identifiy a key through it's `NativeKeyCode`. +/// - The `Dead` variant here, can specify the character which is inserted when pressing the +/// dead-key twice. +/// +/// [`KeyboardEvent.key`]: https://w3c.github.io/uievents-key/ +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Key<'a> { + /// A key string that corresponds to the character typed by the user, taking into account the + /// user’s current locale setting, and any system-level keyboard mapping overrides that are in + /// effect. + Character(&'a str), + + /// This variant is used when the key cannot be translated to any other variant. + /// + /// The native scancode is provided (if available) in order to allow the user to specify + /// keybindings for keys which are not defined by this API. + Unidentified(NativeKeyCode), + + /// Contains the text representation of the dead-key when available. + /// + /// ## Platform-specific + /// - **Web:** Always contains `None` + Dead(Option), + + /// The `Alt` (Alternative) key. + /// + /// This key enables the alternate modifier function for interpreting concurrent or subsequent + /// keyboard input. This key value is also used for the Apple Option key. + Alt, + /// The Alternate Graphics (AltGr or AltGraph) key. + /// + /// This key is used enable the ISO Level 3 shift modifier (the standard `Shift` key is the + /// level 2 modifier). + AltGraph, + /// The `Caps Lock` (Capital) key. + /// + /// Toggle capital character lock function for interpreting subsequent keyboard input event. + CapsLock, + /// The `Control` or `Ctrl` key. + /// + /// Used to enable control modifier function for interpreting concurrent or subsequent keyboard + /// input. + Control, + /// The Function switch `Fn` key. Activating this key simultaneously with another key changes + /// that key’s value to an alternate character or function. This key is often handled directly + /// in the keyboard hardware and does not usually generate key events. + Fn, + /// The Function-Lock (`FnLock` or `F-Lock`) key. Activating this key switches the mode of the + /// keyboard to changes some keys' values to an alternate character or function. This key is + /// often handled directly in the keyboard hardware and does not usually generate key events. + FnLock, + /// The `NumLock` or Number Lock key. Used to toggle numpad mode function for interpreting + /// subsequent keyboard input. + NumLock, + /// Toggle between scrolling and cursor movement modes. + ScrollLock, + /// Used to enable shift modifier function for interpreting concurrent or subsequent keyboard + /// input. + Shift, + /// The Symbol modifier key (used on some virtual keyboards). + Symbol, + SymbolLock, + Hyper, + /// Used to enable "super" modifier function for interpreting concurrent or subsequent keyboard + /// input. This key value is used for the "Windows Logo" key and the Apple `Command` or `⌘` key. + /// + /// Note: In some contexts (e.g. the Web) this is referred to as the "Meta" key. + Super, + /// The `Enter` or `↵` key. Used to activate current selection or accept current input. This key + /// value is also used for the `Return` (Macintosh numpad) key. This key value is also used for + /// the Android `KEYCODE_DPAD_CENTER`. + Enter, + /// The Horizontal Tabulation `Tab` key. + Tab, + /// Used in text to insert a space between words. Usually located below the character keys. + Space, + /// Navigate or traverse downward. (`KEYCODE_DPAD_DOWN`) + ArrowDown, + /// Navigate or traverse leftward. (`KEYCODE_DPAD_LEFT`) + ArrowLeft, + /// Navigate or traverse rightward. (`KEYCODE_DPAD_RIGHT`) + ArrowRight, + /// Navigate or traverse upward. (`KEYCODE_DPAD_UP`) + ArrowUp, + /// The End key, used with keyboard entry to go to the end of content (`KEYCODE_MOVE_END`). + End, + /// The Home key, used with keyboard entry, to go to start of content (`KEYCODE_MOVE_HOME`). + /// For the mobile phone `Home` key (which goes to the phone’s main screen), use [`GoHome`]. + /// + /// [`GoHome`]: Self::GoHome + Home, + /// Scroll down or display next page of content. + PageDown, + /// Scroll up or display previous page of content. + PageUp, + /// Used to remove the character to the left of the cursor. This key value is also used for + /// the key labeled `Delete` on MacOS keyboards. + Backspace, + /// Remove the currently selected input. + Clear, + /// Copy the current selection. (`APPCOMMAND_COPY`) + Copy, + /// The Cursor Select key. + CrSel, + /// Cut the current selection. (`APPCOMMAND_CUT`) + Cut, + /// Used to delete the character to the right of the cursor. This key value is also used for the + /// key labeled `Delete` on MacOS keyboards when `Fn` is active. + Delete, + /// The Erase to End of Field key. This key deletes all characters from the current cursor + /// position to the end of the current field. + EraseEof, + /// The Extend Selection (Exsel) key. + ExSel, + /// Toggle between text modes for insertion or overtyping. + /// (`KEYCODE_INSERT`) + Insert, + /// The Paste key. (`APPCOMMAND_PASTE`) + Paste, + /// Redo the last action. (`APPCOMMAND_REDO`) + Redo, + /// Undo the last action. (`APPCOMMAND_UNDO`) + Undo, + /// The Accept (Commit, OK) key. Accept current option or input method sequence conversion. + Accept, + /// Redo or repeat an action. + Again, + /// The Attention (Attn) key. + Attn, + Cancel, + /// Show the application’s context menu. + /// This key is commonly found between the right `Super` key and the right `Control` key. + ContextMenu, + /// The `Esc` key. This key was originally used to initiate an escape sequence, but is + /// now more generally used to exit or "escape" the current context, such as closing a dialog + /// or exiting full screen mode. + Escape, + Execute, + /// Open the Find dialog. (`APPCOMMAND_FIND`) + Find, + /// Open a help dialog or toggle display of help information. (`APPCOMMAND_HELP`, + /// `KEYCODE_HELP`) + Help, + /// Pause the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Pause` button on media controllers. Use `"MediaPause"` + /// instead. + Pause, + /// Play or resume the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Play` button on media controllers. Use `"MediaPlay"` + /// instead. + Play, + /// The properties (Props) key. + Props, + Select, + /// The ZoomIn key. (`KEYCODE_ZOOM_IN`) + ZoomIn, + /// The ZoomOut key. (`KEYCODE_ZOOM_OUT`) + ZoomOut, + /// The Brightness Down key. Typically controls the display brightness. + /// (`KEYCODE_BRIGHTNESS_DOWN`) + BrightnessDown, + /// The Brightness Up key. Typically controls the display brightness. (`KEYCODE_BRIGHTNESS_UP`) + BrightnessUp, + /// Toggle removable media to eject (open) and insert (close) state. (`KEYCODE_MEDIA_EJECT`) + Eject, + LogOff, + /// Toggle power state. (`KEYCODE_POWER`) + /// Note: Note: Some devices might not expose this key to the operating environment. + Power, + /// The `PowerOff` key. Sometime called `PowerDown`. + PowerOff, + /// Initiate print-screen function. + PrintScreen, + /// The Hibernate key. This key saves the current state of the computer to disk so that it can + /// be restored. The computer will then shutdown. + Hibernate, + /// The Standby key. This key turns off the display and places the computer into a low-power + /// mode without completely shutting down. It is sometimes labelled `Suspend` or `Sleep` key. + /// (`KEYCODE_SLEEP`) + Standby, + /// The WakeUp key. (`KEYCODE_WAKEUP`) + WakeUp, + /// Initate the multi-candidate mode. + AllCandidates, + Alphanumeric, + /// Initiate the Code Input mode to allow characters to be entered by + /// their code points. + CodeInput, + /// The Compose key, also known as "Multi_key" on the X Window System. This key acts in a + /// manner similar to a dead key, triggering a mode where subsequent key presses are combined to + /// produce a different character. + Compose, + /// Convert the current input method sequence. + Convert, + /// The Final Mode `Final` key used on some Asian keyboards, to enable the final mode for IMEs. + FinalMode, + /// Switch to the first character group. (ISO/IEC 9995) + GroupFirst, + /// Switch to the last character group. (ISO/IEC 9995) + GroupLast, + /// Switch to the next character group. (ISO/IEC 9995) + GroupNext, + /// Switch to the previous character group. (ISO/IEC 9995) + GroupPrevious, + /// Toggle between or cycle through input modes of IMEs. + ModeChange, + NextCandidate, + /// Accept current input method sequence without + /// conversion in IMEs. + NonConvert, + PreviousCandidate, + Process, + SingleCandidate, + /// Toggle between Hangul and English modes. + HangulMode, + HanjaMode, + JunjaMode, + /// The Eisu key. This key may close the IME, but its purpose is defined by the current IME. + /// (`KEYCODE_EISU`) + Eisu, + /// The (Half-Width) Characters key. + Hankaku, + /// The Hiragana (Japanese Kana characters) key. + Hiragana, + /// The Hiragana/Katakana toggle key. (`KEYCODE_KATAKANA_HIRAGANA`) + HiraganaKatakana, + /// The Kana Mode (Kana Lock) key. This key is used to enter hiragana mode (typically from + /// romaji mode). + KanaMode, + /// The Kanji (Japanese name for ideographic characters of Chinese origin) Mode key. This key is + /// typically used to switch to a hiragana keyboard for the purpose of converting input into + /// kanji. (`KEYCODE_KANA`) + KanjiMode, + /// The Katakana (Japanese Kana characters) key. + Katakana, + /// The Roman characters function key. + Romaji, + /// The Zenkaku (Full-Width) Characters key. + Zenkaku, + /// The Zenkaku/Hankaku (full-width/half-width) toggle key. (`KEYCODE_ZENKAKU_HANKAKU`) + ZenkakuHankaku, + /// General purpose virtual function key, as index 1. + Soft1, + /// General purpose virtual function key, as index 2. + Soft2, + /// General purpose virtual function key, as index 3. + Soft3, + /// General purpose virtual function key, as index 4. + Soft4, + /// Select next (numerically or logically) lower channel. (`APPCOMMAND_MEDIA_CHANNEL_DOWN`, + /// `KEYCODE_CHANNEL_DOWN`) + ChannelDown, + /// Select next (numerically or logically) higher channel. (`APPCOMMAND_MEDIA_CHANNEL_UP`, + /// `KEYCODE_CHANNEL_UP`) + ChannelUp, + /// Close the current document or message (Note: This doesn’t close the application). + /// (`APPCOMMAND_CLOSE`) + Close, + /// Open an editor to forward the current message. (`APPCOMMAND_FORWARD_MAIL`) + MailForward, + /// Open an editor to reply to the current message. (`APPCOMMAND_REPLY_TO_MAIL`) + MailReply, + /// Send the current message. (`APPCOMMAND_SEND_MAIL`) + MailSend, + /// Close the current media, for example to close a CD or DVD tray. (`KEYCODE_MEDIA_CLOSE`) + MediaClose, + /// Initiate or continue forward playback at faster than normal speed, or increase speed if + /// already fast forwarding. (`APPCOMMAND_MEDIA_FAST_FORWARD`, `KEYCODE_MEDIA_FAST_FORWARD`) + MediaFastForward, + /// Pause the currently playing media. (`APPCOMMAND_MEDIA_PAUSE`, `KEYCODE_MEDIA_PAUSE`) + /// + /// Note: Media controller devices should use this value rather than `"Pause"` for their pause + /// keys. + MediaPause, + /// Initiate or continue media playback at normal speed, if not currently playing at normal + /// speed. (`APPCOMMAND_MEDIA_PLAY`, `KEYCODE_MEDIA_PLAY`) + MediaPlay, + /// Toggle media between play and pause states. (`APPCOMMAND_MEDIA_PLAY_PAUSE`, + /// `KEYCODE_MEDIA_PLAY_PAUSE`) + MediaPlayPause, + /// Initiate or resume recording of currently selected media. (`APPCOMMAND_MEDIA_RECORD`, + /// `KEYCODE_MEDIA_RECORD`) + MediaRecord, + /// Initiate or continue reverse playback at faster than normal speed, or increase speed if + /// already rewinding. (`APPCOMMAND_MEDIA_REWIND`, `KEYCODE_MEDIA_REWIND`) + MediaRewind, + /// Stop media playing, pausing, forwarding, rewinding, or recording, if not already stopped. + /// (`APPCOMMAND_MEDIA_STOP`, `KEYCODE_MEDIA_STOP`) + MediaStop, + /// Seek to next media or program track. (`APPCOMMAND_MEDIA_NEXTTRACK`, `KEYCODE_MEDIA_NEXT`) + MediaTrackNext, + /// Seek to previous media or program track. (`APPCOMMAND_MEDIA_PREVIOUSTRACK`, + /// `KEYCODE_MEDIA_PREVIOUS`) + MediaTrackPrevious, + /// Open a new document or message. (`APPCOMMAND_NEW`) + New, + /// Open an existing document or message. (`APPCOMMAND_OPEN`) + Open, + /// Print the current document or message. (`APPCOMMAND_PRINT`) + Print, + /// Save the current document or message. (`APPCOMMAND_SAVE`) + Save, + /// Spellcheck the current document or selection. (`APPCOMMAND_SPELL_CHECK`) + SpellCheck, + /// The `11` key found on media numpads that + /// have buttons from `1` ... `12`. + Key11, + /// The `12` key found on media numpads that + /// have buttons from `1` ... `12`. + Key12, + /// Adjust audio balance leftward. (`VK_AUDIO_BALANCE_LEFT`) + AudioBalanceLeft, + /// Adjust audio balance rightward. (`VK_AUDIO_BALANCE_RIGHT`) + AudioBalanceRight, + /// Decrease audio bass boost or cycle down through bass boost states. (`APPCOMMAND_BASS_DOWN`, + /// `VK_BASS_BOOST_DOWN`) + AudioBassBoostDown, + /// Toggle bass boost on/off. (`APPCOMMAND_BASS_BOOST`) + AudioBassBoostToggle, + /// Increase audio bass boost or cycle up through bass boost states. (`APPCOMMAND_BASS_UP`, + /// `VK_BASS_BOOST_UP`) + AudioBassBoostUp, + /// Adjust audio fader towards front. (`VK_FADER_FRONT`) + AudioFaderFront, + /// Adjust audio fader towards rear. (`VK_FADER_REAR`) + AudioFaderRear, + /// Advance surround audio mode to next available mode. (`VK_SURROUND_MODE_NEXT`) + AudioSurroundModeNext, + /// Decrease treble. (`APPCOMMAND_TREBLE_DOWN`) + AudioTrebleDown, + /// Increase treble. (`APPCOMMAND_TREBLE_UP`) + AudioTrebleUp, + /// Decrease audio volume. (`APPCOMMAND_VOLUME_DOWN`, `KEYCODE_VOLUME_DOWN`) + AudioVolumeDown, + /// Increase audio volume. (`APPCOMMAND_VOLUME_UP`, `KEYCODE_VOLUME_UP`) + AudioVolumeUp, + /// Toggle between muted state and prior volume level. (`APPCOMMAND_VOLUME_MUTE`, + /// `KEYCODE_VOLUME_MUTE`) + AudioVolumeMute, + /// Toggle the microphone on/off. (`APPCOMMAND_MIC_ON_OFF_TOGGLE`) + MicrophoneToggle, + /// Decrease microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_DOWN`) + MicrophoneVolumeDown, + /// Increase microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_UP`) + MicrophoneVolumeUp, + /// Mute the microphone. (`APPCOMMAND_MICROPHONE_VOLUME_MUTE`, `KEYCODE_MUTE`) + MicrophoneVolumeMute, + /// Show correction list when a word is incorrectly identified. (`APPCOMMAND_CORRECTION_LIST`) + SpeechCorrectionList, + /// Toggle between dictation mode and command/control mode. + /// (`APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE`) + SpeechInputToggle, + /// The first generic "LaunchApplication" key. This is commonly associated with launching "My + /// Computer", and may have a computer symbol on the key. (`APPCOMMAND_LAUNCH_APP1`) + LaunchApplication1, + /// The second generic "LaunchApplication" key. This is commonly associated with launching + /// "Calculator", and may have a calculator symbol on the key. (`APPCOMMAND_LAUNCH_APP2`, + /// `KEYCODE_CALCULATOR`) + LaunchApplication2, + /// The "Calendar" key. (`KEYCODE_CALENDAR`) + LaunchCalendar, + /// The "Contacts" key. (`KEYCODE_CONTACTS`) + LaunchContacts, + /// The "Mail" key. (`APPCOMMAND_LAUNCH_MAIL`) + LaunchMail, + /// The "Media Player" key. (`APPCOMMAND_LAUNCH_MEDIA_SELECT`) + LaunchMediaPlayer, + LaunchMusicPlayer, + LaunchPhone, + LaunchScreenSaver, + LaunchSpreadsheet, + LaunchWebBrowser, + LaunchWebCam, + LaunchWordProcessor, + /// Navigate to previous content or page in current history. (`APPCOMMAND_BROWSER_BACKWARD`) + BrowserBack, + /// Open the list of browser favorites. (`APPCOMMAND_BROWSER_FAVORITES`) + BrowserFavorites, + /// Navigate to next content or page in current history. (`APPCOMMAND_BROWSER_FORWARD`) + BrowserForward, + /// Go to the user’s preferred home page. (`APPCOMMAND_BROWSER_HOME`) + BrowserHome, + /// Refresh the current page or content. (`APPCOMMAND_BROWSER_REFRESH`) + BrowserRefresh, + /// Call up the user’s preferred search page. (`APPCOMMAND_BROWSER_SEARCH`) + BrowserSearch, + /// Stop loading the current page or content. (`APPCOMMAND_BROWSER_STOP`) + BrowserStop, + /// The Application switch key, which provides a list of recent apps to switch between. + /// (`KEYCODE_APP_SWITCH`) + AppSwitch, + /// The Call key. (`KEYCODE_CALL`) + Call, + /// The Camera key. (`KEYCODE_CAMERA`) + Camera, + /// The Camera focus key. (`KEYCODE_FOCUS`) + CameraFocus, + /// The End Call key. (`KEYCODE_ENDCALL`) + EndCall, + /// The Back key. (`KEYCODE_BACK`) + GoBack, + /// The Home key, which goes to the phone’s main screen. (`KEYCODE_HOME`) + GoHome, + /// The Headset Hook key. (`KEYCODE_HEADSETHOOK`) + HeadsetHook, + LastNumberRedial, + /// The Notification key. (`KEYCODE_NOTIFICATION`) + Notification, + /// Toggle between manner mode state: silent, vibrate, ring, ... (`KEYCODE_MANNER_MODE`) + MannerMode, + VoiceDial, + /// Switch to viewing TV. (`KEYCODE_TV`) + TV, + /// TV 3D Mode. (`KEYCODE_3D_MODE`) + TV3DMode, + /// Toggle between antenna and cable input. (`KEYCODE_TV_ANTENNA_CABLE`) + TVAntennaCable, + /// Audio description. (`KEYCODE_TV_AUDIO_DESCRIPTION`) + TVAudioDescription, + /// Audio description mixing volume down. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN`) + TVAudioDescriptionMixDown, + /// Audio description mixing volume up. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP`) + TVAudioDescriptionMixUp, + /// Contents menu. (`KEYCODE_TV_CONTENTS_MENU`) + TVContentsMenu, + /// Contents menu. (`KEYCODE_TV_DATA_SERVICE`) + TVDataService, + /// Switch the input mode on an external TV. (`KEYCODE_TV_INPUT`) + TVInput, + /// Switch to component input #1. (`KEYCODE_TV_INPUT_COMPONENT_1`) + TVInputComponent1, + /// Switch to component input #2. (`KEYCODE_TV_INPUT_COMPONENT_2`) + TVInputComponent2, + /// Switch to composite input #1. (`KEYCODE_TV_INPUT_COMPOSITE_1`) + TVInputComposite1, + /// Switch to composite input #2. (`KEYCODE_TV_INPUT_COMPOSITE_2`) + TVInputComposite2, + /// Switch to HDMI input #1. (`KEYCODE_TV_INPUT_HDMI_1`) + TVInputHDMI1, + /// Switch to HDMI input #2. (`KEYCODE_TV_INPUT_HDMI_2`) + TVInputHDMI2, + /// Switch to HDMI input #3. (`KEYCODE_TV_INPUT_HDMI_3`) + TVInputHDMI3, + /// Switch to HDMI input #4. (`KEYCODE_TV_INPUT_HDMI_4`) + TVInputHDMI4, + /// Switch to VGA input #1. (`KEYCODE_TV_INPUT_VGA_1`) + TVInputVGA1, + /// Media context menu. (`KEYCODE_TV_MEDIA_CONTEXT_MENU`) + TVMediaContext, + /// Toggle network. (`KEYCODE_TV_NETWORK`) + TVNetwork, + /// Number entry. (`KEYCODE_TV_NUMBER_ENTRY`) + TVNumberEntry, + /// Toggle the power on an external TV. (`KEYCODE_TV_POWER`) + TVPower, + /// Radio. (`KEYCODE_TV_RADIO_SERVICE`) + TVRadioService, + /// Satellite. (`KEYCODE_TV_SATELLITE`) + TVSatellite, + /// Broadcast Satellite. (`KEYCODE_TV_SATELLITE_BS`) + TVSatelliteBS, + /// Communication Satellite. (`KEYCODE_TV_SATELLITE_CS`) + TVSatelliteCS, + /// Toggle between available satellites. (`KEYCODE_TV_SATELLITE_SERVICE`) + TVSatelliteToggle, + /// Analog Terrestrial. (`KEYCODE_TV_TERRESTRIAL_ANALOG`) + TVTerrestrialAnalog, + /// Digital Terrestrial. (`KEYCODE_TV_TERRESTRIAL_DIGITAL`) + TVTerrestrialDigital, + /// Timer programming. (`KEYCODE_TV_TIMER_PROGRAMMING`) + TVTimer, + /// Switch the input mode on an external AVR (audio/video receiver). (`KEYCODE_AVR_INPUT`) + AVRInput, + /// Toggle the power on an external AVR (audio/video receiver). (`KEYCODE_AVR_POWER`) + AVRPower, + /// General purpose color-coded media function key, as index 0 (red). (`VK_COLORED_KEY_0`, + /// `KEYCODE_PROG_RED`) + ColorF0Red, + /// General purpose color-coded media function key, as index 1 (green). (`VK_COLORED_KEY_1`, + /// `KEYCODE_PROG_GREEN`) + ColorF1Green, + /// General purpose color-coded media function key, as index 2 (yellow). (`VK_COLORED_KEY_2`, + /// `KEYCODE_PROG_YELLOW`) + ColorF2Yellow, + /// General purpose color-coded media function key, as index 3 (blue). (`VK_COLORED_KEY_3`, + /// `KEYCODE_PROG_BLUE`) + ColorF3Blue, + /// General purpose color-coded media function key, as index 4 (grey). (`VK_COLORED_KEY_4`) + ColorF4Grey, + /// General purpose color-coded media function key, as index 5 (brown). (`VK_COLORED_KEY_5`) + ColorF5Brown, + /// Toggle the display of Closed Captions. (`VK_CC`, `KEYCODE_CAPTIONS`) + ClosedCaptionToggle, + /// Adjust brightness of device, by toggling between or cycling through states. (`VK_DIMMER`) + Dimmer, + /// Swap video sources. (`VK_DISPLAY_SWAP`) + DisplaySwap, + /// Select Digital Video Rrecorder. (`KEYCODE_DVR`) + DVR, + /// Exit the current application. (`VK_EXIT`) + Exit, + /// Clear program or content stored as favorite 0. (`VK_CLEAR_FAVORITE_0`) + FavoriteClear0, + /// Clear program or content stored as favorite 1. (`VK_CLEAR_FAVORITE_1`) + FavoriteClear1, + /// Clear program or content stored as favorite 2. (`VK_CLEAR_FAVORITE_2`) + FavoriteClear2, + /// Clear program or content stored as favorite 3. (`VK_CLEAR_FAVORITE_3`) + FavoriteClear3, + /// Select (recall) program or content stored as favorite 0. (`VK_RECALL_FAVORITE_0`) + FavoriteRecall0, + /// Select (recall) program or content stored as favorite 1. (`VK_RECALL_FAVORITE_1`) + FavoriteRecall1, + /// Select (recall) program or content stored as favorite 2. (`VK_RECALL_FAVORITE_2`) + FavoriteRecall2, + /// Select (recall) program or content stored as favorite 3. (`VK_RECALL_FAVORITE_3`) + FavoriteRecall3, + /// Store current program or content as favorite 0. (`VK_STORE_FAVORITE_0`) + FavoriteStore0, + /// Store current program or content as favorite 1. (`VK_STORE_FAVORITE_1`) + FavoriteStore1, + /// Store current program or content as favorite 2. (`VK_STORE_FAVORITE_2`) + FavoriteStore2, + /// Store current program or content as favorite 3. (`VK_STORE_FAVORITE_3`) + FavoriteStore3, + /// Toggle display of program or content guide. (`VK_GUIDE`, `KEYCODE_GUIDE`) + Guide, + /// If guide is active and displayed, then display next day’s content. (`VK_NEXT_DAY`) + GuideNextDay, + /// If guide is active and displayed, then display previous day’s content. (`VK_PREV_DAY`) + GuidePreviousDay, + /// Toggle display of information about currently selected context or media. (`VK_INFO`, + /// `KEYCODE_INFO`) + Info, + /// Toggle instant replay. (`VK_INSTANT_REPLAY`) + InstantReplay, + /// Launch linked content, if available and appropriate. (`VK_LINK`) + Link, + /// List the current program. (`VK_LIST`) + ListProgram, + /// Toggle display listing of currently available live content or programs. (`VK_LIVE`) + LiveContent, + /// Lock or unlock current content or program. (`VK_LOCK`) + Lock, + /// Show a list of media applications: audio/video players and image viewers. (`VK_APPS`) + /// + /// Note: Do not confuse this key value with the Windows' `VK_APPS` / `VK_CONTEXT_MENU` key, + /// which is encoded as `"ContextMenu"`. + MediaApps, + /// Audio track key. (`KEYCODE_MEDIA_AUDIO_TRACK`) + MediaAudioTrack, + /// Select previously selected channel or media. (`VK_LAST`, `KEYCODE_LAST_CHANNEL`) + MediaLast, + /// Skip backward to next content or program. (`KEYCODE_MEDIA_SKIP_BACKWARD`) + MediaSkipBackward, + /// Skip forward to next content or program. (`VK_SKIP`, `KEYCODE_MEDIA_SKIP_FORWARD`) + MediaSkipForward, + /// Step backward to next content or program. (`KEYCODE_MEDIA_STEP_BACKWARD`) + MediaStepBackward, + /// Step forward to next content or program. (`KEYCODE_MEDIA_STEP_FORWARD`) + MediaStepForward, + /// Media top menu. (`KEYCODE_MEDIA_TOP_MENU`) + MediaTopMenu, + /// Navigate in. (`KEYCODE_NAVIGATE_IN`) + NavigateIn, + /// Navigate to next key. (`KEYCODE_NAVIGATE_NEXT`) + NavigateNext, + /// Navigate out. (`KEYCODE_NAVIGATE_OUT`) + NavigateOut, + /// Navigate to previous key. (`KEYCODE_NAVIGATE_PREVIOUS`) + NavigatePrevious, + /// Cycle to next favorite channel (in favorites list). (`VK_NEXT_FAVORITE_CHANNEL`) + NextFavoriteChannel, + /// Cycle to next user profile (if there are multiple user profiles). (`VK_USER`) + NextUserProfile, + /// Access on-demand content or programs. (`VK_ON_DEMAND`) + OnDemand, + /// Pairing key to pair devices. (`KEYCODE_PAIRING`) + Pairing, + /// Move picture-in-picture window down. (`VK_PINP_DOWN`) + PinPDown, + /// Move picture-in-picture window. (`VK_PINP_MOVE`) + PinPMove, + /// Toggle display of picture-in-picture window. (`VK_PINP_TOGGLE`) + PinPToggle, + /// Move picture-in-picture window up. (`VK_PINP_UP`) + PinPUp, + /// Decrease media playback speed. (`VK_PLAY_SPEED_DOWN`) + PlaySpeedDown, + /// Reset playback to normal speed. (`VK_PLAY_SPEED_RESET`) + PlaySpeedReset, + /// Increase media playback speed. (`VK_PLAY_SPEED_UP`) + PlaySpeedUp, + /// Toggle random media or content shuffle mode. (`VK_RANDOM_TOGGLE`) + RandomToggle, + /// Not a physical key, but this key code is sent when the remote control battery is low. + /// (`VK_RC_LOW_BATTERY`) + RcLowBattery, + /// Toggle or cycle between media recording speeds. (`VK_RECORD_SPEED_NEXT`) + RecordSpeedNext, + /// Toggle RF (radio frequency) input bypass mode (pass RF input directly to the RF output). + /// (`VK_RF_BYPASS`) + RfBypass, + /// Toggle scan channels mode. (`VK_SCAN_CHANNELS_TOGGLE`) + ScanChannelsToggle, + /// Advance display screen mode to next available mode. (`VK_SCREEN_MODE_NEXT`) + ScreenModeNext, + /// Toggle display of device settings screen. (`VK_SETTINGS`, `KEYCODE_SETTINGS`) + Settings, + /// Toggle split screen mode. (`VK_SPLIT_SCREEN_TOGGLE`) + SplitScreenToggle, + /// Switch the input mode on an external STB (set top box). (`KEYCODE_STB_INPUT`) + STBInput, + /// Toggle the power on an external STB (set top box). (`KEYCODE_STB_POWER`) + STBPower, + /// Toggle display of subtitles, if available. (`VK_SUBTITLE`) + Subtitle, + /// Toggle display of teletext, if available (`VK_TELETEXT`, `KEYCODE_TV_TELETEXT`). + Teletext, + /// Advance video mode to next available mode. (`VK_VIDEO_MODE_NEXT`) + VideoModeNext, + /// Cause device to identify itself in some manner, e.g., audibly or visibly. (`VK_WINK`) + Wink, + /// Toggle between full-screen and scaled content, or alter magnification level. (`VK_ZOOM`, + /// `KEYCODE_TV_ZOOM_MODE`) + ZoomToggle, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} + +impl<'a> Key<'a> { + pub fn to_text(&self) -> Option<&'a str> { + match self { + Key::Character(ch) => Some(*ch), + Key::Enter => Some("\r"), + Key::Backspace => Some("\x08"), + Key::Tab => Some("\t"), + Key::Space => Some(" "), + Key::Escape => Some("\x1b"), + _ => None, + } + } +} + +impl<'a> From<&'a str> for Key<'a> { + fn from(src: &'a str) -> Key<'a> { + Key::Character(src) + } +} + +/// Represents the location of a physical key. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum KeyLocation { + Standard, + Left, + Right, + Numpad, +} diff --git a/vendor/tao/src/lib.rs b/vendor/tao/src/lib.rs new file mode 100644 index 0000000000..072dbb6508 --- /dev/null +++ b/vendor/tao/src/lib.rs @@ -0,0 +1,199 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +//! Tao is a cross-platform application window creation and event loop management library. +//! +//! # Building windows +//! +//! Before you can build a [`Window`], you first need to build an [`EventLoop`]. This is done with the +//! [`EventLoop::new()`] function. +//! +//! ```no_run +//! use tao::event_loop::EventLoop; +//! let event_loop = EventLoop::new(); +//! ``` +//! +//! Once this is done there are two ways to create a [`Window`]: +//! +//! - Calling [`Window::new(&event_loop)`][window_new]. +//! - Calling [`let builder = WindowBuilder::new()`][window_builder_new] then [`builder.build(&event_loop)`][window_builder_build]. +//! +//! The first method is the simplest, and will give you default values for everything. The second +//! method allows you to customize the way your [`Window`] will look and behave by modifying the +//! fields of the [`WindowBuilder`] object before you create the [`Window`]. +//! +//! # Event handling +//! +//! Once a [`Window`] has been created, it will generate different *events*. A [`Window`] object can +//! generate [`WindowEvent`]s when certain input events occur, such as a cursor moving over the +//! window or a key getting pressed while the window is focused. Devices can generate +//! [`DeviceEvent`]s, which contain unfiltered event data that isn't specific to a certain window. +//! Some user activity, like mouse movement, can generate both a [`WindowEvent`] *and* a +//! [`DeviceEvent`]. You can also create and handle your own custom [`UserEvent`]s, if desired. +//! +//! You can retrieve events by calling [`EventLoop::run`][event_loop_run]. This function will +//! dispatch events for every [`Window`] that was created with that particular [`EventLoop`], and +//! will run until the `control_flow` argument given to the closure is set to +//! [`ControlFlow`]`::`[`ExitWithCode`] (which [`ControlFlow`]`::`[`Exit`] aliases to), at which +//! point [`Event`]`::`[`LoopDestroyed`] is emitted and the entire program terminates. +//! +//! Tao no longer uses a `EventLoop::poll_events() -> impl Iterator`-based event loop +//! model, since that can't be implemented properly on some platforms (e.g web, iOS) and works poorly on +//! most other platforms. However, this model can be re-implemented to an extent with +//! [`EventLoopExtRunReturn::run_return`]. See that method's documentation for more reasons about why +//! it's discouraged, beyond compatibility reasons. +//! +//! +//! ```no_run +//! use tao::{ +//! event::{Event, WindowEvent}, +//! event_loop::{ControlFlow, EventLoop}, +//! window::WindowBuilder, +//! }; +//! +//! let event_loop = EventLoop::new(); +//! let window = WindowBuilder::new().build(&event_loop).unwrap(); +//! +//! event_loop.run(move |event, _, control_flow| { +//! // ControlFlow::Poll continuously runs the event loop, even if the OS hasn't +//! // dispatched any events. This is ideal for games and similar applications. +//! *control_flow = ControlFlow::Poll; +//! +//! // ControlFlow::Wait pauses the event loop if no events are available to process. +//! // This is ideal for non-game applications that only update in response to user +//! // input, and uses significantly less power/CPU time than ControlFlow::Poll. +//! *control_flow = ControlFlow::Wait; +//! +//! match event { +//! Event::WindowEvent { +//! event: WindowEvent::CloseRequested, +//! .. +//! } => { +//! println!("The close button was pressed; stopping"); +//! *control_flow = ControlFlow::Exit +//! }, +//! Event::MainEventsCleared => { +//! // Application update code. +//! +//! // Queue a RedrawRequested event. +//! // +//! // You only need to call this if you've determined that you need to redraw, in +//! // applications which do not always need to. Applications that redraw continuously +//! // can just render here instead. +//! window.request_redraw(); +//! }, +//! Event::RedrawRequested(_) => { +//! // Redraw the application. +//! // +//! // It's preferable for applications that do not render continuously to render in +//! // this event rather than in MainEventsCleared, since rendering in here allows +//! // the program to gracefully handle redraws requested by the OS. +//! }, +//! _ => () +//! } +//! }); +//! ``` +//! +//! [`Event`]`::`[`WindowEvent`] has a [`WindowId`] member. In multi-window environments, it should be +//! compared to the value returned by [`Window::id()`][window_id_fn] to determine which [`Window`] +//! dispatched the event. +//! +//! # Drawing on the window +//! +//! Tao doesn't directly provide any methods for drawing on a [`Window`]. However it allows you to +//! retrieve the raw handle of the window and display (see the [`platform`] module and/or the +//! [`raw_window_handle`] and [`raw_display_handle`] methods), which in turn allows you to create an +//! OpenGL/Vulkan/DirectX/Metal/etc. context that can be used to render graphics. +//! +//! Note that many platforms will display garbage data in the window's client area if the +//! application doesn't render anything to the window by the time the desktop compositor is ready to +//! display the window to the user. If you notice this happening, you should create the window with +//! [`visible` set to `false`](crate::window::WindowBuilder::with_visible) and explicitly make the +//! window visible only once you're ready to render into it. +//! +//! [`EventLoop`]: event_loop::EventLoop +//! [`EventLoopExtRunReturn::run_return`]: ./platform/run_return/trait.EventLoopExtRunReturn.html#tymethod.run_return +//! [`EventLoop::new()`]: event_loop::EventLoop::new +//! [event_loop_run]: event_loop::EventLoop::run +//! [`ControlFlow`]: event_loop::ControlFlow +//! [`Exit`]: event_loop::ControlFlow::Exit +//! [`ExitWithCode`]: event_loop::ControlFlow::ExitWithCode +//! [`Window`]: window::Window +//! [`WindowId`]: window::WindowId +//! [`WindowBuilder`]: window::WindowBuilder +//! [window_new]: window::Window::new +//! [window_builder_new]: window::WindowBuilder::new +//! [window_builder_build]: window::WindowBuilder::build +//! [window_id_fn]: window::Window::id +//! [`Event`]: event::Event +//! [`WindowEvent`]: event::WindowEvent +//! [`DeviceEvent`]: event::DeviceEvent +//! [`UserEvent`]: event::Event::UserEvent +//! [`LoopDestroyed`]: event::Event::LoopDestroyed +//! [`platform`]: platform +//! [`raw_window_handle`]: ./window/struct.Window.html#method.raw_window_handle +//! [`raw_display_handle`]: ./window/struct.Window.html#method.raw_display_handle +#![allow( + clippy::match_str_case_mismatch, + clippy::upper_case_acronyms, + clippy::from_over_into, + clippy::option_map_unit_fn, + clippy::needless_lifetimes, + clippy::type_complexity, + clippy::identity_op, + clippy::wrong_self_convention, + clippy::non_send_fields_in_send_ty +)] +#![deny(rustdoc::broken_intra_doc_links)] + +use dpi::PixelUnit; +#[cfg(feature = "rwh_04")] +pub use rwh_04; +#[cfg(feature = "rwh_05")] +pub use rwh_05; +#[cfg(feature = "rwh_06")] +pub use rwh_06; + +#[allow(unused_imports)] +#[macro_use] +extern crate lazy_static; +#[allow(unused_imports)] +#[macro_use] +extern crate log; +#[cfg(feature = "serde")] +#[macro_use] +extern crate serde; +#[macro_use] +extern crate bitflags; +#[cfg(any(target_os = "macos", target_os = "ios"))] +#[macro_use(class, msg_send, sel)] +extern crate objc2; + +pub use dpi; + +#[macro_use] +pub mod error; +pub mod event; +pub mod event_loop; +mod icon; +pub mod keyboard; +pub mod monitor; +mod platform_impl; + +pub mod window; + +pub mod platform; + +pub(crate) fn extract_width_height(size: dpi::Size) -> (PixelUnit, PixelUnit) { + match size { + dpi::Size::Physical(size) => ( + PixelUnit::Physical(size.width.into()), + PixelUnit::Physical(size.height.into()), + ), + dpi::Size::Logical(size) => ( + PixelUnit::Logical(size.width.into()), + PixelUnit::Logical(size.height.into()), + ), + } +} diff --git a/vendor/tao/src/monitor.rs b/vendor/tao/src/monitor.rs new file mode 100644 index 0000000000..7e5404f578 --- /dev/null +++ b/vendor/tao/src/monitor.rs @@ -0,0 +1,163 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +//! Types useful for interacting with a user's monitors. +//! +//! If you want to get basic information about a monitor, you can use the [`MonitorHandle`][monitor_handle] +//! type. This is retrieved from one of the following methods, which return an iterator of +//! [`MonitorHandle`][monitor_handle]: +//! - [`EventLoopWindowTarget::available_monitors`][loop_get] +//! - [`Window::available_monitors`][window_get]. +//! +//! [monitor_handle]: crate::monitor::MonitorHandle +//! [loop_get]: crate::event_loop::EventLoopWindowTarget::available_monitors +//! [window_get]: crate::window::Window::available_monitors +use crate::{ + dpi::{PhysicalPosition, PhysicalSize}, + platform_impl, +}; + +/// Describes a fullscreen video mode of a monitor. +/// +/// Can be acquired with: +/// - [`MonitorHandle::video_modes`][monitor_get]. +/// +/// [monitor_get]: crate::monitor::MonitorHandle::video_modes +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct VideoMode { + pub(crate) video_mode: platform_impl::VideoMode, +} + +impl std::fmt::Debug for VideoMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.video_mode.fmt(f) + } +} + +impl PartialOrd for VideoMode { + fn partial_cmp(&self, other: &VideoMode) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VideoMode { + fn cmp(&self, other: &VideoMode) -> std::cmp::Ordering { + // TODO: we can impl `Ord` for `PhysicalSize` once we switch from `f32` + // to `u32` there + let size: (u32, u32) = self.size().into(); + let other_size: (u32, u32) = other.size().into(); + self.monitor().cmp(&other.monitor()).then( + size + .cmp(&other_size) + .then( + self + .refresh_rate() + .cmp(&other.refresh_rate()) + .then(self.bit_depth().cmp(&other.bit_depth())), + ) + .reverse(), + ) + } +} + +impl VideoMode { + /// Returns the resolution of this video mode. + #[inline] + pub fn size(&self) -> PhysicalSize { + self.video_mode.size() + } + + /// Returns the bit depth of this video mode, as in how many bits you have + /// available per color. This is generally 24 bits or 32 bits on modern + /// systems, depending on whether the alpha channel is counted or not. + /// + /// ## Platform-specific + /// - **iOS:** Always returns 32. + #[inline] + pub fn bit_depth(&self) -> u16 { + self.video_mode.bit_depth() + } + + /// Returns the refresh rate of this video mode. **Note**: the returned + /// refresh rate is an integer approximation, and you shouldn't rely on this + /// value to be exact. + #[inline] + pub fn refresh_rate(&self) -> u16 { + self.video_mode.refresh_rate() + } + + /// Returns the monitor that this video mode is valid for. Each monitor has + /// a separate set of valid video modes. + #[inline] + pub fn monitor(&self) -> MonitorHandle { + self.video_mode.monitor() + } +} + +impl std::fmt::Display for VideoMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}x{} @ {} Hz ({} bpp)", + self.size().width, + self.size().height, + self.refresh_rate(), + self.bit_depth() + ) + } +} + +/// Handle to a monitor. +/// +/// Allows you to retrieve information about a given monitor and can be used in [`Window`] creation. +/// +/// [`Window`]: crate::window::Window +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct MonitorHandle { + pub(crate) inner: platform_impl::MonitorHandle, +} + +impl MonitorHandle { + /// Returns a human-readable name of the monitor. + /// + /// Returns `None` if the monitor doesn't exist anymore. + #[inline] + pub fn name(&self) -> Option { + self.inner.name() + } + + /// Returns the monitor's resolution. + #[inline] + pub fn size(&self) -> PhysicalSize { + self.inner.size() + } + + /// Returns the top-left corner position of the monitor relative to the larger full + /// screen area. + #[inline] + pub fn position(&self) -> PhysicalPosition { + self.inner.position() + } + + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. + /// + /// See the [`dpi`](crate::dpi) module for more information. + /// + /// ## Platform-specific + /// + /// - **Android:** Always returns 1.0. + #[inline] + pub fn scale_factor(&self) -> f64 { + self.inner.scale_factor() + } + + /// Returns all fullscreen video modes supported by this monitor. + /// + /// ## Platform-specific + /// - **Linux:** Unsupported. This will always return empty iterator. + #[inline] + pub fn video_modes(&self) -> impl Iterator { + self.inner.video_modes() + } +} diff --git a/vendor/tao/src/platform/android.rs b/vendor/tao/src/platform/android.rs new file mode 100644 index 0000000000..47b45c76e8 --- /dev/null +++ b/vendor/tao/src/platform/android.rs @@ -0,0 +1,48 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(target_os = "android")] + +pub mod prelude { + pub use crate::platform_impl::ndk_glue::*; + pub use tao_macros::{android_fn, generate_package_name}; +} +use crate::{ + event_loop::{EventLoop, EventLoopWindowTarget}, + platform_impl::ndk_glue::Rect, + window::{Window, WindowBuilder}, +}; +use ndk::configuration::Configuration; + +/// Additional methods on `EventLoop` that are specific to Android. +pub trait EventLoopExtAndroid {} + +impl EventLoopExtAndroid for EventLoop {} + +/// Additional methods on `EventLoopWindowTarget` that are specific to Android. +pub trait EventLoopWindowTargetExtAndroid {} + +/// Additional methods on `Window` that are specific to Android. +pub trait WindowExtAndroid { + fn content_rect(&self) -> Rect; + + fn config(&self) -> Configuration; +} + +impl WindowExtAndroid for Window { + fn content_rect(&self) -> Rect { + self.window.content_rect() + } + + fn config(&self) -> Configuration { + self.window.config() + } +} + +impl EventLoopWindowTargetExtAndroid for EventLoopWindowTarget {} + +/// Additional methods on `WindowBuilder` that are specific to Android. +pub trait WindowBuilderExtAndroid {} + +impl WindowBuilderExtAndroid for WindowBuilder {} diff --git a/vendor/tao/src/platform/ios.rs b/vendor/tao/src/platform/ios.rs new file mode 100644 index 0000000000..42b1f8a279 --- /dev/null +++ b/vendor/tao/src/platform/ios.rs @@ -0,0 +1,345 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(target_os = "ios")] + +use std::os::raw::c_void; + +use crate::{ + event_loop::{EventLoop, EventLoopWindowTarget}, + monitor::{MonitorHandle, VideoMode}, + platform_impl::set_badge_count, + window::{Window, WindowBuilder}, +}; + +/// Additional methods on [`EventLoop`] that are specific to iOS. +pub trait EventLoopExtIOS { + /// Returns the [`Idiom`] (phone/tablet/tv/etc) for the current device. + fn idiom(&self) -> Idiom; +} + +impl EventLoopExtIOS for EventLoop { + fn idiom(&self) -> Idiom { + self.event_loop.idiom() + } +} + +/// Additional methods on [`Window`] that are specific to iOS. +pub trait WindowExtIOS { + /// Returns a pointer to the [`UIWindow`] that is used by this window. + /// + /// The pointer will become invalid when the [`Window`] is destroyed. + /// + /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc + fn ui_window(&self) -> *mut c_void; + + /// Returns a pointer to the [`UIViewController`] that is used by this window. + /// + /// The pointer will become invalid when the [`Window`] is destroyed. + /// + /// [`UIViewController`]: https://developer.apple.com/documentation/uikit/uiviewcontroller?language=objc + fn ui_view_controller(&self) -> *mut c_void; + + /// Returns a pointer to the [`UIView`] that is used by this window. + /// + /// The pointer will become invalid when the [`Window`] is destroyed. + /// + /// [`UIView`]: https://developer.apple.com/documentation/uikit/uiview?language=objc + fn ui_view(&self) -> *mut c_void; + + /// Sets the [`contentScaleFactor`] of the underlying [`UIWindow`] to `scale_factor`. + /// + /// The default value is device dependent, and it's recommended GLES or Metal applications set + /// this to [`MonitorHandle::scale_factor()`]. + /// + /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc + /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc + fn set_scale_factor(&self, scale_factor: f64); + + /// Sets the valid orientations for the [`Window`]. + /// + /// The default value is [`ValidOrientations::LandscapeAndPortrait`]. + /// + /// This changes the value returned by + /// [`-[UIViewController supportedInterfaceOrientations]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations?language=objc), + /// and then calls + /// [`-[UIViewController attemptRotationToDeviceOrientation]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621400-attemptrotationtodeviceorientati?language=objc). + fn set_valid_orientations(&self, valid_orientations: ValidOrientations); + + /// Sets whether the [`Window`] prefers the home indicator hidden. + /// + /// The default is to prefer showing the home indicator. + /// + /// This changes the value returned by + /// [`-[UIViewController prefersHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden?language=objc), + /// and then calls + /// [`-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887509-setneedsupdateofhomeindicatoraut?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn set_prefers_home_indicator_hidden(&self, hidden: bool); + + /// Sets the screen edges for which the system gestures will take a lower priority than the + /// application's touch handling. + /// + /// This changes the value returned by + /// [`-[UIViewController preferredScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys?language=objc), + /// and then calls + /// [`-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887507-setneedsupdateofscreenedgesdefer?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge); + + /// Sets whether the [`Window`] prefers the status bar hidden. + /// + /// The default is to prefer showing the status bar. + /// + /// This changes the value returned by + /// [`-[UIViewController prefersStatusBarHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc), + /// and then calls + /// [`-[UIViewController setNeedsStatusBarAppearanceUpdate]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc). + fn set_prefers_status_bar_hidden(&self, hidden: bool); + + /// Sets the badge count on iOS launcher. 0 hides the count + fn set_badge_count(&self, count: i32); +} + +impl WindowExtIOS for Window { + #[inline] + fn ui_window(&self) -> *mut c_void { + self.window.ui_window() as _ + } + + #[inline] + fn ui_view_controller(&self) -> *mut c_void { + self.window.ui_view_controller() as _ + } + + #[inline] + fn ui_view(&self) -> *mut c_void { + self.window.ui_view() as _ + } + + #[inline] + fn set_scale_factor(&self, scale_factor: f64) { + self.window.set_scale_factor(scale_factor) + } + + #[inline] + fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { + self.window.set_valid_orientations(valid_orientations) + } + + #[inline] + fn set_prefers_home_indicator_hidden(&self, hidden: bool) { + self.window.set_prefers_home_indicator_hidden(hidden) + } + + #[inline] + fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { + self + .window + .set_preferred_screen_edges_deferring_system_gestures(edges) + } + + #[inline] + fn set_prefers_status_bar_hidden(&self, hidden: bool) { + self.window.set_prefers_status_bar_hidden(hidden) + } + + #[inline] + fn set_badge_count(&self, count: i32) { + self.window.set_badge_count(count) + } +} + +pub trait EventLoopWindowTargetExtIOS { + /// Sets the badge count on iOS launcher. 0 hides the count + fn set_badge_count(&self, count: i32); +} + +impl EventLoopWindowTargetExtIOS for EventLoopWindowTarget { + fn set_badge_count(&self, count: i32) { + set_badge_count(count) + } +} + +/// Additional methods on [`WindowBuilder`] that are specific to iOS. +pub trait WindowBuilderExtIOS { + /// Sets the root view class used by the [`Window`], otherwise a barebones [`UIView`] is provided. + /// + /// An instance of the class will be initialized by calling [`-[UIView initWithFrame:]`](https://developer.apple.com/documentation/uikit/uiview/1622488-initwithframe?language=objc). + /// + /// [`UIView`]: https://developer.apple.com/documentation/uikit/uiview?language=objc + fn with_root_view_class(self, root_view_class: *const c_void) -> WindowBuilder; + + /// Sets the [`contentScaleFactor`] of the underlying [`UIWindow`] to `scale_factor`. + /// + /// The default value is device dependent, and it's recommended GLES or Metal applications set + /// this to [`MonitorHandle::scale_factor()`]. + /// + /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc + /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc + fn with_scale_factor(self, scale_factor: f64) -> WindowBuilder; + + /// Sets the valid orientations for the [`Window`]. + /// + /// The default value is [`ValidOrientations::LandscapeAndPortrait`]. + /// + /// This sets the initial value returned by + /// [`-[UIViewController supportedInterfaceOrientations]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations?language=objc). + fn with_valid_orientations(self, valid_orientations: ValidOrientations) -> WindowBuilder; + + /// Sets whether the [`Window`] prefers the home indicator hidden. + /// + /// The default is to prefer showing the home indicator. + /// + /// This sets the initial value returned by + /// [`-[UIViewController prefersHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn with_prefers_home_indicator_hidden(self, hidden: bool) -> WindowBuilder; + + /// Sets the screen edges for which the system gestures will take a lower priority than the + /// application's touch handling. + /// + /// This sets the initial value returned by + /// [`-[UIViewController preferredScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn with_preferred_screen_edges_deferring_system_gestures( + self, + edges: ScreenEdge, + ) -> WindowBuilder; + + /// Sets whether the [`Window`] prefers the status bar hidden. + /// + /// The default is to prefer showing the status bar. + /// + /// This sets the initial value returned by + /// [`-[UIViewController prefersStatusBarHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc). + fn with_prefers_status_bar_hidden(self, hidden: bool) -> WindowBuilder; +} + +impl WindowBuilderExtIOS for WindowBuilder { + #[inline] + fn with_root_view_class(mut self, root_view_class: *const c_void) -> WindowBuilder { + self.platform_specific.root_view_class = unsafe { &*(root_view_class as *const _) }; + self + } + + #[inline] + fn with_scale_factor(mut self, scale_factor: f64) -> WindowBuilder { + self.platform_specific.scale_factor = Some(scale_factor); + self + } + + #[inline] + fn with_valid_orientations(mut self, valid_orientations: ValidOrientations) -> WindowBuilder { + self.platform_specific.valid_orientations = valid_orientations; + self + } + + #[inline] + fn with_prefers_home_indicator_hidden(mut self, hidden: bool) -> WindowBuilder { + self.platform_specific.prefers_home_indicator_hidden = hidden; + self + } + + #[inline] + fn with_preferred_screen_edges_deferring_system_gestures( + mut self, + edges: ScreenEdge, + ) -> WindowBuilder { + self + .platform_specific + .preferred_screen_edges_deferring_system_gestures = edges; + self + } + + #[inline] + fn with_prefers_status_bar_hidden(mut self, hidden: bool) -> WindowBuilder { + self.platform_specific.prefers_status_bar_hidden = hidden; + self + } +} + +/// Additional methods on [`MonitorHandle`] that are specific to iOS. +pub trait MonitorHandleExtIOS { + /// Returns a pointer to the [`UIScreen`] that is used by this monitor. + /// + /// [`UIScreen`]: https://developer.apple.com/documentation/uikit/uiscreen?language=objc + fn ui_screen(&self) -> *mut c_void; + + /// Returns the preferred [`VideoMode`] for this monitor. + /// + /// This translates to a call to [`-[UIScreen preferredMode]`](https://developer.apple.com/documentation/uikit/uiscreen/1617823-preferredmode?language=objc). + fn preferred_video_mode(&self) -> VideoMode; +} + +impl MonitorHandleExtIOS for MonitorHandle { + #[inline] + fn ui_screen(&self) -> *mut c_void { + self.inner.ui_screen() as _ + } + + #[inline] + fn preferred_video_mode(&self) -> VideoMode { + self.inner.preferred_video_mode() + } +} + +/// Valid orientations for a particular [`Window`]. +#[non_exhaustive] +#[derive(Clone, Copy, Debug)] +pub enum ValidOrientations { + /// Excludes `PortraitUpsideDown` on iphone + LandscapeAndPortrait, + + Landscape, + + /// Excludes `PortraitUpsideDown` on iphone + Portrait, +} + +impl Default for ValidOrientations { + #[inline] + fn default() -> ValidOrientations { + ValidOrientations::LandscapeAndPortrait + } +} + +/// The device [idiom]. +/// +/// [idiom]: https://developer.apple.com/documentation/uikit/uidevice/1620037-userinterfaceidiom?language=objc +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Idiom { + Unspecified, + + /// iPhone and iPod touch. + Phone, + + /// iPad. + Pad, + + /// tvOS and Apple TV. + TV, + CarPlay, +} + +bitflags! { + /// The [edges] of a screen. + /// + /// [edges]: https://developer.apple.com/documentation/uikit/uirectedge?language=objc + #[derive(Clone, Copy ,Default)] + pub struct ScreenEdge: u8 { + const NONE = 0; + const TOP = 1 << 0; + const LEFT = 1 << 1; + const BOTTOM = 1 << 2; + const RIGHT = 1 << 3; + const ALL = ScreenEdge::TOP.bits() | ScreenEdge::LEFT.bits() + | ScreenEdge::BOTTOM.bits() | ScreenEdge::RIGHT.bits(); + } +} diff --git a/vendor/tao/src/platform/linux.rs b/vendor/tao/src/platform/linux.rs new file mode 100644 index 0000000000..2f0603c23a --- /dev/null +++ b/vendor/tao/src/platform/linux.rs @@ -0,0 +1,5 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(target_os = "linux")] diff --git a/vendor/tao/src/platform/macos.rs b/vendor/tao/src/platform/macos.rs new file mode 100644 index 0000000000..32b8cbbf3f --- /dev/null +++ b/vendor/tao/src/platform/macos.rs @@ -0,0 +1,458 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(target_os = "macos")] + +use std::os::raw::c_void; + +use objc2_foundation::NSObject; + +use crate::{ + dpi::{LogicalSize, Position}, + event_loop::{EventLoop, EventLoopWindowTarget}, + monitor::MonitorHandle, + platform_impl::{get_aux_state_mut, set_badge_label, set_dock_visibility, Parent}, + window::{Window, WindowBuilder}, +}; + +/// Additional methods on `Window` that are specific to MacOS. +pub trait WindowExtMacOS { + /// Returns a pointer to the cocoa `NSWindow` that is used by this window. + /// + /// The pointer will become invalid when the `Window` is destroyed. + fn ns_window(&self) -> *mut c_void; + + /// Returns a pointer to the cocoa `NSView` that is used by this window. + /// + /// The pointer will become invalid when the `Window` is destroyed. + fn ns_view(&self) -> *mut c_void; + + /// Returns whether or not the window is in simple fullscreen mode. + fn simple_fullscreen(&self) -> bool; + + /// Toggles a fullscreen mode that doesn't require a new macOS space. + /// Returns a boolean indicating whether the transition was successful (this + /// won't work if the window was already in the native fullscreen). + /// + /// This is how fullscreen used to work on macOS in versions before Lion. + /// And allows the user to have a fullscreen window without using another + /// space or taking control over the entire monitor. + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool; + + /// Returns whether or not the window has shadow. + fn has_shadow(&self) -> bool; + + /// Sets whether or not the window has shadow. + fn set_has_shadow(&self, has_shadow: bool); + + /// Set the window traffic light position relative to the upper left corner + fn set_traffic_light_inset>(&self, position: P); + /// Put the window in a state which indicates a file save is required. + /// + /// + fn set_is_document_edited(&self, edited: bool); + + /// Get the window's edit state + fn is_document_edited(&self) -> bool; + + /// Sets whether the system can automatically organize windows into tabs. + /// + /// + fn set_allows_automatic_window_tabbing(&self, enabled: bool); + + /// Returns whether the system can automatically organize windows into tabs. + fn allows_automatic_window_tabbing(&self) -> bool; + + /// Group windows together by using the same tabbing identifier. + /// + /// + fn set_tabbing_identifier(&self, identifier: &str); + + /// Returns the window's tabbing identifier. + fn tabbing_identifier(&self) -> String; + + /// The content view consumes the full size of the window. + /// + /// + fn set_fullsize_content_view(&self, fullsize: bool); + + /// A Boolean value that indicates whether the title bar draws its background. + /// + /// + fn set_titlebar_transparent(&self, transparent: bool); + + /// Sets the badge label on the taskbar + fn set_badge_label(&self, label: Option); +} + +impl WindowExtMacOS for Window { + #[inline] + fn ns_window(&self) -> *mut c_void { + self.window.ns_window() + } + + #[inline] + fn ns_view(&self) -> *mut c_void { + self.window.ns_view() + } + + #[inline] + fn simple_fullscreen(&self) -> bool { + self.window.simple_fullscreen() + } + + #[inline] + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { + self.window.set_simple_fullscreen(fullscreen) + } + + #[inline] + fn has_shadow(&self) -> bool { + self.window.has_shadow() + } + + #[inline] + fn set_has_shadow(&self, has_shadow: bool) { + self.window.set_has_shadow(has_shadow) + } + + #[inline] + fn set_traffic_light_inset>(&self, position: P) { + self.window.set_traffic_light_inset(position) + } + + #[inline] + fn set_is_document_edited(&self, edited: bool) { + self.window.set_is_document_edited(edited) + } + + #[inline] + fn is_document_edited(&self) -> bool { + self.window.is_document_edited() + } + + #[inline] + fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + self.window.set_allows_automatic_window_tabbing(enabled) + } + + #[inline] + fn allows_automatic_window_tabbing(&self) -> bool { + self.window.allows_automatic_window_tabbing() + } + + #[inline] + fn set_tabbing_identifier(&self, identifier: &str) { + self.window.set_tabbing_identifier(identifier) + } + + #[inline] + fn tabbing_identifier(&self) -> String { + self.window.tabbing_identifier() + } + + #[inline] + fn set_fullsize_content_view(&self, fullsize: bool) { + self.window.set_fullsize_content_view(fullsize); + } + + #[inline] + fn set_titlebar_transparent(&self, transparent: bool) { + self.window.set_titlebar_transparent(transparent); + } + + #[inline] + fn set_badge_label(&self, label: Option) { + self.window.set_badge_label(label); + } +} + +/// Corresponds to `NSApplicationActivationPolicy`. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum ActivationPolicy { + /// Corresponds to `NSApplicationActivationPolicyRegular`. + #[default] + Regular, + /// Corresponds to `NSApplicationActivationPolicyAccessory`. + Accessory, + /// Corresponds to `NSApplicationActivationPolicyProhibited`. + Prohibited, +} + +/// Additional methods on `WindowBuilder` that are specific to MacOS. +/// +/// **Note:** Properties dealing with the titlebar will be overwritten by the `with_decorations` method +/// on the base `WindowBuilder`: +/// +/// - `with_titlebar_transparent` +/// - `with_title_hidden` +/// - `with_titlebar_hidden` +/// - `with_titlebar_buttons_hidden` +/// - `with_fullsize_content_view` +pub trait WindowBuilderExtMacOS { + /// Sets a parent to the window to be created. + fn with_parent_window(self, parent: *mut c_void) -> WindowBuilder; + /// Enables click-and-drag behavior for the entire window, not just the titlebar. + fn with_movable_by_window_background(self, movable_by_window_background: bool) -> WindowBuilder; + /// Makes the titlebar transparent and allows the content to appear behind it. + fn with_titlebar_transparent(self, titlebar_transparent: bool) -> WindowBuilder; + /// Hides the window title. + fn with_title_hidden(self, title_hidden: bool) -> WindowBuilder; + /// Hides the window titlebar. + fn with_titlebar_hidden(self, titlebar_hidden: bool) -> WindowBuilder; + /// Hides the window titlebar buttons. + fn with_titlebar_buttons_hidden(self, titlebar_buttons_hidden: bool) -> WindowBuilder; + /// Makes the window content appear behind the titlebar. + fn with_fullsize_content_view(self, fullsize_content_view: bool) -> WindowBuilder; + /// Build window with `resizeIncrements` property. Values must not be 0. + fn with_resize_increments(self, increments: LogicalSize) -> WindowBuilder; + fn with_disallow_hidpi(self, disallow_hidpi: bool) -> WindowBuilder; + /// Sets whether or not the window has shadow. + fn with_has_shadow(self, has_shadow: bool) -> WindowBuilder; + /// Sets the traffic light position to (x, y) relative to the upper left corner + fn with_traffic_light_inset>(self, inset: P) -> WindowBuilder; + /// Sets whether the system can automatically organize windows into tabs. + fn with_automatic_window_tabbing(self, automatic_tabbing: bool) -> WindowBuilder; + /// Defines the window [tabbing identifier]. + /// + /// [tabbing identifier]: + fn with_tabbing_identifier(self, identifier: &str) -> WindowBuilder; +} + +impl WindowBuilderExtMacOS for WindowBuilder { + #[inline] + fn with_parent_window(mut self, parent: *mut c_void) -> WindowBuilder { + self.platform_specific.parent = Parent::ChildOf(parent); + self + } + + #[inline] + fn with_movable_by_window_background( + mut self, + movable_by_window_background: bool, + ) -> WindowBuilder { + self.platform_specific.movable_by_window_background = movable_by_window_background; + self + } + + #[inline] + fn with_titlebar_transparent(mut self, titlebar_transparent: bool) -> WindowBuilder { + self.platform_specific.titlebar_transparent = titlebar_transparent; + self + } + + #[inline] + fn with_titlebar_hidden(mut self, titlebar_hidden: bool) -> WindowBuilder { + self.platform_specific.titlebar_hidden = titlebar_hidden; + self + } + + #[inline] + fn with_titlebar_buttons_hidden(mut self, titlebar_buttons_hidden: bool) -> WindowBuilder { + self.platform_specific.titlebar_buttons_hidden = titlebar_buttons_hidden; + self + } + + #[inline] + fn with_title_hidden(mut self, title_hidden: bool) -> WindowBuilder { + self.platform_specific.title_hidden = title_hidden; + self + } + + #[inline] + fn with_fullsize_content_view(mut self, fullsize_content_view: bool) -> WindowBuilder { + self.platform_specific.fullsize_content_view = fullsize_content_view; + self + } + + #[inline] + fn with_resize_increments(mut self, increments: LogicalSize) -> WindowBuilder { + self.platform_specific.resize_increments = Some(increments); + self + } + + #[inline] + fn with_disallow_hidpi(mut self, disallow_hidpi: bool) -> WindowBuilder { + self.platform_specific.disallow_hidpi = disallow_hidpi; + self + } + + #[inline] + fn with_has_shadow(mut self, has_shadow: bool) -> WindowBuilder { + self.platform_specific.has_shadow = has_shadow; + self + } + + #[inline] + fn with_traffic_light_inset>(mut self, inset: P) -> WindowBuilder { + self.platform_specific.traffic_light_inset = Some(inset.into()); + self + } + + #[inline] + fn with_automatic_window_tabbing(mut self, automatic_tabbing: bool) -> WindowBuilder { + self.platform_specific.automatic_tabbing = automatic_tabbing; + self + } + + #[inline] + fn with_tabbing_identifier(mut self, tabbing_identifier: &str) -> WindowBuilder { + self + .platform_specific + .tabbing_identifier + .replace(tabbing_identifier.into()); + self + } +} + +pub trait EventLoopExtMacOS { + /// Sets the activation policy for the application. It is set to + /// `NSApplicationActivationPolicyRegular` by default. + /// + /// This function only takes effect if it's called before calling + /// [`run`](crate::event_loop::EventLoop::run) or + /// [`run_return`](crate::platform::run_return::EventLoopExtRunReturn::run_return). + /// To set the activation policy after that, use + /// [`EventLoopWindowTargetExtMacOS::set_activation_policy_at_runtime`](crate::platform::macos::EventLoopWindowTargetExtMacOS::set_activation_policy_at_runtime). + fn set_activation_policy(&mut self, activation_policy: ActivationPolicy); + + /// Sets the visibility of the application in the dock. + /// + /// This function only takes effect if it's called before calling + /// [`run`](crate::event_loop::EventLoop::run) or + /// [`run_return`](crate::platform::run_return::EventLoopExtRunReturn::run_return). + fn set_dock_visibility(&mut self, visible: bool); + + /// Used to prevent the application from automatically activating when launched if + /// another application is already active + /// + /// The default behavior is to ignore other applications and activate when launched. + /// + /// This function only takes effect if it's called before calling + /// [`run`](crate::event_loop::EventLoop::run) or + /// [`run_return`](crate::platform::run_return::EventLoopExtRunReturn::run_return) + fn set_activate_ignoring_other_apps(&mut self, ignore: bool); +} + +impl EventLoopExtMacOS for EventLoop { + #[inline] + fn set_activation_policy(&mut self, activation_policy: ActivationPolicy) { + unsafe { + get_aux_state_mut(&**self.event_loop.delegate).activation_policy = activation_policy; + } + } + + #[inline] + fn set_dock_visibility(&mut self, visible: bool) { + unsafe { + get_aux_state_mut(&**self.event_loop.delegate).dock_visibility = visible; + } + } + + #[inline] + fn set_activate_ignoring_other_apps(&mut self, ignore: bool) { + unsafe { + get_aux_state_mut(&**self.event_loop.delegate).activate_ignoring_other_apps = ignore; + } + } +} + +/// Additional methods on `MonitorHandle` that are specific to MacOS. +pub trait MonitorHandleExtMacOS { + /// Returns the identifier of the monitor for Cocoa. + fn native_id(&self) -> u32; + /// Returns a pointer to the NSScreen representing this monitor. + fn ns_screen(&self) -> Option<*mut c_void>; +} + +impl MonitorHandleExtMacOS for MonitorHandle { + #[inline] + fn native_id(&self) -> u32 { + self.inner.native_identifier() + } + + fn ns_screen(&self) -> Option<*mut c_void> { + self + .inner + .ns_screen() + .map(|s| objc2::rc::Retained::into_raw(s) as *mut c_void) + } +} + +/// Additional methods on `EventLoopWindowTarget` that are specific to macOS. +pub trait EventLoopWindowTargetExtMacOS { + /// Hide the entire application. In most applications this is typically triggered with Command-H. + fn hide_application(&self); + /// Show the entire application. + fn show_application(&self); + /// Hide the other applications. In most applications this is typically triggered with Command+Option-H. + fn hide_other_applications(&self); + /// Sets the activation policy for the application. It is set to + /// `NSApplicationActivationPolicyRegular` by default. + /// + /// To set the activation policy before the app starts running, see + /// [`EventLoopExtMacOS::set_activation_policy`](crate::platform::macos::EventLoopExtMacOS::set_activation_policy). + fn set_activation_policy_at_runtime(&self, activation_policy: ActivationPolicy); + + /// Sets the visibility of the application in the dock. + /// + /// To set the dock visibility before the app starts running, see + /// [`EventLoopExtMacOS::set_dock_visibility`](crate::platform::macos::EventLoopExtMacOS::set_dock_visibility). + fn set_dock_visibility(&self, visible: bool); + + /// Sets the badge label on macos dock + fn set_badge_label(&self, label: Option); +} + +impl EventLoopWindowTargetExtMacOS for EventLoopWindowTarget { + fn hide_application(&self) { + // TODO: Safety. + let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() }; + objc2_app_kit::NSApplication::sharedApplication(mtm).hide(None) + } + + fn show_application(&self) { + // TODO: Safety. + let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() }; + unsafe { objc2_app_kit::NSApplication::sharedApplication(mtm).unhide(None) } + } + + fn hide_other_applications(&self) { + // TODO: Safety. + let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() }; + objc2_app_kit::NSApplication::sharedApplication(mtm).hideOtherApplications(None) + } + + fn set_activation_policy_at_runtime(&self, activation_policy: ActivationPolicy) { + use objc2_app_kit::NSApplicationActivationPolicy; + + let ns_activation_policy = match activation_policy { + ActivationPolicy::Regular => NSApplicationActivationPolicy::Regular, + ActivationPolicy::Accessory => NSApplicationActivationPolicy::Accessory, + ActivationPolicy::Prohibited => NSApplicationActivationPolicy::Prohibited, + }; + + // TODO: Safety. + let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() }; + objc2_app_kit::NSApplication::sharedApplication(mtm).setActivationPolicy(ns_activation_policy); + } + + fn set_dock_visibility(&self, visible: bool) { + let Some(Ok(delegate)) = (unsafe { + // TODO: Safety. + let mtm = objc2_foundation::MainThreadMarker::new_unchecked(); + objc2_app_kit::NSApplication::sharedApplication(mtm) + .delegate() + .map(|delegate| delegate.downcast::()) + }) else { + return; + }; + set_dock_visibility(&delegate, visible); + } + + fn set_badge_label(&self, label: Option) { + set_badge_label(label); + } +} diff --git a/vendor/tao/src/platform/mod.rs b/vendor/tao/src/platform/mod.rs new file mode 100644 index 0000000000..2ca20b36d4 --- /dev/null +++ b/vendor/tao/src/platform/mod.rs @@ -0,0 +1,28 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +//! Contains traits with platform-specific methods in them. +//! +//! Contains the follow OS-specific modules: +//! +//! - `android` +//! - `ios` +//! - `macos` +//! - `unix` +//! - `linux` +//! - `windows` +//! +//! And the following platform-specific module: +//! +//! - `run_return` (available on `windows`, `unix`, `macos`, and `android`) +//! +//! However only the module corresponding to the platform you're compiling to will be available. + +pub mod android; +pub mod ios; +pub mod linux; +pub mod macos; +pub mod run_return; +pub mod unix; +pub mod windows; diff --git a/vendor/tao/src/platform/run_return.rs b/vendor/tao/src/platform/run_return.rs new file mode 100644 index 0000000000..e86ae1f6ab --- /dev/null +++ b/vendor/tao/src/platform/run_return.rs @@ -0,0 +1,50 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(not(target_os = "ios"))] + +use crate::{ + event::Event, + event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, +}; + +/// Additional methods on `EventLoop` to return control flow to the caller. +pub trait EventLoopExtRunReturn { + /// A type provided by the user that can be passed through `Event::UserEvent`. + type UserEvent; + + /// Initializes the `tao` event loop. + /// + /// Unlike `run`, this function accepts non-`'static` (i.e. non-`move`) closures and returns + /// control flow to the caller when `control_flow` is set to `ControlFlow::Exit`. + /// + /// # Caveats + /// Despite its appearance at first glance, this is *not* a perfect replacement for + /// `poll_events`. For example, this function will not return on Windows or macOS while a + /// window is getting resized, resulting in all application logic outside of the + /// `event_handler` closure not running until the resize operation ends. Other OS operations + /// may also result in such freezes. This behavior is caused by fundamental limitations in the + /// underlying OS APIs, which cannot be hidden by `tao` without severe stability repercussions. + /// + /// You are strongly encouraged to use `run`, unless the use of this is absolutely necessary. + /// + /// ## Platform-specific + /// + /// - **Unix-alikes** (**X11** or **Wayland**): This function returns `1` upon disconnection from + /// the display server. + fn run_return(&mut self, event_handler: F) -> i32 + where + F: FnMut(Event<'_, Self::UserEvent>, &EventLoopWindowTarget, &mut ControlFlow); +} + +impl EventLoopExtRunReturn for EventLoop { + type UserEvent = T; + + fn run_return(&mut self, event_handler: F) -> i32 + where + F: FnMut(Event<'_, Self::UserEvent>, &EventLoopWindowTarget, &mut ControlFlow), + { + self.event_loop.run_return(event_handler) + } +} diff --git a/vendor/tao/src/platform/unix.rs b/vendor/tao/src/platform/unix.rs new file mode 100644 index 0000000000..88584869e4 --- /dev/null +++ b/vendor/tao/src/platform/unix.rs @@ -0,0 +1,308 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] + +#[cfg(feature = "x11")] +use std::{os::raw::c_int, sync::Arc}; + +// XConnection utilities +#[doc(hidden)] +#[cfg(feature = "x11")] +pub use crate::platform_impl::x11; + +#[cfg(feature = "x11")] +use crate::platform_impl::x11::xdisplay::XError; +pub use crate::platform_impl::EventLoop as UnixEventLoop; +use crate::{ + error::{ExternalError, OsError}, + event_loop::{EventLoopBuilder, EventLoopWindowTarget}, + monitor::MonitorHandle, + platform_impl::{Parent, Window as UnixWindow}, + window::{Window, WindowBuilder}, +}; + +#[cfg(feature = "x11")] +use self::x11::xdisplay::XConnection; + +/// Additional methods on `EventLoop` that are specific to Unix. +pub trait EventLoopBuilderExtUnix { + /// Whether to allow the event loop to be created off of the main thread. + /// + /// By default, the window is only allowed to be created on the main + /// thread, to make platform compatibility easier. + /// + /// # `Window` caveats + /// + /// Note that any `Window` created on the new thread will be destroyed when the thread + /// terminates. Attempting to use a `Window` after its parent thread terminates has + /// unspecified, although explicitly not undefined, behavior. + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self; + + /// Set the gtk application id. + /// + /// If no application ID is given then some features (most notably application uniqueness) will be disabled. + fn with_app_id>(&mut self, id: S) -> &mut Self; +} + +impl EventLoopBuilderExtUnix for EventLoopBuilder { + #[inline] + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self { + self.platform_specific.any_thread = any_thread; + self + } + + fn with_app_id>(&mut self, id: S) -> &mut Self { + self.platform_specific.app_id = Some(id.into()); + self + } +} + +/// Additional methods on `Window` that are specific to Unix. +pub trait WindowExtUnix { + /// Create a new Tao window from an existing GTK window. Generally you should use + /// the non-Linux `WindowBuilder`, this is for those who need lower level window access + /// and know what they're doing. + fn new_from_gtk_window( + event_loop_window_target: &EventLoopWindowTarget, + window: gtk::ApplicationWindow, + ) -> Result; + + /// Returns the `gtk::ApplicatonWindow` from gtk crate that is used by this window. + fn gtk_window(&self) -> >k::ApplicationWindow; + + /// Returns the vertical `gtk::Box` that is added by default as the sole child of this window. + /// Returns `None` if the default vertical `gtk::Box` creation was disabled by [`WindowBuilderExtUnix::with_default_vbox`]. + fn default_vbox(&self) -> Option<>k::Box>; + + /// Whether to show the window icon in the taskbar or not. + fn set_skip_taskbar(&self, skip: bool) -> Result<(), ExternalError>; + + fn set_badge_count(&self, count: Option, desktop_filename: Option); +} + +impl WindowExtUnix for Window { + fn gtk_window(&self) -> >k::ApplicationWindow { + &self.window.window + } + + fn default_vbox(&self) -> Option<>k::Box> { + self.window.default_vbox.as_ref() + } + + fn set_skip_taskbar(&self, skip: bool) -> Result<(), ExternalError> { + self.window.set_skip_taskbar(skip) + } + + fn new_from_gtk_window( + event_loop_window_target: &EventLoopWindowTarget, + window: gtk::ApplicationWindow, + ) -> Result { + let window = UnixWindow::new_from_gtk_window(&event_loop_window_target.p, window)?; + Ok(Window { window: window }) + } + + fn set_badge_count(&self, count: Option, desktop_filename: Option) { + self.window.set_badge_count(count, desktop_filename); + } +} + +pub trait WindowBuilderExtUnix { + /// Whether to create the window icon with the taskbar icon or not. + fn with_skip_taskbar(self, skip: bool) -> WindowBuilder; + /// Set this window as a transient dialog for `parent` + /// + fn with_transient_for(self, parent: &impl gtk::glib::IsA) -> WindowBuilder; + + /// Whether to enable or disable the internal draw for transparent window. + /// + /// When tranparent attribute is enabled, we will call `connect_draw` and draw a transparent background. + /// For anyone who wants to draw the background themselves, set this to `false`. + /// Default is `true`. + fn with_transparent_draw(self, draw: bool) -> WindowBuilder; + + /// Whether to enable or disable the double buffered rendering of the window. + /// + /// Default is `true`. + fn with_double_buffered(self, double_buffered: bool) -> WindowBuilder; + + /// Whether to enable the rgba visual for the window. + /// + /// Default is `false` but is always `true` if [`WindowAttributes::transparent`](crate::window::WindowAttributes::transparent) is `true` + fn with_rgba_visual(self, rgba_visual: bool) -> WindowBuilder; + + /// Wether to set this window as app paintable + /// + /// + /// + /// Default is `false` but is always `true` if [`WindowAttributes::transparent`](crate::window::WindowAttributes::transparent) is `true` + fn with_app_paintable(self, app_paintable: bool) -> WindowBuilder; + + /// Whether to set cursor moved event. Cursor event is suited for native GUI frameworks and + /// games. But it can block gtk's own pipeline occasionally. Turn this off can help Gtk looks + /// smoother. + /// + /// Default is `true`. + fn with_cursor_moved_event(self, cursor_moved: bool) -> WindowBuilder; + + /// Whether to create a vertical `gtk::Box` and add it as the sole child of this window. + /// Created by default. + fn with_default_vbox(self, add: bool) -> WindowBuilder; +} + +impl WindowBuilderExtUnix for WindowBuilder { + fn with_skip_taskbar(mut self, skip: bool) -> WindowBuilder { + self.platform_specific.skip_taskbar = skip; + self + } + + fn with_transient_for(mut self, parent: &impl gtk::glib::IsA) -> WindowBuilder { + use gtk::glib::Cast; + self.platform_specific.parent = Parent::ChildOf(parent.clone().upcast()); + self + } + + fn with_transparent_draw(mut self, draw: bool) -> WindowBuilder { + self.platform_specific.auto_transparent = draw; + self + } + + fn with_double_buffered(mut self, double_buffered: bool) -> WindowBuilder { + self.platform_specific.double_buffered = double_buffered; + self + } + + fn with_rgba_visual(mut self, rgba_visual: bool) -> WindowBuilder { + self.platform_specific.rgba_visual = rgba_visual; + self + } + + fn with_app_paintable(mut self, app_paintable: bool) -> WindowBuilder { + self.platform_specific.app_paintable = app_paintable; + self + } + + fn with_cursor_moved_event(mut self, cursor_moved: bool) -> WindowBuilder { + self.platform_specific.cursor_moved = cursor_moved; + self + } + + fn with_default_vbox(mut self, add: bool) -> WindowBuilder { + self.platform_specific.default_vbox = add; + self + } +} + +/// Additional methods on `EventLoopWindowTarget` that are specific to Unix. +pub trait EventLoopWindowTargetExtUnix { + /// True if the `EventLoopWindowTarget` uses Wayland. + fn is_wayland(&self) -> bool; + + /// True if the `EventLoopWindowTarget` uses X11. + #[cfg(feature = "x11")] + fn is_x11(&self) -> bool; + + #[cfg(feature = "x11")] + fn xlib_xconnection(&self) -> Option>; + + // /// Returns a pointer to the `wl_display` object of wayland that is used by this + // /// `EventLoopWindowTarget`. + // /// + // /// Returns `None` if the `EventLoop` doesn't use wayland (if it uses xlib for example). + // /// + // /// The pointer will become invalid when the winit `EventLoop` is destroyed. + // fn wayland_display(&self) -> Option<*mut raw::c_void>; + + /// Returns the gtk application for this event loop. + fn gtk_app(&self) -> >k::Application; + + /// Sets the badge count on the taskbar + fn set_badge_count(&self, count: Option, desktop_filename: Option); +} + +impl EventLoopWindowTargetExtUnix for EventLoopWindowTarget { + #[inline] + fn is_wayland(&self) -> bool { + self.p.is_wayland() + } + + #[cfg(feature = "x11")] + #[inline] + fn is_x11(&self) -> bool { + !self.p.is_wayland() + } + + #[cfg(feature = "x11")] + #[inline] + fn xlib_xconnection(&self) -> Option> { + if self.is_x11() { + if let Ok(xconn) = XConnection::new(Some(x_error_callback)) { + Some(Arc::new(xconn)) + } else { + None + } + } else { + None + } + } + + // #[inline] + // fn wayland_display(&self) -> Option<*mut raw::c_void> { + // match self.p { + // LinuxEventLoopWindowTarget::Wayland(ref p) => { + // Some(p.display().get_display_ptr() as *mut _) + // } + // #[cfg(feature = "x11")] + // _ => None, + // } + // } + + #[inline] + fn gtk_app(&self) -> >k::Application { + &self.p.app + } + + #[inline] + fn set_badge_count(&self, count: Option, desktop_filename: Option) { + self.p.set_badge_count(count, desktop_filename); + } +} + +#[cfg(feature = "x11")] +unsafe extern "C" fn x_error_callback( + _display: *mut x11::ffi::Display, + event: *mut x11::ffi::XErrorEvent, +) -> c_int { + let error = XError { + // TODO get the error text as description + description: String::new(), + error_code: (*event).error_code, + request_code: (*event).request_code, + minor_code: (*event).minor_code, + }; + + error!("X11 error: {:#?}", error); + + // Fun fact: this return value is completely ignored. + 0 +} + +/// Additional methods on `MonitorHandle` that are specific to Unix. +pub trait MonitorHandleExtUnix { + /// Returns the gdk handle of the monitor. + fn gdk_monitor(&self) -> >k::gdk::Monitor; +} + +impl MonitorHandleExtUnix for MonitorHandle { + #[inline] + fn gdk_monitor(&self) -> >k::gdk::Monitor { + &self.inner.monitor + } +} diff --git a/vendor/tao/src/platform/windows.rs b/vendor/tao/src/platform/windows.rs new file mode 100644 index 0000000000..a248b04b3a --- /dev/null +++ b/vendor/tao/src/platform/windows.rs @@ -0,0 +1,449 @@ +// Copyright 2014-2021 The tao contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(target_os = "windows")] + +use std::path::Path; + +use crate::{ + dpi::PhysicalSize, + error::ExternalError, + event::DeviceId, + event_loop::EventLoopBuilder, + monitor::MonitorHandle, + platform_impl::{Parent, WinIcon}, + window::{BadIcon, Icon, Theme, Window, WindowBuilder}, +}; +use windows::Win32::UI::Input::KeyboardAndMouse::*; + +pub type HWND = isize; +pub type HMENU = isize; + +/// Additional methods on `EventLoop` that are specific to Windows. +pub trait EventLoopBuilderExtWindows { + /// Whether to allow the event loop to be created off of the main thread. + /// + /// By default, the window is only allowed to be created on the main + /// thread, to make platform compatibility easier. + /// + /// # `Window` caveats + /// + /// Note that any `Window` created on the new thread will be destroyed when the thread + /// terminates. Attempting to use a `Window` after its parent thread terminates has + /// unspecified, although explicitly not undefined, behavior. + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self; + + /// Whether to enable process-wide DPI awareness. + /// + /// By default, `tao` will attempt to enable process-wide DPI awareness. If + /// that's undesirable, you can disable it with this function. + /// + /// # Example + /// + /// Disable process-wide DPI awareness. + /// + /// ``` + /// use tao::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "windows")] + /// use tao::platform::windows::EventLoopBuilderExtWindows; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "windows")] + /// builder.with_dpi_aware(false); + /// # if false { // We can't test this part + /// let event_loop = builder.build(); + /// # } + /// ``` + fn with_dpi_aware(&mut self, dpi_aware: bool) -> &mut Self; + + /// A callback to be executed before dispatching a win32 message to the window procedure. + /// Return true to disable tao's internal message dispatching. + /// + /// # Example + /// + /// ``` + /// # use windows::Win32::UI::WindowsAndMessaging::{ACCEL, CreateAcceleratorTableW, TranslateAcceleratorW, DispatchMessageW, TranslateMessage, MSG}; + /// use tao::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "windows")] + /// use tao::platform::windows::EventLoopBuilderExtWindows; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "windows")] + /// builder.with_msg_hook(|msg|{ + /// let msg = msg as *const MSG; + /// # let accels_: Vec = Vec::new(); + /// # let accels = accels_.as_slice(); + /// let translated = unsafe { + /// TranslateAcceleratorW( + /// (*msg).hwnd, + /// CreateAcceleratorTableW(accels).unwrap(), + /// msg, + /// ) == 1 + /// }; + /// translated + /// }); + /// ``` + fn with_msg_hook(&mut self, callback: F) -> &mut Self + where + F: FnMut(*const std::ffi::c_void) -> bool + 'static; + + /// Forces a theme or uses the system settings if `None` was provided. + /// + /// This will only affect some controls like context menus. + /// + /// ## Note + /// + /// Since this setting is app-wide, using [`WindowBuilder::with_theme`] + /// will not change the affected controls for that specific window, + /// so it is recommended to always use the same theme used for this app-wide setting + /// or use `None` so it automatically uses the theme of this method + /// or falls back to the system preference. + fn with_theme(&mut self, theme: Option) -> &mut Self; +} + +impl EventLoopBuilderExtWindows for EventLoopBuilder { + #[inline] + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self { + self.platform_specific.any_thread = any_thread; + self + } + + #[inline] + fn with_dpi_aware(&mut self, dpi_aware: bool) -> &mut Self { + self.platform_specific.dpi_aware = dpi_aware; + self + } + + #[inline] + fn with_msg_hook(&mut self, callback: F) -> &mut Self + where + F: FnMut(*const std::ffi::c_void) -> bool + 'static, + { + self.platform_specific.msg_hook = Some(Box::new(callback)); + self + } + + #[inline] + fn with_theme(&mut self, theme: Option) -> &mut Self { + self.platform_specific.preferred_theme = theme; + self + } +} + +/// Additional methods on `Window` that are specific to Windows. +pub trait WindowExtWindows { + /// Returns the HINSTANCE of the window + fn hinstance(&self) -> isize; + /// Returns the native handle that is used by this window. + /// + /// The pointer will become invalid when the native window was destroyed. + fn hwnd(&self) -> isize; + + /// Enables or disables mouse and keyboard input to the specified window. + /// + /// A window must be enabled before it can be activated. + /// If an application has create a modal dialog box by disabling its owner window + /// (as described in [`WindowBuilderExtWindows::with_owner_window`]), the application must enable + /// the owner window before destroying the dialog box. + /// Otherwise, another window will receive the keyboard focus and be activated. + /// + /// If a child window is disabled, it is ignored when the system tries to determine which + /// window should receive mouse messages. + /// + /// For more information, see + /// and + fn set_enable(&self, enabled: bool); + + /// This sets `ICON_BIG`. A good ceiling here is 256x256. + fn set_taskbar_icon(&self, taskbar_icon: Option); + + /// This sets the overlay icon + fn set_overlay_icon(&self, icon: Option<&Icon>); + + /// Returns the current window theme. + fn theme(&self) -> Theme; + + /// Reset the dead key state of the keyboard. + /// + /// This is useful when a dead key is bound to trigger an action. Then + /// this function can be called to reset the dead key state so that + /// follow-up text input won't be affected by the dead key. + fn reset_dead_keys(&self); + + /// Starts the resizing drag from given edge + fn begin_resize_drag(&self, edge: isize, button: u32, x: i32, y: i32); + + /// Whether to show the window icon in the taskbar or not. + fn set_skip_taskbar(&self, skip: bool) -> Result<(), ExternalError>; + + /// Shows or hides the background drop shadow for undecorated windows. + /// + /// Enabling the shadow causes a thin 1px line to appear on the top of the window. + fn set_undecorated_shadow(&self, shadow: bool); + + /// Returns whether this window has shadow for undecorated windows. + fn has_undecorated_shadow(&self) -> bool; + + /// Sets right-to-left layout. + /// + /// Enabling this mainly flips the orientation of menus and title bar buttons + fn set_rtl(&self, rtl: bool); +} + +impl WindowExtWindows for Window { + #[inline] + fn hinstance(&self) -> isize { + self.window.hinstance().0 as _ + } + + #[inline] + fn hwnd(&self) -> isize { + self.window.hwnd().0 as _ + } + + #[inline] + fn set_enable(&self, enabled: bool) { + unsafe { + let _ = EnableWindow(self.window.hwnd(), enabled); + } + } + + #[inline] + fn set_taskbar_icon(&self, taskbar_icon: Option) { + self.window.set_taskbar_icon(taskbar_icon) + } + + #[inline] + fn theme(&self) -> Theme { + self.window.theme() + } + + #[inline] + fn reset_dead_keys(&self) { + self.window.reset_dead_keys(); + } + + #[inline] + fn begin_resize_drag(&self, edge: isize, button: u32, x: i32, y: i32) { + self.window.begin_resize_drag(edge, button, x, y) + } + + #[inline] + fn set_skip_taskbar(&self, skip: bool) -> Result<(), ExternalError> { + self.window.set_skip_taskbar(skip) + } + + #[inline] + fn set_undecorated_shadow(&self, shadow: bool) { + self.window.set_undecorated_shadow(shadow) + } + + #[inline] + fn has_undecorated_shadow(&self) -> bool { + self.window.has_undecorated_shadow() + } + + #[inline] + fn set_rtl(&self, rtl: bool) { + self.window.set_rtl(rtl) + } + + #[inline] + fn set_overlay_icon(&self, icon: Option<&Icon>) { + self.window.set_overlay_icon(icon); + } +} + +/// Additional methods on `WindowBuilder` that are specific to Windows. +pub trait WindowBuilderExtWindows { + /// Sets a parent to the window to be created. + /// + /// A child window has the WS_CHILD style and is confined to the client area of its parent window. + /// + /// For more information, see + fn with_parent_window(self, parent: HWND) -> WindowBuilder; + + /// Set an owner to the window to be created. Can be used to create a dialog box, for example. + /// Can be used in combination with [`WindowExtWindows::set_enable(false)`](WindowExtWindows::set_enable) + /// on the owner window to create a modal dialog box. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + fn with_owner_window(self, parent: HWND) -> WindowBuilder; + + /// Sets a menu on the window to be created. + /// + /// Parent and menu are mutually exclusive; a child window cannot have a menu! + /// + /// The menu must have been manually created beforehand with [`windows::Win32::UI::WindowsAndMessaging::CreateMenu`] + /// or similar. + /// + /// Note: Dark mode cannot be supported for win32 menus, it's simply not possible to change how the menus look. + /// If you use this, it is recommended that you combine it with `with_theme(Some(Theme::Light))` to avoid a jarring effect. + fn with_menu(self, menu: HMENU) -> WindowBuilder; + + /// This sets `ICON_BIG`. A good ceiling here is 256x256. + fn with_taskbar_icon(self, taskbar_icon: Option) -> WindowBuilder; + + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + fn with_no_redirection_bitmap(self, flag: bool) -> WindowBuilder; + + /// Enables or disables drag and drop support (enabled by default). Will interfere with other crates + /// that use multi-threaded COM API (`CoInitializeEx` with `COINIT_MULTITHREADED` instead of + /// `COINIT_APARTMENTTHREADED`) on the same thread. Note that tao may still attempt to initialize + /// COM API regardless of this option. Currently only fullscreen mode does that, but there may be more in the future. + /// If you need COM API with `COINIT_MULTITHREADED` you must initialize it before calling any tao functions. + /// See for more information. + fn with_drag_and_drop(self, flag: bool) -> WindowBuilder; + + /// Whether to create the window icon with the taskbar icon or not. + fn with_skip_taskbar(self, skip: bool) -> WindowBuilder; + + /// Customize the window class name. + fn with_window_classname>(self, classname: S) -> WindowBuilder; + + /// Shows or hides the background drop shadow for undecorated windows. + /// + /// The shadow is hidden by default. + /// Enabling the shadow causes a thin 1px line to appear on the top of the window. + fn with_undecorated_shadow(self, shadow: bool) -> WindowBuilder; + + /// Sets right-to-left layout. + fn with_rtl(self, rtl: bool) -> WindowBuilder; +} + +impl WindowBuilderExtWindows for WindowBuilder { + #[inline] + fn with_parent_window(mut self, parent: HWND) -> WindowBuilder { + self.platform_specific.parent = Parent::ChildOf(windows::Win32::Foundation::HWND(parent as _)); + self + } + + #[inline] + fn with_owner_window(mut self, parent: HWND) -> WindowBuilder { + self.platform_specific.parent = Parent::OwnedBy(windows::Win32::Foundation::HWND(parent as _)); + self + } + + #[inline] + fn with_menu(mut self, menu: HMENU) -> WindowBuilder { + self.platform_specific.menu = Some(windows::Win32::UI::WindowsAndMessaging::HMENU(menu as _)); + self + } + + #[inline] + fn with_taskbar_icon(mut self, taskbar_icon: Option) -> WindowBuilder { + self.platform_specific.taskbar_icon = taskbar_icon; + self + } + + #[inline] + fn with_no_redirection_bitmap(mut self, flag: bool) -> WindowBuilder { + self.platform_specific.no_redirection_bitmap = flag; + self + } + + #[inline] + fn with_drag_and_drop(mut self, flag: bool) -> WindowBuilder { + self.platform_specific.drag_and_drop = flag; + self + } + + #[inline] + fn with_skip_taskbar(mut self, skip: bool) -> WindowBuilder { + self.platform_specific.skip_taskbar = skip; + self + } + + #[inline] + fn with_window_classname>(mut self, classname: S) -> WindowBuilder { + self.platform_specific.window_classname = classname.into(); + self + } + + #[inline] + fn with_undecorated_shadow(mut self, shadow: bool) -> WindowBuilder { + self.platform_specific.decoration_shadow = shadow; + self + } + + #[inline] + fn with_rtl(mut self, rtl: bool) -> WindowBuilder { + self.platform_specific.rtl = rtl; + self + } +} + +/// Additional methods on `MonitorHandle` that are specific to Windows. +pub trait MonitorHandleExtWindows { + /// Returns the name of the monitor adapter specific to the Win32 API. + fn native_id(&self) -> String; + + /// Returns the handle of the monitor - `HMONITOR`. + fn hmonitor(&self) -> isize; +} + +impl MonitorHandleExtWindows for MonitorHandle { + #[inline] + fn native_id(&self) -> String { + self.inner.native_identifier() + } + + #[inline] + fn hmonitor(&self) -> isize { + self.inner.hmonitor().0 as _ + } +} + +/// Additional methods on `DeviceId` that are specific to Windows. +pub trait DeviceIdExtWindows { + /// Returns an identifier that persistently refers to this specific device. + /// + /// Will return `None` if the device is no longer available. + fn persistent_identifier(&self) -> Option; +} + +impl DeviceIdExtWindows for DeviceId { + #[inline] + fn persistent_identifier(&self) -> Option { + self.0.persistent_identifier() + } +} + +/// Additional methods on `Icon` that are specific to Windows. +pub trait IconExtWindows: Sized { + /// Create an icon from a file path. + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + fn from_path>(path: P, size: Option>) -> Result; + + /// Create an icon from a resource embedded in this executable or library. + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + fn from_resource(ordinal: u16, size: Option>) -> Result; +} + +impl IconExtWindows for Icon { + fn from_path>(path: P, size: Option>) -> Result { + let win_icon = WinIcon::from_path(path, size)?; + Ok(Icon { inner: win_icon }) + } + + fn from_resource(ordinal: u16, size: Option>) -> Result { + let win_icon = WinIcon::from_resource(ordinal, size)?; + Ok(Icon { inner: win_icon }) + } +} diff --git a/vendor/tao/src/platform_impl/android/mod.rs b/vendor/tao/src/platform_impl/android/mod.rs new file mode 100644 index 0000000000..45d02f1c20 --- /dev/null +++ b/vendor/tao/src/platform_impl/android/mod.rs @@ -0,0 +1,1262 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(target_os = "android")] +use crate::{ + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, + error, event, + event_loop::{self, ControlFlow}, + keyboard::{Key, KeyCode, KeyLocation, NativeKeyCode}, + monitor, + window::{self, ResizeDirection, Theme, WindowSizeConstraints}, +}; +use crossbeam_channel::{Receiver, Sender}; +use ndk::{ + configuration::Configuration, + event::{InputEvent, KeyAction, MotionAction}, + looper::{ForeignLooper, Poll, ThreadLooper}, +}; +use std::{ + collections::VecDeque, + sync::RwLock, + time::{Duration, Instant}, +}; + +pub mod ndk_glue; +use ndk_glue::{Event, Rect}; + +lazy_static! { + static ref CONFIG: RwLock = RwLock::new(Configuration::new()); +} + +enum EventSource { + Callback, + InputQueue, + User, +} + +fn poll(poll: Poll) -> Option { + match poll { + Poll::Event { ident, .. } => match ident { + ndk_glue::NDK_GLUE_LOOPER_EVENT_PIPE_IDENT => Some(EventSource::Callback), + ndk_glue::NDK_GLUE_LOOPER_INPUT_QUEUE_IDENT => Some(EventSource::InputQueue), + _ => unreachable!(), + }, + Poll::Timeout => None, + Poll::Wake => Some(EventSource::User), + Poll::Callback => unreachable!(), + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra {} + +pub struct EventLoop { + window_target: event_loop::EventLoopWindowTarget, + receiver: Receiver, + sender_to_clone: Sender, + first_event: Option, + start_cause: event::StartCause, + looper: ThreadLooper, + running: bool, +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +macro_rules! call_event_handler { + ( $event_handler:expr, $window_target:expr, $cf:expr, $event:expr ) => {{ + if let ControlFlow::ExitWithCode(code) = $cf { + $event_handler($event, $window_target, &mut ControlFlow::ExitWithCode(code)); + } else { + $event_handler($event, $window_target, &mut $cf); + } + }}; +} + +impl EventLoop { + pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Self { + let (sender, receiver) = crossbeam_channel::unbounded(); + + Self { + window_target: event_loop::EventLoopWindowTarget { + p: EventLoopWindowTarget { + _marker: std::marker::PhantomData, + }, + _marker: std::marker::PhantomData, + }, + sender_to_clone: sender, + receiver, + first_event: None, + start_cause: event::StartCause::Init, + looper: ThreadLooper::for_thread().unwrap(), + running: false, + } + } + + pub fn run(mut self, event_handler: F) -> ! + where + F: + 'static + FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + { + let exit_code = self.run_return(event_handler); + ::std::process::exit(exit_code); + } + + pub fn run_return(&mut self, mut event_handler: F) -> i32 + where + F: FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + { + let mut control_flow = ControlFlow::default(); + + 'event_loop: loop { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::NewEvents(self.start_cause) + ); + + let mut redraw = false; + let mut resized = false; + + match self.first_event.take() { + Some(EventSource::Callback) => match ndk_glue::poll_events().unwrap() { + Event::Resume => { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::Resumed + ); + } + Event::WindowResized => resized = true, + Event::WindowRedrawNeeded => redraw = true, + Event::Pause => { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::Suspended + ); + } + Event::Stop => self.running = false, + Event::Start => self.running = true, + Event::ConfigChanged => { + // #[allow(deprecated)] // TODO: use ndk-context instead + // let am = ndk_glue::native_activity().asset_manager(); + // let config = Configuration::from_asset_manager(&am); + // let old_scale_factor = MonitorHandle.scale_factor(); + // *CONFIG.write().unwrap() = config; + // let scale_factor = MonitorHandle.scale_factor(); + // if (scale_factor - old_scale_factor).abs() < f64::EPSILON { + // let mut size = MonitorHandle.size(); + // let event = event::Event::WindowEvent { + // window_id: window::WindowId(WindowId), + // event: event::WindowEvent::ScaleFactorChanged { + // new_inner_size: &mut size, + // scale_factor, + // }, + // }; + // call_event_handler!(event_handler, self.window_target(), control_flow, event); + // } + } + Event::WindowHasFocus => { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Focused(true), + } + ); + } + Event::WindowLostFocus => { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Focused(false), + } + ); + } + _ => {} + }, + Some(EventSource::InputQueue) => { + if let Some(input_queue) = ndk_glue::input_queue().as_ref() { + while let Ok(Some(event)) = input_queue.event() { + if let Some(event) = input_queue.pre_dispatch(event) { + let mut handled = true; + let window_id = window::WindowId(WindowId); + let device_id = event::DeviceId(DeviceId); + match &event { + InputEvent::MotionEvent(motion_event) => { + let phase = match motion_event.action() { + MotionAction::Down | MotionAction::PointerDown => { + Some(event::TouchPhase::Started) + } + MotionAction::Up | MotionAction::PointerUp => Some(event::TouchPhase::Ended), + MotionAction::Move => Some(event::TouchPhase::Moved), + MotionAction::Cancel => Some(event::TouchPhase::Cancelled), + _ => { + handled = false; + None // TODO mouse events + } + }; + if let Some(phase) = phase { + let pointers: Box>> = match phase + { + event::TouchPhase::Started | event::TouchPhase::Ended => { + Box::new(std::iter::once( + motion_event.pointer_at_index(motion_event.pointer_index()), + )) + } + event::TouchPhase::Moved | event::TouchPhase::Cancelled => { + Box::new(motion_event.pointers()) + } + }; + + for pointer in pointers { + let location = PhysicalPosition { + x: pointer.x() as _, + y: pointer.y() as _, + }; + let event = event::Event::WindowEvent { + window_id, + event: event::WindowEvent::Touch(event::Touch { + device_id, + phase, + location, + id: pointer.pointer_id() as u64, + force: None, + }), + }; + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event + ); + } + } + } + InputEvent::KeyEvent(key) => { + let state = match key.action() { + KeyAction::Down => event::ElementState::Pressed, + KeyAction::Up => event::ElementState::Released, + _ => event::ElementState::Released, + }; + + let keycode = key.key_code(); + let native = NativeKeyCode::Android(keycode.into()); + let physical_key = KeyCode::Unidentified(native); + let logical_key = keycode_to_logical(keycode, native); + // TODO: maybe use getUnicodeChar to get the logical key + + let event = event::Event::WindowEvent { + window_id, + event: event::WindowEvent::KeyboardInput { + device_id, + event: event::KeyEvent { + state, + physical_key, + logical_key, + location: keycode_to_location(keycode), + repeat: key.repeat_count() > 0, + text: None, + platform_specific: KeyEventExtra {}, + }, + is_synthetic: false, + }, + }; + call_event_handler!(event_handler, self.window_target(), control_flow, event); + } + _ => {} + }; + input_queue.finish_event(event, handled); + } + } + } + } + Some(EventSource::User) => { + while let Ok(event) = self.receiver.try_recv() { + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::UserEvent(event) + ); + } + } + None => {} + } + + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::MainEventsCleared + ); + + if resized && self.running { + let size = MonitorHandle.size(); + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Resized(size), + }; + call_event_handler!(event_handler, self.window_target(), control_flow, event); + } + + if redraw && self.running { + let event = event::Event::RedrawRequested(window::WindowId(WindowId)); + call_event_handler!(event_handler, self.window_target(), control_flow, event); + } + + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event::Event::RedrawEventsCleared + ); + + match control_flow { + ControlFlow::ExitWithCode(code) => { + self.first_event = poll( + self + .looper + .poll_once_timeout(Duration::from_millis(0)) + .unwrap(), + ); + self.start_cause = event::StartCause::WaitCancelled { + start: Instant::now(), + requested_resume: None, + }; + break 'event_loop code; + } + ControlFlow::Poll => { + self.first_event = poll( + self + .looper + .poll_all_timeout(Duration::from_millis(0)) + .unwrap(), + ); + self.start_cause = event::StartCause::Poll; + } + ControlFlow::Wait => { + self.first_event = poll(self.looper.poll_all().unwrap()); + self.start_cause = event::StartCause::WaitCancelled { + start: Instant::now(), + requested_resume: None, + } + } + ControlFlow::WaitUntil(instant) => { + let start = Instant::now(); + let duration = if instant <= start { + Duration::default() + } else { + instant - start + }; + self.first_event = poll(self.looper.poll_all_timeout(duration).unwrap()); + self.start_cause = if self.first_event.is_some() { + event::StartCause::WaitCancelled { + start, + requested_resume: Some(instant), + } + } else { + event::StartCause::ResumeTimeReached { + start, + requested_resume: instant, + } + } + } + } + } + } + + pub fn window_target(&self) -> &event_loop::EventLoopWindowTarget { + &self.window_target + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + queue: self.sender_to_clone.clone(), + looper: ForeignLooper::for_thread().expect("called from event loop thread"), + } + } +} + +pub struct EventLoopProxy { + queue: Sender, + looper: ForeignLooper, +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), event_loop::EventLoopClosed> { + _ = self.queue.try_send(event); + self.looper.wake(); + Ok(()) + } +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { + queue: self.queue.clone(), + looper: self.looper.clone(), + } + } +} + +#[derive(Clone)] +pub struct EventLoopWindowTarget { + _marker: std::marker::PhantomData, +} + +impl EventLoopWindowTarget { + pub fn primary_monitor(&self) -> Option { + Some(monitor::MonitorHandle { + inner: MonitorHandle, + }) + } + + #[inline] + pub fn monitor_from_point(&self, _x: f64, _y: f64) -> Option { + warn!("`Window::monitor_from_point` is ignored on Android"); + return None; + } + + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle); + v + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Android(rwh_05::AndroidDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06(&self) -> Result { + Ok(rwh_06::RawDisplayHandle::Android( + rwh_06::AndroidDisplayHandle::new(), + )) + } + + pub fn cursor_position(&self) -> Result, error::ExternalError> { + debug!("`EventLoopWindowTarget::cursor_position` is ignored on Android"); + Ok((0, 0).into()) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct WindowId; + +impl WindowId { + pub fn dummy() -> Self { + WindowId + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct DeviceId; + +impl DeviceId { + pub fn dummy() -> Self { + DeviceId + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct PlatformSpecificWindowBuilderAttributes; + +pub struct Window; + +impl Window { + pub fn new( + _el: &EventLoopWindowTarget, + _window_attrs: window::WindowAttributes, + _: PlatformSpecificWindowBuilderAttributes, + ) -> Result { + // FIXME this ignores requested window attributes + Ok(Self) + } + + pub fn id(&self) -> WindowId { + WindowId + } + + pub fn primary_monitor(&self) -> Option { + Some(monitor::MonitorHandle { + inner: MonitorHandle, + }) + } + + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle); + v + } + + #[inline] + pub fn monitor_from_point(&self, _x: f64, _y: f64) -> Option { + warn!("`Window::monitor_from_point` is ignored on Android"); + None + } + + pub fn current_monitor(&self) -> Option { + Some(monitor::MonitorHandle { + inner: MonitorHandle, + }) + } + + pub fn scale_factor(&self) -> f64 { + MonitorHandle.scale_factor() + } + + pub fn request_redraw(&self) { + // TODO + } + + pub fn inner_position(&self) -> Result, error::NotSupportedError> { + Err(error::NotSupportedError::new()) + } + + pub fn outer_position(&self) -> Result, error::NotSupportedError> { + Err(error::NotSupportedError::new()) + } + + pub fn set_outer_position(&self, _position: Position) { + // no effect + } + + pub fn inner_size(&self) -> PhysicalSize { + self.outer_size() + } + + pub fn set_inner_size(&self, _size: Size) { + warn!("Cannot set window size on Android"); + } + + pub fn outer_size(&self) -> PhysicalSize { + MonitorHandle.size() + } + + pub fn set_min_inner_size(&self, _: Option) {} + pub fn set_max_inner_size(&self, _: Option) {} + pub fn set_inner_size_constraints(&self, _: WindowSizeConstraints) {} + + pub fn set_title(&self, _title: &str) {} + pub fn title(&self) -> String { + String::new() + } + + pub fn set_visible(&self, _visibility: bool) {} + + pub fn set_focus(&self) { + //FIXME: implementation goes here + warn!("set_focus not yet implemented on Android"); + } + + pub fn set_focusable(&self, _focusable: bool) { + warn!("set_focusable not yet implemented on Android"); + } + + pub fn is_focused(&self) -> bool { + log::warn!("`Window::is_focused` is ignored on Android"); + false + } + + pub fn is_always_on_top(&self) -> bool { + log::warn!("`Window::is_always_on_top` is ignored on Android"); + false + } + + pub fn set_resizable(&self, _resizeable: bool) { + warn!("`Window::set_resizable` is ignored on Android") + } + + pub fn set_minimizable(&self, _minimizable: bool) { + warn!("`Window::set_minimizable` is ignored on Android") + } + + pub fn set_maximizable(&self, _maximizable: bool) { + warn!("`Window::set_maximizable` is ignored on Android") + } + + pub fn set_closable(&self, _closable: bool) { + warn!("`Window::set_closable` is ignored on Android") + } + + pub fn set_minimized(&self, _minimized: bool) {} + + pub fn set_maximized(&self, _maximized: bool) {} + + pub fn is_maximized(&self) -> bool { + false + } + + pub fn is_minimized(&self) -> bool { + false + } + + pub fn is_visible(&self) -> bool { + log::warn!("`Window::is_visible` is ignored on Android"); + false + } + + pub fn is_resizable(&self) -> bool { + warn!("`Window::is_resizable` is ignored on Android"); + false + } + + pub fn is_minimizable(&self) -> bool { + warn!("`Window::is_minimizable` is ignored on Android"); + false + } + + pub fn is_maximizable(&self) -> bool { + warn!("`Window::is_maximizable` is ignored on Android"); + false + } + + pub fn is_closable(&self) -> bool { + warn!("`Window::is_closable` is ignored on Android"); + false + } + + pub fn is_decorated(&self) -> bool { + warn!("`Window::is_decorated` is ignored on Android"); + false + } + + pub fn set_fullscreen(&self, _monitor: Option) { + warn!("Cannot set fullscreen on Android"); + } + + pub fn fullscreen(&self) -> Option { + None + } + + pub fn set_decorations(&self, _decorations: bool) {} + + pub fn set_always_on_bottom(&self, _always_on_bottom: bool) {} + + pub fn set_always_on_top(&self, _always_on_top: bool) {} + + pub fn set_window_icon(&self, _window_icon: Option) {} + + pub fn set_ime_position(&self, _position: Position) {} + + pub fn request_user_attention(&self, _request_type: Option) {} + + pub fn set_cursor_icon(&self, _: window::CursorIcon) {} + + pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + + pub fn set_cursor_grab(&self, _: bool) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + + pub fn set_cursor_visible(&self, _: bool) {} + + pub fn drag_window(&self) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + + pub fn drag_resize_window( + &self, + _direction: ResizeDirection, + ) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + + pub fn set_background_color(&self, _color: Option) {} + + pub fn set_ignore_cursor_events(&self, _ignore: bool) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + + pub fn cursor_position(&self) -> Result, error::ExternalError> { + debug!("`Window::cursor_position` is ignored on Android"); + Ok((0, 0).into()) + } + + #[cfg(feature = "rwh_04")] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + // TODO: Use main activity instead? + let mut handle = rwh_04::AndroidNdkHandle::empty(); + if let Some(w) = ndk_glue::window_manager().as_ref() { + handle.a_native_window = w.as_obj().as_raw() as *mut _; + } else { + panic!("Cannot get the native window, it's null and will always be null before Event::Resumed and after Event::Suspended. Make sure you only call this function between those events."); + }; + rwh_04::RawWindowHandle::AndroidNdk(handle) + } + + #[cfg(feature = "rwh_05")] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + // TODO: Use main activity instead? + let mut handle = rwh_05::AndroidNdkWindowHandle::empty(); + if let Some(w) = ndk_glue::window_manager().as_ref() { + handle.a_native_window = w.as_obj().as_raw() as *mut _; + } else { + panic!("Cannot get the native window, it's null and will always be null before Event::Resumed and after Event::Suspended. Make sure you only call this function between those events."); + }; + rwh_05::RawWindowHandle::AndroidNdk(handle) + } + + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Android(rwh_05::AndroidDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + pub fn raw_window_handle_rwh_06(&self) -> Result { + // TODO: Use main activity instead? + if let Some(w) = ndk_glue::window_manager().as_ref() { + let native_window = + unsafe { std::ptr::NonNull::new_unchecked(w.as_obj().as_raw() as *mut _) }; + // native_window shuldn't be null + let handle = rwh_06::AndroidNdkWindowHandle::new(native_window); + Ok(rwh_06::RawWindowHandle::AndroidNdk(handle)) + } else { + Err(rwh_06::HandleError::Unavailable) + } + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06(&self) -> Result { + Ok(rwh_06::RawDisplayHandle::Android( + rwh_06::AndroidDisplayHandle::new(), + )) + } + pub fn config(&self) -> Configuration { + CONFIG.read().unwrap().clone() + } + + pub fn content_rect(&self) -> Rect { + ndk_glue::content_rect() + } + + pub fn theme(&self) -> Theme { + Theme::Light + } +} + +#[derive(Default, Clone, Debug)] +pub struct OsError; + +use std::fmt::{self, Display, Formatter}; +impl Display for OsError { + fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> { + write!(fmt, "Android OS Error") + } +} + +pub(crate) use crate::icon::NoIcon as PlatformIcon; + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct MonitorHandle; + +impl MonitorHandle { + pub fn name(&self) -> Option { + Some("Android Device".to_owned()) + } + + pub fn size(&self) -> PhysicalSize { + // TODO decide how to get JNIENV + if let Some(w) = ndk_glue::window_manager().as_ref() { + let ctx = ndk_context::android_context(); + let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap(); + let mut env = vm.attach_current_thread().unwrap(); + let window_manager = w.as_obj(); + let metrics = env + .call_method( + window_manager, + "getCurrentWindowMetrics", + "()Landroid/view/WindowMetrics;", + &[], + ) + .unwrap() + .l() + .unwrap(); + let rect = env + .call_method(&metrics, "getBounds", "()Landroid/graphics/Rect;", &[]) + .unwrap() + .l() + .unwrap(); + let width = env + .call_method(&rect, "width", "()I", &[]) + .unwrap() + .i() + .unwrap(); + let height = env + .call_method(&rect, "height", "()I", &[]) + .unwrap() + .i() + .unwrap(); + PhysicalSize::new(width as u32, height as u32) + } else { + PhysicalSize::new(0, 0) + } + } + + pub fn position(&self) -> PhysicalPosition { + (0, 0).into() + } + + pub fn scale_factor(&self) -> f64 { + let config = CONFIG.read().unwrap(); + config + .density() + .map(|dpi| dpi as f64 / 160.0) + .unwrap_or(1.0) + } + + pub fn video_modes(&self) -> impl Iterator { + let size = self.size().into(); + let mut v = Vec::new(); + // FIXME this is not the real refresh rate + // (it is guarunteed to support 32 bit color though) + v.push(monitor::VideoMode { + video_mode: VideoMode { + size, + bit_depth: 32, + refresh_rate: 60, + monitor: self.clone(), + }, + }); + v.into_iter() + } +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VideoMode { + size: (u32, u32), + bit_depth: u16, + refresh_rate: u16, + monitor: MonitorHandle, +} + +impl VideoMode { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + pub fn monitor(&self) -> monitor::MonitorHandle { + monitor::MonitorHandle { + inner: self.monitor.clone(), + } + } +} + +fn keycode_to_logical(keycode: ndk::event::Keycode, native: NativeKeyCode) -> Key<'static> { + use ndk::event::Keycode::*; + + // The android `Keycode` is sort-of layout dependent. More specifically + // if I press the Z key using a US layout, then I get KEYCODE_Z, + // but if I press the same key after switching to a HUN layout, I get + // KEYCODE_Y. + // + // To prevents us from using this value to determine the `physical_key` + // (also know as winit's `KeyCode`) + // + // Unfortunately the documentation says that the scancode values + // "are not reliable and vary from device to device". Which seems to mean + // that there's no way to reliably get the physical_key on android. + + match keycode { + Unknown => Key::Unidentified(native), + + // Can be added on demand + SoftLeft => Key::Unidentified(native), + SoftRight => Key::Unidentified(native), + + // Using `BrowserHome` instead of `GoHome` according to + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + Home => Key::BrowserHome, + Back => Key::BrowserBack, + Call => Key::Call, + Endcall => Key::EndCall, + + //------------------------------------------------------------------------------- + // Reporting unidentified, because the specific character is layout dependent. + // (I'm not sure though) + Keycode0 => Key::Unidentified(native), + Keycode1 => Key::Unidentified(native), + Keycode2 => Key::Unidentified(native), + Keycode3 => Key::Unidentified(native), + Keycode4 => Key::Unidentified(native), + Keycode5 => Key::Unidentified(native), + Keycode6 => Key::Unidentified(native), + Keycode7 => Key::Unidentified(native), + Keycode8 => Key::Unidentified(native), + Keycode9 => Key::Unidentified(native), + Star => Key::Unidentified(native), + Pound => Key::Unidentified(native), + A => Key::Unidentified(native), + B => Key::Unidentified(native), + C => Key::Unidentified(native), + D => Key::Unidentified(native), + E => Key::Unidentified(native), + F => Key::Unidentified(native), + G => Key::Unidentified(native), + H => Key::Unidentified(native), + I => Key::Unidentified(native), + J => Key::Unidentified(native), + K => Key::Unidentified(native), + L => Key::Unidentified(native), + M => Key::Unidentified(native), + N => Key::Unidentified(native), + O => Key::Unidentified(native), + P => Key::Unidentified(native), + Q => Key::Unidentified(native), + R => Key::Unidentified(native), + S => Key::Unidentified(native), + T => Key::Unidentified(native), + U => Key::Unidentified(native), + V => Key::Unidentified(native), + W => Key::Unidentified(native), + X => Key::Unidentified(native), + Y => Key::Unidentified(native), + Z => Key::Unidentified(native), + Comma => Key::Unidentified(native), + Period => Key::Unidentified(native), + Grave => Key::Unidentified(native), + Minus => Key::Unidentified(native), + Equals => Key::Unidentified(native), + LeftBracket => Key::Unidentified(native), + RightBracket => Key::Unidentified(native), + Backslash => Key::Unidentified(native), + Semicolon => Key::Unidentified(native), + Apostrophe => Key::Unidentified(native), + Slash => Key::Unidentified(native), + At => Key::Unidentified(native), + Plus => Key::Unidentified(native), + //------------------------------------------------------------------------------- + DpadUp => Key::ArrowUp, + DpadDown => Key::ArrowDown, + DpadLeft => Key::ArrowLeft, + DpadRight => Key::ArrowRight, + DpadCenter => Key::Enter, + + VolumeUp => Key::AudioVolumeUp, + VolumeDown => Key::AudioVolumeDown, + Power => Key::Power, + Camera => Key::Camera, + Clear => Key::Clear, + + AltLeft => Key::Alt, + AltRight => Key::Alt, + ShiftLeft => Key::Shift, + ShiftRight => Key::Shift, + Tab => Key::Tab, + Space => Key::Space, + Sym => Key::Symbol, + Explorer => Key::LaunchWebBrowser, + Envelope => Key::LaunchMail, + Enter => Key::Enter, + Del => Key::Backspace, + + // According to https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_NUM + Num => Key::Alt, + + Headsethook => Key::HeadsetHook, + Focus => Key::CameraFocus, + + Menu => Key::Unidentified(native), + + Notification => Key::Notification, + Search => Key::BrowserSearch, + MediaPlayPause => Key::MediaPlayPause, + MediaStop => Key::MediaStop, + MediaNext => Key::MediaTrackNext, + MediaPrevious => Key::MediaTrackPrevious, + MediaRewind => Key::MediaRewind, + MediaFastForward => Key::MediaFastForward, + Mute => Key::MicrophoneVolumeMute, + PageUp => Key::PageUp, + PageDown => Key::PageDown, + Pictsymbols => Key::Unidentified(native), + SwitchCharset => Key::Unidentified(native), + + // ----------------------------------------------------------------- + // Gamepad events should be exposed through a separate API, not + // keyboard events + ButtonA => Key::Unidentified(native), + ButtonB => Key::Unidentified(native), + ButtonC => Key::Unidentified(native), + ButtonX => Key::Unidentified(native), + ButtonY => Key::Unidentified(native), + ButtonZ => Key::Unidentified(native), + ButtonL1 => Key::Unidentified(native), + ButtonR1 => Key::Unidentified(native), + ButtonL2 => Key::Unidentified(native), + ButtonR2 => Key::Unidentified(native), + ButtonThumbl => Key::Unidentified(native), + ButtonThumbr => Key::Unidentified(native), + ButtonStart => Key::Unidentified(native), + ButtonSelect => Key::Unidentified(native), + ButtonMode => Key::Unidentified(native), + // ----------------------------------------------------------------- + Escape => Key::Escape, + ForwardDel => Key::Delete, + CtrlLeft => Key::Control, + CtrlRight => Key::Control, + CapsLock => Key::CapsLock, + ScrollLock => Key::ScrollLock, + MetaLeft => Key::Super, + MetaRight => Key::Super, + Function => Key::Fn, + Sysrq => Key::PrintScreen, + Break => Key::Pause, + MoveHome => Key::Home, + MoveEnd => Key::End, + Insert => Key::Insert, + Forward => Key::BrowserForward, + MediaPlay => Key::MediaPlay, + MediaPause => Key::MediaPause, + MediaClose => Key::MediaClose, + MediaEject => Key::Eject, + MediaRecord => Key::MediaRecord, + F1 => Key::F1, + F2 => Key::F2, + F3 => Key::F3, + F4 => Key::F4, + F5 => Key::F5, + F6 => Key::F6, + F7 => Key::F7, + F8 => Key::F8, + F9 => Key::F9, + F10 => Key::F10, + F11 => Key::F11, + F12 => Key::F12, + NumLock => Key::NumLock, + Numpad0 => Key::Unidentified(native), + Numpad1 => Key::Unidentified(native), + Numpad2 => Key::Unidentified(native), + Numpad3 => Key::Unidentified(native), + Numpad4 => Key::Unidentified(native), + Numpad5 => Key::Unidentified(native), + Numpad6 => Key::Unidentified(native), + Numpad7 => Key::Unidentified(native), + Numpad8 => Key::Unidentified(native), + Numpad9 => Key::Unidentified(native), + NumpadDivide => Key::Unidentified(native), + NumpadMultiply => Key::Unidentified(native), + NumpadSubtract => Key::Unidentified(native), + NumpadAdd => Key::Unidentified(native), + NumpadDot => Key::Unidentified(native), + NumpadComma => Key::Unidentified(native), + NumpadEnter => Key::Unidentified(native), + NumpadEquals => Key::Unidentified(native), + NumpadLeftParen => Key::Unidentified(native), + NumpadRightParen => Key::Unidentified(native), + + VolumeMute => Key::AudioVolumeMute, + Info => Key::Info, + ChannelUp => Key::ChannelUp, + ChannelDown => Key::ChannelDown, + ZoomIn => Key::ZoomIn, + ZoomOut => Key::ZoomOut, + Tv => Key::TV, + Window => Key::Unidentified(native), + Guide => Key::Guide, + Dvr => Key::DVR, + Bookmark => Key::BrowserFavorites, + Captions => Key::ClosedCaptionToggle, + Settings => Key::Settings, + TvPower => Key::TVPower, + TvInput => Key::TVInput, + StbPower => Key::STBPower, + StbInput => Key::STBInput, + AvrPower => Key::AVRPower, + AvrInput => Key::AVRInput, + ProgRed => Key::ColorF0Red, + ProgGreen => Key::ColorF1Green, + ProgYellow => Key::ColorF2Yellow, + ProgBlue => Key::ColorF3Blue, + AppSwitch => Key::AppSwitch, + Button1 => Key::Unidentified(native), + Button2 => Key::Unidentified(native), + Button3 => Key::Unidentified(native), + Button4 => Key::Unidentified(native), + Button5 => Key::Unidentified(native), + Button6 => Key::Unidentified(native), + Button7 => Key::Unidentified(native), + Button8 => Key::Unidentified(native), + Button9 => Key::Unidentified(native), + Button10 => Key::Unidentified(native), + Button11 => Key::Unidentified(native), + Button12 => Key::Unidentified(native), + Button13 => Key::Unidentified(native), + Button14 => Key::Unidentified(native), + Button15 => Key::Unidentified(native), + Button16 => Key::Unidentified(native), + LanguageSwitch => Key::GroupNext, + MannerMode => Key::MannerMode, + Keycode3dMode => Key::TV3DMode, + Contacts => Key::LaunchContacts, + Calendar => Key::LaunchCalendar, + Music => Key::LaunchMusicPlayer, + Calculator => Key::LaunchApplication2, + ZenkakuHankaku => Key::ZenkakuHankaku, + Eisu => Key::Eisu, + Muhenkan => Key::NonConvert, + Henkan => Key::Convert, + KatakanaHiragana => Key::HiraganaKatakana, + Yen => Key::Unidentified(native), + Ro => Key::Unidentified(native), + Kana => Key::KanjiMode, + Assist => Key::Unidentified(native), + BrightnessDown => Key::BrightnessDown, + BrightnessUp => Key::BrightnessUp, + MediaAudioTrack => Key::MediaAudioTrack, + Sleep => Key::Standby, + Wakeup => Key::WakeUp, + Pairing => Key::Pairing, + MediaTopMenu => Key::MediaTopMenu, + Keycode11 => Key::Unidentified(native), + Keycode12 => Key::Unidentified(native), + LastChannel => Key::MediaLast, + TvDataService => Key::TVDataService, + VoiceAssist => Key::VoiceDial, + TvRadioService => Key::TVRadioService, + TvTeletext => Key::Teletext, + TvNumberEntry => Key::TVNumberEntry, + TvTerrestrialAnalog => Key::TVTerrestrialAnalog, + TvTerrestrialDigital => Key::TVTerrestrialDigital, + TvSatellite => Key::TVSatellite, + TvSatelliteBs => Key::TVSatelliteBS, + TvSatelliteCs => Key::TVSatelliteCS, + TvSatelliteService => Key::TVSatelliteToggle, + TvNetwork => Key::TVNetwork, + TvAntennaCable => Key::TVAntennaCable, + TvInputHdmi1 => Key::TVInputHDMI1, + TvInputHdmi2 => Key::TVInputHDMI2, + TvInputHdmi3 => Key::TVInputHDMI3, + TvInputHdmi4 => Key::TVInputHDMI4, + TvInputComposite1 => Key::TVInputComposite1, + TvInputComposite2 => Key::TVInputComposite2, + TvInputComponent1 => Key::TVInputComponent1, + TvInputComponent2 => Key::TVInputComponent2, + TvInputVga1 => Key::TVInputVGA1, + TvAudioDescription => Key::TVAudioDescription, + TvAudioDescriptionMixUp => Key::TVAudioDescriptionMixUp, + TvAudioDescriptionMixDown => Key::TVAudioDescriptionMixDown, + TvZoomMode => Key::ZoomToggle, + TvContentsMenu => Key::TVContentsMenu, + TvMediaContextMenu => Key::TVMediaContext, + TvTimerProgramming => Key::TVTimer, + Help => Key::Help, + NavigatePrevious => Key::NavigatePrevious, + NavigateNext => Key::NavigateNext, + NavigateIn => Key::NavigateIn, + NavigateOut => Key::NavigateOut, + StemPrimary => Key::Unidentified(native), + Stem1 => Key::Unidentified(native), + Stem2 => Key::Unidentified(native), + Stem3 => Key::Unidentified(native), + DpadUpLeft => Key::Unidentified(native), + DpadDownLeft => Key::Unidentified(native), + DpadUpRight => Key::Unidentified(native), + DpadDownRight => Key::Unidentified(native), + MediaSkipForward => Key::MediaSkipForward, + MediaSkipBackward => Key::MediaSkipBackward, + MediaStepForward => Key::MediaStepForward, + MediaStepBackward => Key::MediaStepBackward, + SoftSleep => Key::Unidentified(native), + Cut => Key::Cut, + Copy => Key::Copy, + Paste => Key::Paste, + SystemNavigationUp => Key::Unidentified(native), + SystemNavigationDown => Key::Unidentified(native), + SystemNavigationLeft => Key::Unidentified(native), + SystemNavigationRight => Key::Unidentified(native), + AllApps => Key::Unidentified(native), + Refresh => Key::BrowserRefresh, + ThumbsUp => Key::Unidentified(native), + ThumbsDown => Key::Unidentified(native), + ProfileSwitch => Key::Unidentified(native), + _ => Key::Unidentified(native), + } +} + +fn keycode_to_location(keycode: ndk::event::Keycode) -> KeyLocation { + use ndk::event::Keycode::*; + + match keycode { + AltLeft => KeyLocation::Left, + AltRight => KeyLocation::Right, + ShiftLeft => KeyLocation::Left, + ShiftRight => KeyLocation::Right, + + // According to https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_NUM + Num => KeyLocation::Left, + + CtrlLeft => KeyLocation::Left, + CtrlRight => KeyLocation::Right, + MetaLeft => KeyLocation::Left, + MetaRight => KeyLocation::Right, + + NumLock => KeyLocation::Numpad, + Numpad0 => KeyLocation::Numpad, + Numpad1 => KeyLocation::Numpad, + Numpad2 => KeyLocation::Numpad, + Numpad3 => KeyLocation::Numpad, + Numpad4 => KeyLocation::Numpad, + Numpad5 => KeyLocation::Numpad, + Numpad6 => KeyLocation::Numpad, + Numpad7 => KeyLocation::Numpad, + Numpad8 => KeyLocation::Numpad, + Numpad9 => KeyLocation::Numpad, + NumpadDivide => KeyLocation::Numpad, + NumpadMultiply => KeyLocation::Numpad, + NumpadSubtract => KeyLocation::Numpad, + NumpadAdd => KeyLocation::Numpad, + NumpadDot => KeyLocation::Numpad, + NumpadComma => KeyLocation::Numpad, + NumpadEnter => KeyLocation::Numpad, + NumpadEquals => KeyLocation::Numpad, + NumpadLeftParen => KeyLocation::Numpad, + NumpadRightParen => KeyLocation::Numpad, + + _ => KeyLocation::Standard, + } +} + +// FIXME: Implement android +pub fn keycode_to_scancode(_code: KeyCode) -> Option { + None +} + +pub fn keycode_from_scancode(_scancode: u32) -> KeyCode { + KeyCode::Unidentified(NativeKeyCode::Unidentified) +} diff --git a/vendor/tao/src/platform_impl/android/ndk_glue.rs b/vendor/tao/src/platform_impl/android/ndk_glue.rs new file mode 100644 index 0000000000..a09b4b2915 --- /dev/null +++ b/vendor/tao/src/platform_impl/android/ndk_glue.rs @@ -0,0 +1,378 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +pub use jni::{ + self, + objects::{GlobalRef, JClass, JMap, JObject, JString}, + sys::jobject, + JNIEnv, +}; +use log::Level; +pub use ndk; +use ndk::{ + input_queue::InputQueue, + looper::{FdEvent, ForeignLooper, ThreadLooper}, +}; +use once_cell::sync::{Lazy, OnceCell}; +use std::{ + cell::RefCell, + ffi::{CStr, CString}, + fs::File, + io::{BufRead, BufReader}, + os::unix::prelude::*, + sync::{Arc, Condvar, Mutex, RwLock, RwLockReadGuard}, + thread, +}; + +/// Android pacakge name that could be used to reference classes +/// in the android project. +pub static PACKAGE: OnceCell<&str> = OnceCell::new(); + +/// Generate JNI compilant functions that are necessary for +/// building android apps with tao. +/// +/// Arguments in order: +/// 1. android app domain name in reverse snake_case as an ident (for ex: com_example) +/// 2. android package anme (for ex: wryapp) +/// 3. the android activity that has external linking for the following functions and calls them: +/// - `private external fun create(activity: WryActivity)`` +/// - `private external fun start()` +/// - `private external fun resume()` +/// - `private external fun pause()` +/// - `private external fun stop()` +/// - `private external fun save()` +/// - `private external fun destroy()` +/// - `private external fun memory()` +/// - `private external fun focus(focus: Boolean)` +/// 4. a one time setup function that will be ran once after tao has created its event loop in the `create` function above. +/// 5. the main entry point of your android application. +#[rustfmt::skip] +#[macro_export] +macro_rules! android_binding { + ($domain:ident, $package:ident, $activity:ident, $setup:path, $main:ident) => { + ::tao::android_binding!($domain, $package, $activity, $setup, $main, ::tao) + }; + ($domain:ident, $package:ident, $activity:ident, $setup:path, $main:ident, $tao:path) => {{ + // NOTE: be careful when changing how this use statement is written + use $tao::{platform::android::prelude::android_fn, platform::android::prelude::*}; + fn _____tao_store_package_name__() { + PACKAGE.get_or_init(move || generate_package_name!($domain, $package)); + } + + android_fn!( + $domain, + $package, + $activity, + create, + [JObject], + __VOID__, + [$setup, $main], + _____tao_store_package_name__, + ); + android_fn!($domain, $package, $activity, start, [JObject]); + android_fn!($domain, $package, $activity, stop, [JObject]); + android_fn!($domain, $package, $activity, resume, [JObject]); + android_fn!($domain, $package, $activity, pause, [JObject]); + android_fn!($domain, $package, $activity, save, [JObject]); + android_fn!($domain, $package, $activity, destroy, [JObject]); + android_fn!($domain, $package, $activity, memory, [JObject]); + android_fn!($domain, $package, $activity, focus, [i32]); + }}; +} + +/// `ndk-glue` macros register the reading end of an event pipe with the +/// main [`ThreadLooper`] under this `ident`. +/// When returned from [`ThreadLooper::poll_*`](ThreadLooper::poll_once) +/// an event can be retrieved from [`poll_events()`]. +pub const NDK_GLUE_LOOPER_EVENT_PIPE_IDENT: i32 = 0; + +/// The [`InputQueue`] received from Android is registered with the main +/// [`ThreadLooper`] under this `ident`. +/// When returned from [`ThreadLooper::poll_*`](ThreadLooper::poll_once) +/// an event can be retrieved from [`input_queue()`]. +pub const NDK_GLUE_LOOPER_INPUT_QUEUE_IDENT: i32 = 1; + +pub fn android_log(level: Level, tag: &CStr, msg: &CStr) { + let prio = match level { + Level::Error => ndk_sys::android_LogPriority::ANDROID_LOG_ERROR, + Level::Warn => ndk_sys::android_LogPriority::ANDROID_LOG_WARN, + Level::Info => ndk_sys::android_LogPriority::ANDROID_LOG_INFO, + Level::Debug => ndk_sys::android_LogPriority::ANDROID_LOG_DEBUG, + Level::Trace => ndk_sys::android_LogPriority::ANDROID_LOG_VERBOSE, + }; + unsafe { + ndk_sys::__android_log_write(prio.0 as _, tag.as_ptr(), msg.as_ptr()); + } +} + +pub(crate) struct StaticCell(RefCell); + +unsafe impl Send for StaticCell {} +unsafe impl Sync for StaticCell {} + +impl std::ops::Deref for StaticCell { + type Target = RefCell; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +static WINDOW_MANAGER: StaticCell> = StaticCell(RefCell::new(None)); +static INPUT_QUEUE: Lazy>> = Lazy::new(|| Default::default()); +static CONTENT_RECT: Lazy> = Lazy::new(|| Default::default()); +static LOOPER: Lazy>> = Lazy::new(|| Default::default()); + +pub fn window_manager() -> std::cell::Ref<'static, Option> { + WINDOW_MANAGER.0.borrow() +} + +pub fn input_queue() -> RwLockReadGuard<'static, Option> { + INPUT_QUEUE.read().unwrap() +} + +pub fn content_rect() -> Rect { + CONTENT_RECT.read().unwrap().clone() +} + +pub static PIPE: Lazy<[OwnedFd; 2]> = Lazy::new(|| { + let mut pipe: [RawFd; 2] = Default::default(); + unsafe { libc::pipe(pipe.as_mut_ptr()) }; + pipe.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) }) +}); + +pub fn poll_events() -> Option { + unsafe { + let size = std::mem::size_of::(); + let mut event = Event::Start; + if libc::read(PIPE[0].as_raw_fd(), &mut event as *mut _ as *mut _, size) + == size as libc::ssize_t + { + Some(event) + } else { + None + } + } +} + +unsafe fn wake(event: Event) { + log::trace!("{:?}", event); + let size = std::mem::size_of::(); + let res = libc::write(PIPE[1].as_raw_fd(), &event as *const _ as *const _, size); + assert_eq!(res, size as libc::ssize_t); +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct Rect { + pub left: u32, + pub top: u32, + pub right: u32, + pub bottom: u32, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum Event { + Start, + Resume, + SaveInstanceState, + Pause, + Stop, + Destroy, + ConfigChanged, + LowMemory, + WindowLostFocus, + WindowHasFocus, + WindowCreated, + WindowResized, + WindowRedrawNeeded, + WindowDestroyed, + InputQueueCreated, + InputQueueDestroyed, + ContentRectChanged, +} + +pub unsafe fn create( + mut env: JNIEnv, + _jclass: JClass, + jobject: JObject, + setup: unsafe fn(&str, JNIEnv, &ThreadLooper, GlobalRef), + main: fn(), +) { + //-> jobjectArray { + // Initialize global context + let window_manager = env + .call_method( + &jobject, + "getWindowManager", + "()Landroid/view/WindowManager;", + &[], + ) + .unwrap() + .l() + .unwrap(); + let window_manager = env.new_global_ref(window_manager).unwrap(); + WINDOW_MANAGER.replace(Some(window_manager)); + let activity = env.new_global_ref(jobject).unwrap(); + let vm = env.get_java_vm().unwrap(); + let env = vm.attach_current_thread_as_daemon().unwrap(); + ndk_context::initialize_android_context( + vm.get_java_vm_pointer() as *mut _, + activity.as_obj().as_raw() as *mut _, + ); + + let looper = ThreadLooper::for_thread().unwrap(); + setup(PACKAGE.get().unwrap(), env, &looper, activity); + + let logpipe = { + let mut logpipe: [RawFd; 2] = Default::default(); + libc::pipe(logpipe.as_mut_ptr()); + libc::dup2(logpipe[1], libc::STDOUT_FILENO); + libc::dup2(logpipe[1], libc::STDERR_FILENO); + + logpipe.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) }) + }; + thread::spawn(move || { + let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap(); + let file = File::from_raw_fd(logpipe[0].as_raw_fd()); + let mut reader = BufReader::new(file); + let mut buffer = String::new(); + loop { + buffer.clear(); + if let Ok(len) = reader.read_line(&mut buffer) { + if len == 0 { + break; + } else if let Ok(msg) = CString::new(buffer.clone()) { + android_log(Level::Info, tag, &msg); + } + } + } + }); + + let looper_ready = Arc::new(Condvar::new()); + let signal_looper_ready = looper_ready.clone(); + + thread::spawn(move || { + let looper = ThreadLooper::prepare(); + let foreign = looper.into_foreign(); + foreign + .add_fd( + PIPE[0].as_fd(), + NDK_GLUE_LOOPER_EVENT_PIPE_IDENT, + FdEvent::INPUT, + std::ptr::null_mut(), + ) + .unwrap(); + + { + let mut locked_looper = LOOPER.lock().unwrap(); + *locked_looper = Some(foreign); + signal_looper_ready.notify_one(); + } + + main() + }); + + // Don't return from this function (`ANativeActivity_onCreate`) until the thread + // has created its `ThreadLooper` and assigned it to the static `LOOPER` + // variable. It will be used from `on_input_queue_created` as soon as this + // function returns. + let locked_looper = LOOPER.lock().unwrap(); + let _mutex_guard = looper_ready + .wait_while(locked_looper, |looper| looper.is_none()) + .unwrap(); +} + +pub unsafe fn resume(_: JNIEnv, _: JClass, _: JObject) { + wake(Event::Resume); +} + +pub unsafe fn pause(_: JNIEnv, _: JClass, _: JObject) { + wake(Event::Pause); +} + +pub unsafe fn focus(_: JNIEnv, _: JClass, has_focus: libc::c_int) { + let event = if has_focus == 0 { + Event::WindowLostFocus + } else { + Event::WindowHasFocus + }; + wake(event); +} + +pub unsafe fn start(_: JNIEnv, _: JClass, _: JObject) { + wake(Event::Start); +} + +pub unsafe fn stop(_: JNIEnv, _: JClass, _: JObject) { + wake(Event::Stop); +} + +/////////////////////////////////////////////// +// Events below are not used by event loop yet. +/////////////////////////////////////////////// + +pub unsafe fn save(_: JNIEnv, _: JClass, _: JObject) { + wake(Event::SaveInstanceState); +} + +pub unsafe fn destroy(_: JNIEnv, _: JClass, _: JObject) { + wake(Event::Destroy); + ndk_context::release_android_context(); + WINDOW_MANAGER.take(); +} + +pub unsafe fn memory(_: JNIEnv, _: JClass, _: JObject) { + wake(Event::LowMemory); +} + +/* +unsafe extern "C" fn on_configuration_changed(activity: *mut ANativeActivity) { + wake(activity, Event::ConfigChanged); +} + +unsafe extern "C" fn on_window_resized( + activity: *mut ANativeActivity, + _window: *mut ANativeWindow, +) { + wake(activity, Event::WindowResized); +} + +unsafe extern "C" fn on_input_queue_created( + activity: *mut ANativeActivity, + queue: *mut AInputQueue, +) { + let input_queue = InputQueue::from_ptr(NonNull::new(queue).unwrap()); + let locked_looper = LOOPER.lock().unwrap(); + // The looper should always be `Some` after `fn init()` returns, unless + // future code cleans it up and sets it back to `None` again. + let looper = locked_looper.as_ref().expect("Looper does not exist"); + input_queue.attach_looper(looper, NDK_GLUE_LOOPER_INPUT_QUEUE_IDENT); + *INPUT_QUEUE.write().unwrap() = Some(input_queue); + wake(activity, Event::InputQueueCreated); +} + +unsafe extern "C" fn on_input_queue_destroyed( + activity: *mut ANativeActivity, + queue: *mut AInputQueue, +) { + wake(activity, Event::InputQueueDestroyed); + let mut input_queue_guard = INPUT_QUEUE.write().unwrap(); + assert_eq!(input_queue_guard.as_ref().unwrap().ptr().as_ptr(), queue); + let input_queue = InputQueue::from_ptr(NonNull::new(queue).unwrap()); + input_queue.detach_looper(); + *input_queue_guard = None; +} + +unsafe extern "C" fn on_content_rect_changed(activity: *mut ANativeActivity, rect: *const ARect) { + let rect = Rect { + left: (*rect).left as _, + top: (*rect).top as _, + right: (*rect).right as _, + bottom: (*rect).bottom as _, + }; + *CONTENT_RECT.write().unwrap() = rect; + wake(activity, Event::ContentRectChanged); +} +*/ diff --git a/vendor/tao/src/platform_impl/ios/app_state.rs b/vendor/tao/src/platform_impl/ios/app_state.rs new file mode 100644 index 0000000000..7498544b42 --- /dev/null +++ b/vendor/tao/src/platform_impl/ios/app_state.rs @@ -0,0 +1,1033 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![deny(unused_results)] + +use std::{ + cell::{RefCell, RefMut}, + collections::HashSet, + mem, + os::raw::c_void, + ptr, + time::Instant, +}; + +use objc2::runtime::AnyObject; + +use crate::{ + dpi::LogicalSize, + event::{Event, StartCause, WindowEvent}, + event_loop::ControlFlow, + platform_impl::platform::{ + event_loop::{EventHandler, EventProxy, EventWrapper, Never}, + ffi::{ + id, kCFRunLoopCommonModes, CFAbsoluteTimeGetCurrent, CFRelease, CFRunLoopAddTimer, + CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, + CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, CGRect, CGSize, NSInteger, + NSOperatingSystemVersion, NSUInteger, + }, + }, + window::WindowId as RootWindowId, +}; + +macro_rules! bug { + ($($msg:tt)*) => { + panic!("tao iOS bug, file an issue: {}", format!($($msg)*)) + }; +} + +macro_rules! bug_assert { + ($test:expr, $($msg:tt)*) => { + assert!($test, "tao iOS bug, file an issue: {}", format!($($msg)*)) + }; +} + +enum UserCallbackTransitionResult<'a> { + Success { + event_handler: Box, + active_control_flow: ControlFlow, + processing_redraws: bool, + }, + ReentrancyPrevented { + queued_events: &'a mut Vec, + }, +} + +impl Event<'static, Never> { + fn is_redraw(&self) -> bool { + matches!(self, Event::RedrawRequested(_)) + } +} + +// this is the state machine for the app lifecycle +#[derive(Debug)] +#[must_use = "dropping `AppStateImpl` without inspecting it is probably a bug"] +enum AppStateImpl { + NotLaunched { + queued_windows: Vec, + queued_events: Vec, + queued_gpu_redraws: HashSet, + }, + Launching { + queued_windows: Vec, + queued_events: Vec, + queued_event_handler: Box, + queued_gpu_redraws: HashSet, + }, + ProcessingEvents { + event_handler: Box, + queued_gpu_redraws: HashSet, + active_control_flow: ControlFlow, + }, + // special state to deal with reentrancy and prevent mutable aliasing. + InUserCallback { + queued_events: Vec, + queued_gpu_redraws: HashSet, + }, + ProcessingRedraws { + event_handler: Box, + active_control_flow: ControlFlow, + }, + Waiting { + waiting_event_handler: Box, + start: Instant, + }, + PollFinished { + waiting_event_handler: Box, + }, + Terminated, +} + +struct AppState { + // This should never be `None`, except for briefly during a state transition. + app_state: Option, + control_flow: ControlFlow, + waker: EventLoopWaker, +} + +impl Drop for AppState { + fn drop(&mut self) { + match self.state_mut() { + &mut AppStateImpl::NotLaunched { + ref mut queued_windows, + .. + } + | &mut AppStateImpl::Launching { + ref mut queued_windows, + .. + } => { + for &mut window in queued_windows { + unsafe { + let () = msg_send![window, release]; + } + } + } + _ => {} + } + } +} + +impl AppState { + // requires main thread + unsafe fn get_mut() -> RefMut<'static, AppState> { + // basically everything in UIKit requires the main thread, so it's pointless to use the + // std::sync APIs. + // must be mut because plain `static` requires `Sync` + static mut APP_STATE: RefCell> = RefCell::new(None); + + if cfg!(debug_assertions) { + assert_main_thread!( + "bug in tao: `AppState::get_mut()` can only be called on the main thread" + ); + } + let mut guard = APP_STATE.borrow_mut(); + if guard.is_none() { + #[inline(never)] + #[cold] + unsafe fn init_guard(guard: &mut RefMut<'static, Option>) { + let waker = EventLoopWaker::new(CFRunLoopGetMain()); + **guard = Some(AppState { + app_state: Some(AppStateImpl::NotLaunched { + queued_windows: Vec::new(), + queued_events: Vec::new(), + queued_gpu_redraws: HashSet::new(), + }), + control_flow: ControlFlow::default(), + waker, + }); + } + init_guard(&mut guard) + } + RefMut::map(guard, |state| state.as_mut().unwrap()) + } + + fn state(&self) -> &AppStateImpl { + match &self.app_state { + Some(ref state) => state, + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn state_mut(&mut self) -> &mut AppStateImpl { + match &mut self.app_state { + Some(ref mut state) => state, + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn take_state(&mut self) -> AppStateImpl { + match self.app_state.take() { + Some(state) => state, + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn set_state(&mut self, new_state: AppStateImpl) { + bug_assert!( + self.app_state.is_none(), + "attempted to set an `AppState` without calling `take_state` first {:?}", + self.app_state + ); + self.app_state = Some(new_state) + } + + fn replace_state(&mut self, new_state: AppStateImpl) -> AppStateImpl { + match &mut self.app_state { + Some(ref mut state) => mem::replace(state, new_state), + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn has_launched(&self) -> bool { + !matches!( + self.state(), + &AppStateImpl::NotLaunched { .. } | &AppStateImpl::Launching { .. } + ) + } + + fn will_launch_transition(&mut self, queued_event_handler: Box) { + let (queued_windows, queued_events, queued_gpu_redraws) = match self.take_state() { + AppStateImpl::NotLaunched { + queued_windows, + queued_events, + queued_gpu_redraws, + } => (queued_windows, queued_events, queued_gpu_redraws), + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::Launching { + queued_windows, + queued_events, + queued_event_handler, + queued_gpu_redraws, + }); + } + + fn did_finish_launching_transition(&mut self) -> (Vec, Vec) { + let (windows, events, event_handler, queued_gpu_redraws) = match self.take_state() { + AppStateImpl::Launching { + queued_windows, + queued_events, + queued_event_handler, + queued_gpu_redraws, + } => ( + queued_windows, + queued_events, + queued_event_handler, + queued_gpu_redraws, + ), + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::ProcessingEvents { + event_handler, + active_control_flow: ControlFlow::Poll, + queued_gpu_redraws, + }); + (windows, events) + } + + fn wakeup_transition(&mut self) -> Option { + // before `AppState::did_finish_launching` is called, pretend there is no running + // event loop. + if !self.has_launched() { + return None; + } + + let (event_handler, event) = match (self.control_flow, self.take_state()) { + ( + ControlFlow::Poll, + AppStateImpl::PollFinished { + waiting_event_handler, + }, + ) => ( + waiting_event_handler, + EventWrapper::StaticEvent(Event::NewEvents(StartCause::Poll)), + ), + ( + ControlFlow::Wait, + AppStateImpl::Waiting { + waiting_event_handler, + start, + }, + ) => ( + waiting_event_handler, + EventWrapper::StaticEvent(Event::NewEvents(StartCause::WaitCancelled { + start, + requested_resume: None, + })), + ), + ( + ControlFlow::WaitUntil(requested_resume), + AppStateImpl::Waiting { + waiting_event_handler, + start, + }, + ) => { + let event = if Instant::now() >= requested_resume { + EventWrapper::StaticEvent(Event::NewEvents(StartCause::ResumeTimeReached { + start, + requested_resume, + })) + } else { + EventWrapper::StaticEvent(Event::NewEvents(StartCause::WaitCancelled { + start, + requested_resume: Some(requested_resume), + })) + }; + (waiting_event_handler, event) + } + (ControlFlow::ExitWithCode(_), _) => bug!("unexpected `ControlFlow` `Exit`"), + s => bug!("`EventHandler` unexpectedly woke up {:?}", s), + }; + + self.set_state(AppStateImpl::ProcessingEvents { + event_handler, + queued_gpu_redraws: Default::default(), + active_control_flow: self.control_flow, + }); + Some(event) + } + + fn try_user_callback_transition(&mut self) -> UserCallbackTransitionResult<'_> { + // If we're not able to process an event due to recursion or `Init` not having been sent out + // yet, then queue the events up. + match self.state_mut() { + &mut AppStateImpl::Launching { + ref mut queued_events, + .. + } + | &mut AppStateImpl::NotLaunched { + ref mut queued_events, + .. + } + | &mut AppStateImpl::InUserCallback { + ref mut queued_events, + .. + } => { + // A lifetime cast: early returns are not currently handled well with NLL, but + // polonius handles them well. This transmute is a safe workaround. + return unsafe { + mem::transmute::, UserCallbackTransitionResult<'_>>( + UserCallbackTransitionResult::ReentrancyPrevented { queued_events }, + ) + }; + } + + &mut AppStateImpl::ProcessingEvents { .. } | &mut AppStateImpl::ProcessingRedraws { .. } => {} + + s @ &mut AppStateImpl::PollFinished { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::Terminated => { + bug!("unexpected attempted to process an event {:?}", s) + } + } + + let (event_handler, queued_gpu_redraws, active_control_flow, processing_redraws) = + match self.take_state() { + AppStateImpl::Launching { .. } + | AppStateImpl::NotLaunched { .. } + | AppStateImpl::InUserCallback { .. } => unreachable!(), + AppStateImpl::ProcessingEvents { + event_handler, + queued_gpu_redraws, + active_control_flow, + } => ( + event_handler, + queued_gpu_redraws, + active_control_flow, + false, + ), + AppStateImpl::ProcessingRedraws { + event_handler, + active_control_flow, + } => (event_handler, Default::default(), active_control_flow, true), + AppStateImpl::PollFinished { .. } + | AppStateImpl::Waiting { .. } + | AppStateImpl::Terminated => unreachable!(), + }; + self.set_state(AppStateImpl::InUserCallback { + queued_events: Vec::new(), + queued_gpu_redraws, + }); + UserCallbackTransitionResult::Success { + event_handler, + active_control_flow, + processing_redraws, + } + } + + fn main_events_cleared_transition(&mut self) -> HashSet { + let (event_handler, queued_gpu_redraws, active_control_flow) = match self.take_state() { + AppStateImpl::ProcessingEvents { + event_handler, + queued_gpu_redraws, + active_control_flow, + } => (event_handler, queued_gpu_redraws, active_control_flow), + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::ProcessingRedraws { + event_handler, + active_control_flow, + }); + queued_gpu_redraws + } + + fn events_cleared_transition(&mut self) { + if !self.has_launched() { + return; + } + let (waiting_event_handler, old) = match self.take_state() { + AppStateImpl::ProcessingRedraws { + event_handler, + active_control_flow, + } => (event_handler, active_control_flow), + s => bug!("unexpected state {:?}", s), + }; + + let new = self.control_flow; + match (old, new) { + (ControlFlow::Poll, ControlFlow::Poll) => self.set_state(AppStateImpl::PollFinished { + waiting_event_handler, + }), + (ControlFlow::Wait, ControlFlow::Wait) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { + waiting_event_handler, + start, + }); + } + (ControlFlow::WaitUntil(old_instant), ControlFlow::WaitUntil(new_instant)) + if old_instant == new_instant => + { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { + waiting_event_handler, + start, + }); + } + (_, ControlFlow::Wait) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { + waiting_event_handler, + start, + }); + self.waker.stop() + } + (_, ControlFlow::WaitUntil(new_instant)) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { + waiting_event_handler, + start, + }); + self.waker.start_at(new_instant) + } + (_, ControlFlow::Poll) => { + self.set_state(AppStateImpl::PollFinished { + waiting_event_handler, + }); + self.waker.start() + } + (_, ControlFlow::ExitWithCode(_)) => { + // https://developer.apple.com/library/archive/qa/qa1561/_index.html + // it is not possible to quit an iOS app gracefully and programatically + warn!("`ControlFlow::Exit` ignored on iOS"); + self.control_flow = old + } + } + } + + fn terminated_transition(&mut self) -> Box { + match self.replace_state(AppStateImpl::Terminated) { + AppStateImpl::ProcessingEvents { event_handler, .. } => event_handler, + s => bug!( + "`LoopDestroyed` happened while not processing events {:?}", + s + ), + } + } +} + +// requires main thread and window is a UIWindow +// retains window +pub unsafe fn set_key_window(window: id) { + bug_assert!( + msg_send![window, isKindOfClass: class!(UIWindow)], + "set_key_window called with an incorrect type" + ); + let mut this = AppState::get_mut(); + match this.state_mut() { + &mut AppStateImpl::NotLaunched { + ref mut queued_windows, + .. + } => return queued_windows.push(msg_send![window, retain]), + &mut AppStateImpl::ProcessingEvents { .. } + | &mut AppStateImpl::InUserCallback { .. } + | &mut AppStateImpl::ProcessingRedraws { .. } => {} + s @ &mut AppStateImpl::Launching { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s), + &mut AppStateImpl::Terminated => { + panic!("Attempt to create a `Window` after the app has terminated") + } + } + drop(this); + msg_send![window, makeKeyAndVisible] +} + +// requires main thread and window is a UIWindow +// retains window +pub unsafe fn queue_gl_or_metal_redraw(window: id) { + bug_assert!( + msg_send![window, isKindOfClass: class!(UIWindow)], + "set_key_window called with an incorrect type" + ); + let mut this = AppState::get_mut(); + match this.state_mut() { + &mut AppStateImpl::NotLaunched { + ref mut queued_gpu_redraws, + .. + } + | &mut AppStateImpl::Launching { + ref mut queued_gpu_redraws, + .. + } + | &mut AppStateImpl::ProcessingEvents { + ref mut queued_gpu_redraws, + .. + } + | &mut AppStateImpl::InUserCallback { + ref mut queued_gpu_redraws, + .. + } => drop(queued_gpu_redraws.insert(window)), + s @ &mut AppStateImpl::ProcessingRedraws { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s), + &mut AppStateImpl::Terminated => { + panic!("Attempt to create a `Window` after the app has terminated") + } + } + drop(this); +} + +// requires main thread +pub unsafe fn will_launch(queued_event_handler: Box) { + AppState::get_mut().will_launch_transition(queued_event_handler) +} + +// requires main thread +pub unsafe fn did_finish_launching() { + let mut this = AppState::get_mut(); + let windows = match this.state_mut() { + AppStateImpl::Launching { queued_windows, .. } => mem::take(queued_windows), + s => bug!("unexpected state {:?}", s), + }; + + // start waking up the event loop now! + bug_assert!( + this.control_flow == ControlFlow::Poll, + "unexpectedly not setup to `Poll` on launch!" + ); + this.waker.start(); + + // have to drop RefMut because the window setup code below can trigger new events + drop(this); + + for window in windows { + let count: NSUInteger = msg_send![window, retainCount]; + // make sure the window is still referenced + if count > 1 { + // Do a little screen dance here to account for windows being created before + // `UIApplicationMain` is called. This fixes visual issues such as being + // offcenter and sized incorrectly. Additionally, to fix orientation issues, we + // gotta reset the `rootViewController`. + // + // relevant iOS log: + // ``` + // [ApplicationLifecycle] Windows were created before application initialzation + // completed. This may result in incorrect visual appearance. + // ``` + let screen: id = msg_send![window, screen]; + let _: id = msg_send![screen, retain]; + let () = msg_send![window, setScreen:0 as id]; + let () = msg_send![window, setScreen: screen]; + let () = msg_send![screen, release]; + let controller: id = msg_send![window, rootViewController]; + let () = msg_send![window, setRootViewController: ptr::null::()]; + let () = msg_send![window, setRootViewController: controller]; + let () = msg_send![window, makeKeyAndVisible]; + } + let () = msg_send![window, release]; + } + + let (windows, events) = AppState::get_mut().did_finish_launching_transition(); + + let events = std::iter::once(EventWrapper::StaticEvent(Event::NewEvents( + StartCause::Init, + ))) + .chain(events); + handle_nonuser_events(events); + + // the above window dance hack, could possibly trigger new windows to be created. + // we can just set those windows up normally, as they were created after didFinishLaunching + for window in windows { + let count: NSUInteger = msg_send![window, retainCount]; + // make sure the window is still referenced + if count > 1 { + let () = msg_send![window, makeKeyAndVisible]; + } + let () = msg_send![window, release]; + } +} + +// requires main thread +// AppState::did_finish_launching handles the special transition `Init` +pub unsafe fn handle_wakeup_transition() { + let mut this = AppState::get_mut(); + let wakeup_event = match this.wakeup_transition() { + None => return, + Some(wakeup_event) => wakeup_event, + }; + drop(this); + + handle_nonuser_event(wakeup_event) +} + +// requires main thread +pub unsafe fn handle_nonuser_event(event: EventWrapper) { + handle_nonuser_events(std::iter::once(event)) +} + +// requires main thread +pub unsafe fn handle_nonuser_events>(events: I) { + let mut this = AppState::get_mut(); + let (mut event_handler, active_control_flow, processing_redraws) = + match this.try_user_callback_transition() { + UserCallbackTransitionResult::ReentrancyPrevented { queued_events } => { + queued_events.extend(events); + return; + } + UserCallbackTransitionResult::Success { + event_handler, + active_control_flow, + processing_redraws, + } => (event_handler, active_control_flow, processing_redraws), + }; + let mut control_flow = this.control_flow; + drop(this); + + for wrapper in events { + match wrapper { + EventWrapper::StaticEvent(event) => { + if !processing_redraws && event.is_redraw() { + log::info!("processing `RedrawRequested` during the main event loop"); + } else if processing_redraws && !event.is_redraw() { + log::warn!( + "processing non `RedrawRequested` event after the main event loop: {:#?}", + event + ); + } + event_handler.handle_nonuser_event(event, &mut control_flow) + } + EventWrapper::EventProxy(proxy) => { + handle_event_proxy(&mut event_handler, control_flow, proxy) + } + } + } + + loop { + let mut this = AppState::get_mut(); + let queued_events = match this.state_mut() { + &mut AppStateImpl::InUserCallback { + ref mut queued_events, + queued_gpu_redraws: _, + } => mem::take(queued_events), + s => bug!("unexpected state {:?}", s), + }; + if queued_events.is_empty() { + let queued_gpu_redraws = match this.take_state() { + AppStateImpl::InUserCallback { + queued_events: _, + queued_gpu_redraws, + } => queued_gpu_redraws, + _ => unreachable!(), + }; + this.app_state = Some(if processing_redraws { + bug_assert!( + queued_gpu_redraws.is_empty(), + "redraw queued while processing redraws" + ); + AppStateImpl::ProcessingRedraws { + event_handler, + active_control_flow, + } + } else { + AppStateImpl::ProcessingEvents { + event_handler, + queued_gpu_redraws, + active_control_flow, + } + }); + this.control_flow = control_flow; + break; + } + drop(this); + + for wrapper in queued_events { + match wrapper { + EventWrapper::StaticEvent(event) => { + if !processing_redraws && event.is_redraw() { + log::info!("processing `RedrawRequested` during the main event loop"); + } else if processing_redraws && !event.is_redraw() { + log::warn!( + "processing non-`RedrawRequested` event after the main event loop: {:#?}", + event + ); + } + event_handler.handle_nonuser_event(event, &mut control_flow) + } + EventWrapper::EventProxy(proxy) => { + handle_event_proxy(&mut event_handler, control_flow, proxy) + } + } + } + } +} + +// requires main thread +unsafe fn handle_user_events() { + let mut this = AppState::get_mut(); + let mut control_flow = this.control_flow; + let (mut event_handler, active_control_flow, processing_redraws) = + match this.try_user_callback_transition() { + UserCallbackTransitionResult::ReentrancyPrevented { .. } => { + bug!("unexpected attempted to process an event") + } + UserCallbackTransitionResult::Success { + event_handler, + active_control_flow, + processing_redraws, + } => (event_handler, active_control_flow, processing_redraws), + }; + if processing_redraws { + bug!("user events attempted to be sent out while `ProcessingRedraws`"); + } + drop(this); + + event_handler.handle_user_events(&mut control_flow); + + loop { + let mut this = AppState::get_mut(); + let queued_events = match this.state_mut() { + &mut AppStateImpl::InUserCallback { + ref mut queued_events, + queued_gpu_redraws: _, + } => mem::take(queued_events), + s => bug!("unexpected state {:?}", s), + }; + if queued_events.is_empty() { + let queued_gpu_redraws = match this.take_state() { + AppStateImpl::InUserCallback { + queued_events: _, + queued_gpu_redraws, + } => queued_gpu_redraws, + _ => unreachable!(), + }; + this.app_state = Some(AppStateImpl::ProcessingEvents { + event_handler, + queued_gpu_redraws, + active_control_flow, + }); + this.control_flow = control_flow; + break; + } + drop(this); + + for wrapper in queued_events { + match wrapper { + EventWrapper::StaticEvent(event) => { + event_handler.handle_nonuser_event(event, &mut control_flow) + } + EventWrapper::EventProxy(proxy) => { + handle_event_proxy(&mut event_handler, control_flow, proxy) + } + } + } + event_handler.handle_user_events(&mut control_flow); + } +} + +// requires main thread +pub unsafe fn handle_main_events_cleared() { + let mut this = AppState::get_mut(); + if !this.has_launched() { + return; + } + match this.state_mut() { + &mut AppStateImpl::ProcessingEvents { .. } => {} + _ => bug!("`ProcessingRedraws` happened unexpectedly"), + }; + drop(this); + + // User events are always sent out at the end of the "MainEventLoop" + handle_user_events(); + handle_nonuser_event(EventWrapper::StaticEvent(Event::MainEventsCleared)); + + let mut this = AppState::get_mut(); + let mut redraw_events: Vec = this + .main_events_cleared_transition() + .into_iter() + .map(|window| EventWrapper::StaticEvent(Event::RedrawRequested(RootWindowId(window.into())))) + .collect(); + + if !redraw_events.is_empty() { + redraw_events.push(EventWrapper::StaticEvent(Event::RedrawEventsCleared)); + } + drop(this); + + handle_nonuser_events(redraw_events); +} + +// requires main thread +pub unsafe fn handle_events_cleared() { + AppState::get_mut().events_cleared_transition(); +} + +// requires main thread +pub unsafe fn terminated() { + let mut this = AppState::get_mut(); + let mut event_handler = this.terminated_transition(); + let mut control_flow = this.control_flow; + drop(this); + + event_handler.handle_nonuser_event(Event::LoopDestroyed, &mut control_flow) +} + +fn handle_event_proxy( + event_handler: &mut Box, + control_flow: ControlFlow, + proxy: EventProxy, +) { + match proxy { + EventProxy::DpiChangedProxy { + suggested_size, + scale_factor, + window_id, + } => handle_hidpi_proxy( + event_handler, + control_flow, + suggested_size, + scale_factor, + window_id, + ), + } +} + +fn handle_hidpi_proxy( + event_handler: &mut Box, + mut control_flow: ControlFlow, + suggested_size: LogicalSize, + scale_factor: f64, + window_id: id, +) { + let mut size = suggested_size.to_physical(scale_factor); + let new_inner_size = &mut size; + let event = Event::WindowEvent { + window_id: RootWindowId(window_id.into()), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + }, + }; + event_handler.handle_nonuser_event(event, &mut control_flow); + let (view, screen_frame) = get_view_and_screen_frame(window_id); + let physical_size = *new_inner_size; + let logical_size = physical_size.to_logical(scale_factor); + let size = CGSize::new(logical_size); + let new_frame: CGRect = CGRect::new(screen_frame.origin, size); + unsafe { + let () = msg_send![view, setFrame: new_frame]; + } +} + +fn get_view_and_screen_frame(window_id: id) -> (id, CGRect) { + unsafe { + let view_controller: id = msg_send![window_id, rootViewController]; + let view: id = msg_send![view_controller, view]; + let bounds: CGRect = msg_send![window_id, bounds]; + let screen: id = msg_send![window_id, screen]; + let screen_space: id = msg_send![screen, coordinateSpace]; + let screen_frame: CGRect = + msg_send![window_id, convertRect:bounds, toCoordinateSpace:screen_space]; + (view, screen_frame) + } +} + +struct EventLoopWaker { + timer: CFRunLoopTimerRef, +} + +impl Drop for EventLoopWaker { + fn drop(&mut self) { + unsafe { + CFRunLoopTimerInvalidate(self.timer); + CFRelease(self.timer as _); + } + } +} + +impl EventLoopWaker { + fn new(rl: CFRunLoopRef) -> EventLoopWaker { + extern "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {} + unsafe { + // Create a timer with a 0.1µs interval (1ns does not work) to mimic polling. + // It is initially setup with a first fire time really far into the + // future, but that gets changed to fire immediately in did_finish_launching + let timer = CFRunLoopTimerCreate( + ptr::null_mut(), + f64::MAX, + 0.000_000_1, + 0, + 0, + wakeup_main_loop, + ptr::null_mut(), + ); + CFRunLoopAddTimer(rl, timer, kCFRunLoopCommonModes); + + EventLoopWaker { timer } + } + } + + fn stop(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MAX) } + } + + fn start(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MIN) } + } + + fn start_at(&mut self, instant: Instant) { + let now = Instant::now(); + if now >= instant { + self.start(); + } else { + unsafe { + let current = CFAbsoluteTimeGetCurrent(); + let duration = instant - now; + let fsecs = duration.subsec_nanos() as f64 / 1_000_000_000.0 + duration.as_secs() as f64; + CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs) + } + } + } +} + +macro_rules! os_capabilities { + ( + $( + $(#[$attr:meta])* + $error_name:ident: $objc_call:literal, + $name:ident: $major:literal-$minor:literal + ),* + $(,)* + ) => { + #[derive(Clone, Debug)] + pub struct OSCapabilities { + $( + pub $name: bool, + )* + + os_version: NSOperatingSystemVersion, + } + + impl From for OSCapabilities { + fn from(os_version: NSOperatingSystemVersion) -> OSCapabilities { + $(let $name = os_version.meets_requirements($major, $minor);)* + OSCapabilities { $($name,)* os_version, } + } + } + + impl OSCapabilities {$( + $(#[$attr])* + pub fn $error_name(&self, extra_msg: &str) { + log::warn!( + concat!("`", $objc_call, "` requires iOS {}.{}+. This device is running iOS {}.{}.{}. {}"), + $major, $minor, self.os_version.major, self.os_version.minor, self.os_version.patch, + extra_msg + ) + } + )*} + }; +} + +os_capabilities! { + /// https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + #[allow(unused)] // error message unused + safe_area_err_msg: "-[UIView safeAreaInsets]", + safe_area: 11-0, + /// https://developer.apple.com/documentation/uikit/uiviewcontroller/2887509-setneedsupdateofhomeindicatoraut?language=objc + home_indicator_hidden_err_msg: "-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]", + home_indicator_hidden: 11-0, + /// https://developer.apple.com/documentation/uikit/uiviewcontroller/2887507-setneedsupdateofscreenedgesdefer?language=objc + defer_system_gestures_err_msg: "-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystem]", + defer_system_gestures: 11-0, + /// https://developer.apple.com/documentation/uikit/uiscreen/2806814-maximumframespersecond?language=objc + maximum_frames_per_second_err_msg: "-[UIScreen maximumFramesPerSecond]", + maximum_frames_per_second: 10-3, + /// https://developer.apple.com/documentation/uikit/uitouch/1618110-force?language=objc + #[allow(unused)] // error message unused + force_touch_err_msg: "-[UITouch force]", + force_touch: 9-0, +} + +impl NSOperatingSystemVersion { + fn meets_requirements(&self, required_major: NSInteger, required_minor: NSInteger) -> bool { + (self.major, self.minor) >= (required_major, required_minor) + } +} + +pub fn os_capabilities() -> OSCapabilities { + lazy_static! { + static ref OS_CAPABILITIES: OSCapabilities = { + let version: NSOperatingSystemVersion = unsafe { + let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; + let atleast_ios_8: bool = msg_send![ + process_info, + respondsToSelector: sel!(operatingSystemVersion) + ]; + // tao requires atleast iOS 8 because no one has put the time into supporting earlier os versions. + // Older iOS versions are increasingly difficult to test. For example, Xcode 11 does not support + // debugging on devices with an iOS version of less than 8. Another example, in order to use an iOS + // simulator older than iOS 8, you must download an older version of Xcode (<9), and at least Xcode 7 + // has been tested to not even run on macOS 10.15 - Xcode 8 might? + // + // The minimum required iOS version is likely to grow in the future. + assert!( + atleast_ios_8, + "`tao` requires iOS version 8 or greater" + ); + msg_send![process_info, operatingSystemVersion] + }; + version.into() + }; + } + OS_CAPABILITIES.clone() +} diff --git a/vendor/tao/src/platform_impl/ios/badge.rs b/vendor/tao/src/platform_impl/ios/badge.rs new file mode 100644 index 0000000000..4a1f3b3d7d --- /dev/null +++ b/vendor/tao/src/platform_impl/ios/badge.rs @@ -0,0 +1,15 @@ +use std::ffi::CStr; + +use objc2::{ + msg_send, + runtime::{AnyClass, AnyObject}, +}; + +pub fn set_badge_count(count: i32) { + unsafe { + let ui_application = AnyClass::get(CStr::from_bytes_with_nul(b"UIApplication\0").unwrap()) + .expect("Failed to get UIApplication class"); + let app: *mut AnyObject = msg_send![ui_application, sharedApplication]; + let _: () = msg_send![app, setApplicationIconBadgeNumber:count]; + } +} diff --git a/vendor/tao/src/platform_impl/ios/event_loop.rs b/vendor/tao/src/platform_impl/ios/event_loop.rs new file mode 100644 index 0000000000..9c39854406 --- /dev/null +++ b/vendor/tao/src/platform_impl/ios/event_loop.rs @@ -0,0 +1,385 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + collections::VecDeque, + ffi::c_void, + fmt::{self, Debug}, + marker::PhantomData, + mem, ptr, +}; + +use crossbeam_channel::{self as channel, Receiver, Sender}; + +use crate::{ + dpi::{LogicalSize, PhysicalPosition}, + error::ExternalError, + event::Event, + event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootEventLoopWindowTarget}, + monitor::MonitorHandle as RootMonitorHandle, + platform::ios::Idiom, +}; + +use crate::platform_impl::platform::{ + app_state, + ffi::{ + id, kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, + kCFRunLoopDefaultMode, kCFRunLoopEntry, kCFRunLoopExit, nil, CFIndex, CFRelease, + CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddSource, CFRunLoopGetMain, + CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopSourceContext, CFRunLoopSourceCreate, + CFRunLoopSourceInvalidate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, + NSStringRust, UIApplicationMain, UIUserInterfaceIdiom, + }, + monitor, set_badge_count, view, MonitorHandle, +}; + +#[non_exhaustive] +#[derive(Debug)] +pub enum EventWrapper { + StaticEvent(Event<'static, Never>), + EventProxy(EventProxy), +} + +#[non_exhaustive] +#[derive(Debug, PartialEq)] +pub enum EventProxy { + DpiChangedProxy { + window_id: id, + suggested_size: LogicalSize, + scale_factor: f64, + }, +} + +#[derive(Clone)] +pub struct EventLoopWindowTarget { + receiver: Receiver, + sender_to_clone: Sender, +} + +impl EventLoopWindowTarget { + pub fn available_monitors(&self) -> VecDeque { + // guaranteed to be on main thread + unsafe { monitor::uiscreens() } + } + + #[inline] + pub fn monitor_from_point(&self, _x: f64, _y: f64) -> Option { + warn!("`Window::monitor_from_point` is ignored on iOS"); + None + } + + pub fn primary_monitor(&self) -> Option { + // guaranteed to be on main thread + let monitor = unsafe { monitor::main_uiscreen() }; + + Some(RootMonitorHandle { inner: monitor }) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::UiKit(rwh_05::UiKitDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06(&self) -> Result { + Ok(rwh_06::RawDisplayHandle::UiKit( + rwh_06::UiKitDisplayHandle::new(), + )) + } + + pub fn cursor_position(&self) -> Result, ExternalError> { + debug!("`EventLoopWindowTarget::cursor_position` is ignored on iOS"); + Ok((0, 0).into()) + } + + /// Sets badge count on iOS launcher. 0 hides the count + pub fn set_badge_count(&self, count: i32) { + set_badge_count(count); + } +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +pub struct EventLoop { + window_target: RootEventLoopWindowTarget, +} + +impl EventLoop { + pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> EventLoop { + static mut SINGLETON_INIT: bool = false; + unsafe { + assert_main_thread!("`EventLoop` can only be created on the main thread on iOS"); + assert!( + !SINGLETON_INIT, + "Only one `EventLoop` is supported on iOS. \ + `EventLoopProxy` might be helpful" + ); + SINGLETON_INIT = true; + view::create_delegate_class(); + } + + let (sender_to_clone, receiver) = channel::unbounded(); + + // this line sets up the main run loop before `UIApplicationMain` + setup_control_flow_observers(); + + EventLoop { + window_target: RootEventLoopWindowTarget { + p: EventLoopWindowTarget { + receiver, + sender_to_clone, + }, + _marker: PhantomData, + }, + } + } + + pub fn run(self, event_handler: F) -> ! + where + F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), + { + unsafe { + let application: id = msg_send![class!(UIApplication), sharedApplication]; + assert_eq!( + application, + ptr::null_mut(), + "\ + `EventLoop` cannot be `run` after a call to `UIApplicationMain` on iOS\n\ + Note: `EventLoop::run` calls `UIApplicationMain` on iOS" + ); + app_state::will_launch(Box::new(EventLoopHandler { + f: event_handler, + event_loop: self.window_target, + })); + + UIApplicationMain( + 0, + ptr::null(), + nil, + NSStringRust::alloc(nil).init_str("AppDelegate"), + ); + unreachable!() + } + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.window_target.p.sender_to_clone.clone()) + } + + pub fn window_target(&self) -> &RootEventLoopWindowTarget { + &self.window_target + } +} + +// EventLoopExtIOS +impl EventLoop { + pub fn idiom(&self) -> Idiom { + // guaranteed to be on main thread + unsafe { self::get_idiom() } + } +} + +pub struct EventLoopProxy { + sender: Sender, + source: CFRunLoopSourceRef, +} + +unsafe impl Send for EventLoopProxy {} +unsafe impl Sync for EventLoopProxy {} + +impl Clone for EventLoopProxy { + fn clone(&self) -> EventLoopProxy { + EventLoopProxy::new(self.sender.clone()) + } +} + +impl Drop for EventLoopProxy { + fn drop(&mut self) { + unsafe { + CFRunLoopSourceInvalidate(self.source); + CFRelease(self.source as _); + } + } +} + +impl EventLoopProxy { + fn new(sender: Sender) -> EventLoopProxy { + unsafe { + // just wake up the eventloop + extern "C" fn event_loop_proxy_handler(_: *mut c_void) {} + + // adding a Source to the main CFRunLoop lets us wake it up and + // process user events through the normal OS EventLoop mechanisms. + let rl = CFRunLoopGetMain(); + // we want all the members of context to be zero/null, except one + let mut context: CFRunLoopSourceContext = mem::zeroed(); + context.perform = Some(event_loop_proxy_handler); + let source = CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::MAX - 1, &mut context); + CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes); + CFRunLoopWakeUp(rl); + + EventLoopProxy { sender, source } + } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self + .sender + .send(event) + .map_err(|channel::SendError(x)| EventLoopClosed(x))?; + unsafe { + // let the main thread know there's a new event + CFRunLoopSourceSignal(self.source); + let rl = CFRunLoopGetMain(); + CFRunLoopWakeUp(rl); + } + Ok(()) + } +} + +fn setup_control_flow_observers() { + unsafe { + // begin is queued with the highest priority to ensure it is processed before other observers + extern "C" fn control_flow_begin_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + unsafe { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopAfterWaiting => app_state::handle_wakeup_transition(), + kCFRunLoopEntry => unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } + } + } + + // Core Animation registers its `CFRunLoopObserver` that performs drawing operations in + // `CA::Transaction::ensure_implicit` with a priority of `0x1e8480`. We set the main_end + // priority to be 0, in order to send MainEventsCleared before RedrawRequested. This value was + // chosen conservatively to guard against apple using different priorities for their redraw + // observers in different OS's or on different devices. If it so happens that it's too + // conservative, the main symptom would be non-redraw events coming in after `MainEventsCleared`. + // + // The value of `0x1e8480` was determined by inspecting stack traces and the associated + // registers for every `CFRunLoopAddObserver` call on an iPad Air 2 running iOS 11.4. + // + // Also tested to be `0x1e8480` on iPhone 8, iOS 13 beta 4. + extern "C" fn control_flow_main_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + unsafe { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => app_state::handle_main_events_cleared(), + kCFRunLoopExit => (), // Mode is changed to others like `UITrackingRunLoopMode` + _ => unreachable!(), + } + } + } + + // end is queued with the lowest priority to ensure it is processed after other observers + extern "C" fn control_flow_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + unsafe { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => app_state::handle_events_cleared(), + kCFRunLoopExit => (), // Mode is changed to others like `UITrackingRunLoopMode` + _ => unreachable!(), + } + } + } + + let main_loop = CFRunLoopGetMain(); + + let begin_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopEntry | kCFRunLoopAfterWaiting, + 1, // repeat = true + CFIndex::MIN, + control_flow_begin_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, begin_observer, kCFRunLoopDefaultMode); + + let main_end_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + 1, // repeat = true + 0, // see comment on `control_flow_main_end_handler` + control_flow_main_end_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, main_end_observer, kCFRunLoopDefaultMode); + + let end_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + 1, // repeat = true + CFIndex::MAX, + control_flow_end_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, end_observer, kCFRunLoopDefaultMode); + } +} + +#[non_exhaustive] +#[derive(Debug)] +pub enum Never {} + +pub trait EventHandler: Debug { + fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow); + fn handle_user_events(&mut self, control_flow: &mut ControlFlow); +} + +struct EventLoopHandler { + f: F, + event_loop: RootEventLoopWindowTarget, +} + +impl Debug for EventLoopHandler { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EventLoopHandler") + .field("event_loop", &self.event_loop) + .finish() + } +} + +impl EventHandler for EventLoopHandler +where + F: 'static + FnMut(Event<'_, T>, &RootEventLoopWindowTarget, &mut ControlFlow), + T: 'static, +{ + fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow) { + (self.f)( + event.map_nonuser_event().unwrap(), + &self.event_loop, + control_flow, + ); + } + + fn handle_user_events(&mut self, control_flow: &mut ControlFlow) { + for event in self.event_loop.p.receiver.try_iter() { + (self.f)(Event::UserEvent(event), &self.event_loop, control_flow); + } + } +} + +// must be called on main thread +pub unsafe fn get_idiom() -> Idiom { + let device: id = msg_send![class!(UIDevice), currentDevice]; + let raw_idiom: UIUserInterfaceIdiom = msg_send![device, userInterfaceIdiom]; + raw_idiom.into() +} diff --git a/vendor/tao/src/platform_impl/ios/ffi.rs b/vendor/tao/src/platform_impl/ios/ffi.rs new file mode 100644 index 0000000000..3fcff99a2f --- /dev/null +++ b/vendor/tao/src/platform_impl/ios/ffi.rs @@ -0,0 +1,433 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] + +use std::{convert::TryInto, ffi::CString, ops::BitOr, os::raw::*}; + +use objc2::{ + encode::{Encode, Encoding}, + runtime::{AnyObject, Bool}, +}; + +use crate::{ + dpi::LogicalSize, + platform::ios::{Idiom, ScreenEdge, ValidOrientations}, +}; + +pub type id = *mut AnyObject; +pub const nil: id = 0 as id; + +#[allow(non_camel_case_types)] +pub type BOOL = Bool; +#[allow(deprecated)] +pub const YES: Bool = Bool::YES; +#[allow(deprecated)] +pub const NO: Bool = Bool::NO; + +#[cfg(target_pointer_width = "32")] +pub type CGFloat = f32; +#[cfg(target_pointer_width = "64")] +pub type CGFloat = f64; + +pub type NSInteger = isize; +pub type NSUInteger = usize; + +#[repr(C)] +#[derive(Clone, Debug)] +pub struct NSOperatingSystemVersion { + pub major: NSInteger, + pub minor: NSInteger, + pub patch: NSInteger, +} + +unsafe impl Encode for NSOperatingSystemVersion { + const ENCODING: Encoding = Encoding::Struct( + "?", + &[ + NSInteger::ENCODING, + NSInteger::ENCODING, + NSInteger::ENCODING, + ], + ); +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CGPoint { + pub x: CGFloat, + pub y: CGFloat, +} + +unsafe impl Encode for CGPoint { + const ENCODING: Encoding = Encoding::Struct("CGPoint", &[CGFloat::ENCODING, CGFloat::ENCODING]); +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CGSize { + pub width: CGFloat, + pub height: CGFloat, +} + +impl CGSize { + pub fn new(size: LogicalSize) -> CGSize { + CGSize { + width: size.width as _, + height: size.height as _, + } + } +} + +unsafe impl Encode for CGSize { + const ENCODING: Encoding = Encoding::Struct("CGSize", &[CGFloat::ENCODING, CGFloat::ENCODING]); +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CGRect { + pub origin: CGPoint, + pub size: CGSize, +} + +impl CGRect { + pub fn new(origin: CGPoint, size: CGSize) -> CGRect { + CGRect { origin, size } + } +} + +unsafe impl Encode for CGRect { + const ENCODING: Encoding = Encoding::Struct("CGRect", &[CGPoint::ENCODING, CGSize::ENCODING]); +} + +#[derive(Debug)] +#[allow(dead_code)] +#[repr(isize)] +pub enum UITouchPhase { + Began = 0, + Moved, + Stationary, + Ended, + Cancelled, +} + +unsafe impl Encode for UITouchPhase { + const ENCODING: Encoding = isize::ENCODING; +} + +#[derive(Debug, PartialEq)] +#[allow(dead_code)] +#[repr(isize)] +pub enum UIForceTouchCapability { + Unknown = 0, + Unavailable, + Available, +} + +unsafe impl Encode for UIForceTouchCapability { + const ENCODING: Encoding = isize::ENCODING; +} + +#[derive(Debug, PartialEq)] +#[allow(dead_code)] +#[repr(isize)] +pub enum UITouchType { + Direct = 0, + Indirect, + Pencil, +} + +unsafe impl Encode for UITouchType { + const ENCODING: Encoding = isize::ENCODING; +} + +#[repr(C)] +#[derive(Debug, Clone)] +pub struct UIEdgeInsets { + pub top: CGFloat, + pub left: CGFloat, + pub bottom: CGFloat, + pub right: CGFloat, +} + +unsafe impl Encode for UIEdgeInsets { + const ENCODING: Encoding = Encoding::Struct( + "UIEdgeInsets", + &[ + CGFloat::ENCODING, + CGFloat::ENCODING, + CGFloat::ENCODING, + CGFloat::ENCODING, + ], + ); +} + +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct UIUserInterfaceIdiom(NSInteger); + +unsafe impl Encode for UIUserInterfaceIdiom { + const ENCODING: Encoding = NSInteger::ENCODING; +} + +impl UIUserInterfaceIdiom { + pub const Unspecified: UIUserInterfaceIdiom = UIUserInterfaceIdiom(-1); + pub const Phone: UIUserInterfaceIdiom = UIUserInterfaceIdiom(0); + pub const Pad: UIUserInterfaceIdiom = UIUserInterfaceIdiom(1); + pub const TV: UIUserInterfaceIdiom = UIUserInterfaceIdiom(2); + pub const CarPlay: UIUserInterfaceIdiom = UIUserInterfaceIdiom(3); +} + +impl From for UIUserInterfaceIdiom { + fn from(idiom: Idiom) -> UIUserInterfaceIdiom { + match idiom { + Idiom::Unspecified => UIUserInterfaceIdiom::Unspecified, + Idiom::Phone => UIUserInterfaceIdiom::Phone, + Idiom::Pad => UIUserInterfaceIdiom::Pad, + Idiom::TV => UIUserInterfaceIdiom::TV, + Idiom::CarPlay => UIUserInterfaceIdiom::CarPlay, + } + } +} + +impl Into for UIUserInterfaceIdiom { + fn into(self) -> Idiom { + match self { + UIUserInterfaceIdiom::Unspecified => Idiom::Unspecified, + UIUserInterfaceIdiom::Phone => Idiom::Phone, + UIUserInterfaceIdiom::Pad => Idiom::Pad, + UIUserInterfaceIdiom::TV => Idiom::TV, + UIUserInterfaceIdiom::CarPlay => Idiom::CarPlay, + _ => unreachable!(), + } + } +} + +#[repr(transparent)] +#[derive(Clone, Copy, Debug)] +pub struct UIInterfaceOrientationMask(NSUInteger); + +unsafe impl Encode for UIInterfaceOrientationMask { + const ENCODING: Encoding = NSUInteger::ENCODING; +} + +impl UIInterfaceOrientationMask { + pub const Portrait: UIInterfaceOrientationMask = UIInterfaceOrientationMask(1 << 1); + pub const PortraitUpsideDown: UIInterfaceOrientationMask = UIInterfaceOrientationMask(1 << 2); + pub const LandscapeLeft: UIInterfaceOrientationMask = UIInterfaceOrientationMask(1 << 4); + pub const LandscapeRight: UIInterfaceOrientationMask = UIInterfaceOrientationMask(1 << 3); + pub const Landscape: UIInterfaceOrientationMask = + UIInterfaceOrientationMask(Self::LandscapeLeft.0 | Self::LandscapeRight.0); + pub const AllButUpsideDown: UIInterfaceOrientationMask = + UIInterfaceOrientationMask(Self::Landscape.0 | Self::Portrait.0); + pub const All: UIInterfaceOrientationMask = + UIInterfaceOrientationMask(Self::AllButUpsideDown.0 | Self::PortraitUpsideDown.0); +} + +impl BitOr for UIInterfaceOrientationMask { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self { + UIInterfaceOrientationMask(self.0 | rhs.0) + } +} + +impl UIInterfaceOrientationMask { + pub fn from_valid_orientations_idiom( + valid_orientations: ValidOrientations, + idiom: Idiom, + ) -> UIInterfaceOrientationMask { + match (valid_orientations, idiom) { + (ValidOrientations::LandscapeAndPortrait, Idiom::Phone) => { + UIInterfaceOrientationMask::AllButUpsideDown + } + (ValidOrientations::LandscapeAndPortrait, _) => UIInterfaceOrientationMask::All, + (ValidOrientations::Landscape, _) => UIInterfaceOrientationMask::Landscape, + (ValidOrientations::Portrait, Idiom::Phone) => UIInterfaceOrientationMask::Portrait, + (ValidOrientations::Portrait, _) => { + UIInterfaceOrientationMask::Portrait | UIInterfaceOrientationMask::PortraitUpsideDown + } + } + } +} + +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct UIRectEdge(NSUInteger); + +unsafe impl Encode for UIRectEdge { + const ENCODING: Encoding = NSUInteger::ENCODING; +} + +impl From for UIRectEdge { + fn from(screen_edge: ScreenEdge) -> UIRectEdge { + assert_eq!( + screen_edge.bits() & !ScreenEdge::ALL.bits(), + 0, + "invalid `ScreenEdge`" + ); + UIRectEdge(screen_edge.bits().into()) + } +} + +impl Into for UIRectEdge { + fn into(self) -> ScreenEdge { + let bits: u8 = self.0.try_into().expect("invalid `UIRectEdge`"); + ScreenEdge::from_bits(bits).expect("invalid `ScreenEdge`") + } +} + +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct UIScreenOverscanCompensation(NSInteger); + +unsafe impl Encode for UIScreenOverscanCompensation { + const ENCODING: Encoding = NSInteger::ENCODING; +} + +#[allow(dead_code)] +impl UIScreenOverscanCompensation { + pub const Scale: UIScreenOverscanCompensation = UIScreenOverscanCompensation(0); + pub const InsetBounds: UIScreenOverscanCompensation = UIScreenOverscanCompensation(1); + pub const None: UIScreenOverscanCompensation = UIScreenOverscanCompensation(2); +} + +#[allow(clippy::duplicated_attributes)] +#[link(name = "UIKit", kind = "framework")] +#[link(name = "CoreFoundation", kind = "framework")] +extern "C" { + pub static kCFRunLoopDefaultMode: CFRunLoopMode; + pub static kCFRunLoopCommonModes: CFRunLoopMode; + + pub fn UIApplicationMain( + argc: c_int, + argv: *const c_char, + principalClassName: id, + delegateClassName: id, + ) -> c_int; + + pub fn CFRunLoopGetMain() -> CFRunLoopRef; + pub fn CFRunLoopWakeUp(rl: CFRunLoopRef); + + pub fn CFRunLoopObserverCreate( + allocator: CFAllocatorRef, + activities: CFOptionFlags, + repeats: Boolean, + order: CFIndex, + callout: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, + ) -> CFRunLoopObserverRef; + pub fn CFRunLoopAddObserver( + rl: CFRunLoopRef, + observer: CFRunLoopObserverRef, + mode: CFRunLoopMode, + ); + + pub fn CFRunLoopTimerCreate( + allocator: CFAllocatorRef, + fireDate: CFAbsoluteTime, + interval: CFTimeInterval, + flags: CFOptionFlags, + order: CFIndex, + callout: CFRunLoopTimerCallBack, + context: *mut CFRunLoopTimerContext, + ) -> CFRunLoopTimerRef; + pub fn CFRunLoopAddTimer(rl: CFRunLoopRef, timer: CFRunLoopTimerRef, mode: CFRunLoopMode); + pub fn CFRunLoopTimerSetNextFireDate(timer: CFRunLoopTimerRef, fireDate: CFAbsoluteTime); + pub fn CFRunLoopTimerInvalidate(time: CFRunLoopTimerRef); + + pub fn CFRunLoopSourceCreate( + allocator: CFAllocatorRef, + order: CFIndex, + context: *mut CFRunLoopSourceContext, + ) -> CFRunLoopSourceRef; + pub fn CFRunLoopAddSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFRunLoopMode); + pub fn CFRunLoopSourceInvalidate(source: CFRunLoopSourceRef); + pub fn CFRunLoopSourceSignal(source: CFRunLoopSourceRef); + + pub fn CFAbsoluteTimeGetCurrent() -> CFAbsoluteTime; + pub fn CFRelease(cftype: *const c_void); +} + +pub type Boolean = u8; +pub enum CFAllocator {} +pub type CFAllocatorRef = *mut CFAllocator; +pub enum CFRunLoop {} +pub type CFRunLoopRef = *mut CFRunLoop; +pub type CFRunLoopMode = CFStringRef; +pub enum CFRunLoopObserver {} +pub type CFRunLoopObserverRef = *mut CFRunLoopObserver; +pub enum CFRunLoopTimer {} +pub type CFRunLoopTimerRef = *mut CFRunLoopTimer; +pub enum CFRunLoopSource {} +pub type CFRunLoopSourceRef = *mut CFRunLoopSource; +pub enum CFString {} +pub type CFStringRef = *const CFString; + +pub type CFHashCode = c_ulong; +pub type CFIndex = c_long; +pub type CFOptionFlags = c_ulong; +pub type CFRunLoopActivity = CFOptionFlags; + +pub type CFAbsoluteTime = CFTimeInterval; +pub type CFTimeInterval = f64; + +pub const kCFRunLoopEntry: CFRunLoopActivity = 0; +pub const kCFRunLoopBeforeWaiting: CFRunLoopActivity = 1 << 5; +pub const kCFRunLoopAfterWaiting: CFRunLoopActivity = 1 << 6; +pub const kCFRunLoopExit: CFRunLoopActivity = 1 << 7; + +pub type CFRunLoopObserverCallBack = + extern "C" fn(observer: CFRunLoopObserverRef, activity: CFRunLoopActivity, info: *mut c_void); +pub type CFRunLoopTimerCallBack = extern "C" fn(timer: CFRunLoopTimerRef, info: *mut c_void); + +pub enum CFRunLoopObserverContext {} +pub enum CFRunLoopTimerContext {} + +#[repr(C)] +pub struct CFRunLoopSourceContext { + pub version: CFIndex, + pub info: *mut c_void, + pub retain: Option *const c_void>, + pub release: Option, + pub copyDescription: Option CFStringRef>, + pub equal: Option Boolean>, + pub hash: Option CFHashCode>, + pub schedule: Option, + pub cancel: Option, + pub perform: Option, +} + +// This is named NSStringRust rather than NSString because the "Debug View Heirarchy" feature of +// Xcode requires a non-ambiguous reference to NSString for unclear reasons. This makes Xcode happy +// so please test if you change the name back to NSString. +pub trait NSStringRust: Sized { + unsafe fn alloc(_: Self) -> id { + msg_send![class!(NSString), alloc] + } + + unsafe fn initWithUTF8String_(self, c_string: *const c_char) -> id; + unsafe fn stringByAppendingString_(self, other: id) -> id; + unsafe fn init_str(self, string: &str) -> Self; + unsafe fn UTF8String(self) -> *const c_char; +} + +impl NSStringRust for id { + unsafe fn initWithUTF8String_(self, c_string: *const c_char) -> id { + msg_send![self, initWithUTF8String: c_string] + } + + unsafe fn stringByAppendingString_(self, other: id) -> id { + msg_send![self, stringByAppendingString: other] + } + + unsafe fn init_str(self, string: &str) -> id { + let cstring = CString::new(string).unwrap(); + self.initWithUTF8String_(cstring.as_ptr()) + } + + unsafe fn UTF8String(self) -> *const c_char { + msg_send![self, UTF8String] + } +} diff --git a/vendor/tao/src/platform_impl/ios/keycode.rs b/vendor/tao/src/platform_impl/ios/keycode.rs new file mode 100644 index 0000000000..e84dbac6c0 --- /dev/null +++ b/vendor/tao/src/platform_impl/ios/keycode.rs @@ -0,0 +1,13 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use crate::keyboard::{KeyCode, NativeKeyCode}; + +pub fn keycode_to_scancode(_code: KeyCode) -> Option { + None +} + +pub fn keycode_from_scancode(_scancode: u32) -> KeyCode { + KeyCode::Unidentified(NativeKeyCode::Unidentified) +} diff --git a/vendor/tao/src/platform_impl/ios/mod.rs b/vendor/tao/src/platform_impl/ios/mod.rs new file mode 100644 index 0000000000..2d8fda6bc6 --- /dev/null +++ b/vendor/tao/src/platform_impl/ios/mod.rs @@ -0,0 +1,123 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +//! iOS support +//! +//! # Building app +//! To build ios app you will need rustc built for this targets: +//! +//! - armv7-apple-ios +//! - armv7s-apple-ios +//! - i386-apple-ios +//! - aarch64-apple-ios +//! - x86_64-apple-ios +//! +//! Then +//! +//! ``` +//! cargo build --target=... +//! ``` +//! The simplest way to integrate your app into xcode environment is to build it +//! as a static library. Wrap your main function and export it. +//! +//! ```rust, ignore +//! #[no_mangle] +//! pub extern fn start_tao_app() { +//! start_inner() +//! } +//! +//! fn start_inner() { +//! ... +//! } +//! ``` +//! +//! Compile project and then drag resulting .a into Xcode project. Add tao.h to xcode. +//! +//! ```ignore +//! void start_tao_app(); +//! ``` +//! +//! Use start_tao_app inside your xcode's main function. +//! +//! +//! # App lifecycle and events +//! +//! iOS environment is very different from other platforms and you must be very +//! careful with it's events. Familiarize yourself with +//! [app lifecycle](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/). +//! +//! +//! This is how those event are represented in tao: +//! +//! - applicationDidBecomeActive is Resumed +//! - applicationWillResignActive is Suspended +//! - applicationWillTerminate is LoopDestroyed +//! +//! Keep in mind that after LoopDestroyed event is received every attempt to draw with +//! opengl will result in segfault. +//! +//! Also note that app may not receive the LoopDestroyed event if suspended; it might be SIGKILL'ed. + +// TODO: (mtak-) UIKit requires main thread for virtually all function/method calls. This could be +// worked around in the future by using GCD (grand central dispatch) and/or caching of values like +// window size/position. +macro_rules! assert_main_thread { + ($($t:tt)*) => { + let is_main_thread: bool = msg_send![class!(NSThread), isMainThread]; + if !is_main_thread { + panic!($($t)*); + } + }; +} + +mod app_state; +mod badge; +mod event_loop; +mod ffi; +mod keycode; +mod monitor; +mod view; +mod window; + +use std::fmt; + +pub(crate) use self::event_loop::PlatformSpecificEventLoopAttributes; +pub use self::{ + event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, + keycode::{keycode_from_scancode, keycode_to_scancode}, + monitor::{MonitorHandle, VideoMode}, + window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId}, +}; +pub(crate) use crate::icon::NoIcon as PlatformIcon; +pub(crate) use badge::set_badge_count; + +// todo: implement iOS keyboard event +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId { + uiscreen: ffi::id, +} + +impl DeviceId { + pub unsafe fn dummy() -> Self { + DeviceId { + uiscreen: std::ptr::null_mut(), + } + } +} + +unsafe impl Send for DeviceId {} +unsafe impl Sync for DeviceId {} + +#[non_exhaustive] +#[derive(Debug)] +pub enum OsError {} + +impl fmt::Display for OsError { + fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { + unreachable!() + } +} diff --git a/vendor/tao/src/platform_impl/ios/monitor.rs b/vendor/tao/src/platform_impl/ios/monitor.rs new file mode 100644 index 0000000000..cc1bafb136 --- /dev/null +++ b/vendor/tao/src/platform_impl/ios/monitor.rs @@ -0,0 +1,300 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + collections::{BTreeSet, VecDeque}, + fmt, + ops::{Deref, DerefMut}, +}; + +use crate::{ + dpi::{PhysicalPosition, PhysicalSize}, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform_impl::platform::{ + app_state, + ffi::{id, nil, CGFloat, CGRect, CGSize, NSInteger, NSUInteger}, + }, +}; + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct VideoMode { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, + pub(crate) screen_mode: NativeDisplayMode, + pub(crate) monitor: MonitorHandle, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct NativeDisplayMode(pub id); + +unsafe impl Send for NativeDisplayMode {} + +impl Drop for NativeDisplayMode { + fn drop(&mut self) { + unsafe { + let () = msg_send![self.0, release]; + } + } +} + +impl Clone for NativeDisplayMode { + fn clone(&self) -> Self { + unsafe { + let _: id = msg_send![self.0, retain]; + } + NativeDisplayMode(self.0) + } +} + +impl Clone for VideoMode { + fn clone(&self) -> VideoMode { + VideoMode { + size: self.size, + bit_depth: self.bit_depth, + refresh_rate: self.refresh_rate, + screen_mode: self.screen_mode.clone(), + monitor: self.monitor.clone(), + } + } +} + +impl VideoMode { + unsafe fn retained_new(uiscreen: id, screen_mode: id) -> VideoMode { + assert_main_thread!("`VideoMode` can only be created on the main thread on iOS"); + let os_capabilities = app_state::os_capabilities(); + let refresh_rate: NSInteger = if os_capabilities.maximum_frames_per_second { + msg_send![uiscreen, maximumFramesPerSecond] + } else { + // https://developer.apple.com/library/archive/technotes/tn2460/_index.html + // https://en.wikipedia.org/wiki/IPad_Pro#Model_comparison + // + // All iOS devices support 60 fps, and on devices where `maximumFramesPerSecond` is not + // supported, they are all guaranteed to have 60hz refresh rates. This does not + // correctly handle external displays. ProMotion displays support 120fps, but they were + // introduced at the same time as the `maximumFramesPerSecond` API. + // + // FIXME: earlier OSs could calculate the refresh rate using + // `-[CADisplayLink duration]`. + os_capabilities.maximum_frames_per_second_err_msg("defaulting to 60 fps"); + 60 + }; + let size: CGSize = msg_send![screen_mode, size]; + let screen_mode: id = msg_send![screen_mode, retain]; + let screen_mode = NativeDisplayMode(screen_mode); + VideoMode { + size: (size.width as u32, size.height as u32), + bit_depth: 32, + refresh_rate: refresh_rate as u16, + screen_mode, + monitor: MonitorHandle::retained_new(uiscreen), + } + } + + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: self.monitor.clone(), + } + } +} + +#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Inner { + uiscreen: id, +} + +impl Drop for Inner { + fn drop(&mut self) { + unsafe { + let () = msg_send![self.uiscreen, release]; + } + } +} + +#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct MonitorHandle { + inner: Inner, +} + +impl Deref for MonitorHandle { + type Target = Inner; + + fn deref(&self) -> &Inner { + unsafe { + assert_main_thread!("`MonitorHandle` methods can only be run on the main thread on iOS"); + } + &self.inner + } +} + +impl DerefMut for MonitorHandle { + fn deref_mut(&mut self) -> &mut Inner { + unsafe { + assert_main_thread!("`MonitorHandle` methods can only be run on the main thread on iOS"); + } + &mut self.inner + } +} + +unsafe impl Send for MonitorHandle {} +unsafe impl Sync for MonitorHandle {} + +impl Clone for MonitorHandle { + fn clone(&self) -> MonitorHandle { + MonitorHandle::retained_new(self.uiscreen) + } +} + +impl Drop for MonitorHandle { + fn drop(&mut self) { + unsafe { + assert_main_thread!("`MonitorHandle` can only be dropped on the main thread on iOS"); + } + } +} + +impl fmt::Debug for MonitorHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[derive(Debug)] + #[allow(unused)] + struct MonitorHandle { + name: Option, + size: PhysicalSize, + position: PhysicalPosition, + scale_factor: f64, + } + + let monitor_id_proxy = MonitorHandle { + name: self.name(), + size: self.size(), + position: self.position(), + scale_factor: self.scale_factor(), + }; + + monitor_id_proxy.fmt(f) + } +} + +impl MonitorHandle { + pub fn retained_new(uiscreen: id) -> MonitorHandle { + unsafe { + assert_main_thread!("`MonitorHandle` can only be cloned on the main thread on iOS"); + let _: id = msg_send![uiscreen, retain]; + } + MonitorHandle { + inner: Inner { uiscreen }, + } + } +} + +impl Inner { + pub fn name(&self) -> Option { + unsafe { + let main = main_uiscreen(); + if self.uiscreen == main.uiscreen { + Some("Primary".to_string()) + } else if self.uiscreen == mirrored_uiscreen(&main).uiscreen { + Some("Mirrored".to_string()) + } else { + uiscreens() + .iter() + .position(|rhs| rhs.uiscreen == self.uiscreen) + .map(|idx| idx.to_string()) + } + } + } + + pub fn size(&self) -> PhysicalSize { + unsafe { + let bounds: CGRect = msg_send![self.ui_screen(), nativeBounds]; + PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32) + } + } + + pub fn position(&self) -> PhysicalPosition { + unsafe { + let bounds: CGRect = msg_send![self.ui_screen(), nativeBounds]; + (bounds.origin.x as f64, bounds.origin.y as f64).into() + } + } + + pub fn scale_factor(&self) -> f64 { + unsafe { + let scale: CGFloat = msg_send![self.ui_screen(), nativeScale]; + scale as f64 + } + } + + pub fn video_modes(&self) -> impl Iterator { + let mut modes = BTreeSet::new(); + unsafe { + let available_modes: id = msg_send![self.uiscreen, availableModes]; + let available_mode_count: NSUInteger = msg_send![available_modes, count]; + + for i in 0..available_mode_count { + let mode: id = msg_send![available_modes, objectAtIndex: i]; + modes.insert(RootVideoMode { + video_mode: VideoMode::retained_new(self.uiscreen, mode), + }); + } + } + + modes.into_iter() + } +} + +// MonitorHandleExtIOS +impl Inner { + pub fn ui_screen(&self) -> id { + self.uiscreen + } + + pub fn preferred_video_mode(&self) -> RootVideoMode { + unsafe { + let mode: id = msg_send![self.uiscreen, preferredMode]; + RootVideoMode { + video_mode: VideoMode::retained_new(self.uiscreen, mode), + } + } + } +} + +// requires being run on main thread +pub unsafe fn main_uiscreen() -> MonitorHandle { + let uiscreen: id = msg_send![class!(UIScreen), mainScreen]; + MonitorHandle::retained_new(uiscreen) +} + +// requires being run on main thread +unsafe fn mirrored_uiscreen(monitor: &MonitorHandle) -> MonitorHandle { + let uiscreen: id = msg_send![monitor.uiscreen, mirroredScreen]; + MonitorHandle::retained_new(uiscreen) +} + +// requires being run on main thread +pub unsafe fn uiscreens() -> VecDeque { + let screens: id = msg_send![class!(UIScreen), screens]; + let count: NSUInteger = msg_send![screens, count]; + let mut result = VecDeque::with_capacity(count as _); + let screens_enum: id = msg_send![screens, objectEnumerator]; + loop { + let screen: id = msg_send![screens_enum, nextObject]; + if screen == nil { + break result; + } + result.push_back(MonitorHandle::retained_new(screen)); + } +} diff --git a/vendor/tao/src/platform_impl/ios/view.rs b/vendor/tao/src/platform_impl/ios/view.rs new file mode 100644 index 0000000000..8150be1f9d --- /dev/null +++ b/vendor/tao/src/platform_impl/ios/view.rs @@ -0,0 +1,696 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + collections::HashMap, + ffi::{c_char, CStr, CString}, +}; + +use objc2::runtime::{AnyClass as Class, AnyObject as Object, ClassBuilder as ClassDecl, Sel}; + +use crate::{ + dpi::PhysicalPosition, + event::{DeviceId as RootDeviceId, Event, Force, Touch, TouchPhase, WindowEvent}, + platform::ios::MonitorHandleExtIOS, + platform_impl::platform::{ + app_state::{self, OSCapabilities}, + event_loop::{self, EventProxy, EventWrapper}, + ffi::{ + id, nil, CGFloat, CGPoint, CGRect, UIForceTouchCapability, UIInterfaceOrientationMask, + UIRectEdge, UITouchPhase, UITouchType, BOOL, NO, YES, + }, + window::PlatformSpecificWindowBuilderAttributes, + DeviceId, + }, + window::{Fullscreen, WindowAttributes, WindowId as RootWindowId}, +}; + +macro_rules! add_property { + ( + $decl:ident, + $name:ident: $t:ty, + $setter_name:ident: |$object:ident| $after_set:expr, + $getter_name:ident, + ) => { + add_property!( + $decl, + $name: $t, + $setter_name: true, |_, _|{}; |$object| $after_set, + $getter_name, + ) + }; + ( + $decl:ident, + $name:ident: $t:ty, + $setter_name:ident: $capability:expr, $err:expr; |$object:ident| $after_set:expr, + $getter_name:ident, + ) => { + { + const VAR_NAME: &'static str = concat!("_", stringify!($name)); + $decl.add_ivar::<$t>(&CString::new(VAR_NAME).unwrap()); + let setter = if $capability { + #[allow(non_snake_case)] + extern "C" fn $setter_name($object: &mut Object, _: Sel, value: $t) { + #[allow(deprecated)] // TODO: define_class! + unsafe { + *$object.get_mut_ivar::<$t>(VAR_NAME) = value; + } + $after_set + } + $setter_name + } else { + #[allow(non_snake_case)] + extern "C" fn $setter_name($object: &mut Object, _: Sel, value: $t) { + #[allow(deprecated)] // TODO: define_class! + unsafe { + *$object.get_mut_ivar::<$t>(VAR_NAME) = value; + } + $err(&app_state::os_capabilities(), "ignoring") + } + $setter_name + }; + #[allow(non_snake_case)] + extern "C" fn $getter_name($object: &Object, _: Sel) -> $t { + #[allow(deprecated)] // TODO: define_class! + unsafe { *$object.get_ivar::<$t>(VAR_NAME) } + } + $decl.add_method( + sel!($setter_name:), + setter as extern "C" fn(_, _, _), + ); + $decl.add_method( + sel!($getter_name), + $getter_name as extern "C" fn(_, _) -> _, + ); + } + }; +} + +// requires main thread +unsafe fn get_view_class(root_view_class: &'static Class) -> &'static Class { + static mut CLASSES: Option> = None; + static mut ID: usize = 0; + + if CLASSES.is_none() { + CLASSES = Some(HashMap::default()); + } + + let classes = CLASSES.as_mut().unwrap(); + + classes.entry(root_view_class).or_insert_with(move || { + let uiview_class = class!(UIView); + let is_uiview: bool = msg_send![root_view_class, isSubclassOfClass: uiview_class]; + assert!(is_uiview, "`root_view_class` must inherit from `UIView`"); + + extern "C" fn draw_rect(object: &Object, _: Sel, rect: CGRect) { + unsafe { + let window: id = msg_send![object, window]; + assert!(!window.is_null()); + app_state::handle_nonuser_events( + std::iter::once(EventWrapper::StaticEvent(Event::RedrawRequested( + RootWindowId(window.into()), + ))) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::RedrawEventsCleared, + ))), + ); + let superclass: &'static Class = msg_send![object, superclass]; + let () = msg_send![super(object, superclass), drawRect: rect]; + } + } + + extern "C" fn layout_subviews(object: &Object, _: Sel) { + unsafe { + let superclass: &'static Class = msg_send![object, superclass]; + let () = msg_send![super(object, superclass), layoutSubviews]; + + let window: id = msg_send![object, window]; + assert!(!window.is_null()); + let window_bounds: CGRect = msg_send![window, bounds]; + let screen: id = msg_send![window, screen]; + let screen_space: id = msg_send![screen, coordinateSpace]; + let screen_frame: CGRect = + msg_send![object, convertRect:window_bounds, toCoordinateSpace:screen_space]; + let scale_factor: CGFloat = msg_send![screen, scale]; + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + } + .to_physical(scale_factor.into()); + + // If the app is started in landscape, the view frame and window bounds can be mismatched. + // The view frame will be in portrait and the window bounds in landscape. So apply the + // window bounds to the view frame to make it consistent. + let view_frame: CGRect = msg_send![object, frame]; + if view_frame != window_bounds { + let () = msg_send![object, setFrame: window_bounds]; + } + + app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::Resized(size), + })); + } + } + + extern "C" fn set_content_scale_factor( + object: &Object, + _: Sel, + untrusted_scale_factor: CGFloat, + ) { + unsafe { + let superclass: &'static Class = msg_send![object, superclass]; + let () = msg_send![ + super(object, superclass), + setContentScaleFactor: untrusted_scale_factor + ]; + + let window: id = msg_send![object, window]; + // `window` is null when `setContentScaleFactor` is invoked prior to `[UIWindow + // makeKeyAndVisible]` at window creation time (either manually or internally by + // UIKit when the `UIView` is first created), in which case we send no events here + if window.is_null() { + return; + } + // `setContentScaleFactor` may be called with a value of 0, which means "reset the + // content scale factor to a device-specific default value", so we can't use the + // parameter here. We can query the actual factor using the getter + let scale_factor: CGFloat = msg_send![object, contentScaleFactor]; + assert!( + !scale_factor.is_nan() + && scale_factor.is_finite() + && scale_factor.is_sign_positive() + && scale_factor > 0.0, + "invalid scale_factor set on UIView", + ); + let scale_factor: f64 = scale_factor.into(); + let bounds: CGRect = msg_send![object, bounds]; + let screen: id = msg_send![window, screen]; + let screen_space: id = msg_send![screen, coordinateSpace]; + let screen_frame: CGRect = + msg_send![object, convertRect:bounds, toCoordinateSpace:screen_space]; + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width as _, + height: screen_frame.size.height as _, + }; + app_state::handle_nonuser_events( + std::iter::once(EventWrapper::EventProxy(EventProxy::DpiChangedProxy { + window_id: window, + scale_factor, + suggested_size: size, + })) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), + ); + } + } + + extern "C" fn handle_touches(object: &Object, _: Sel, touches: id, _: id) { + unsafe { + let window: id = msg_send![object, window]; + assert!(!window.is_null()); + let uiscreen: id = msg_send![window, screen]; + let touches_enum: id = msg_send![touches, objectEnumerator]; + let mut touch_events = Vec::new(); + let os_supports_force = app_state::os_capabilities().force_touch; + loop { + let touch: id = msg_send![touches_enum, nextObject]; + if touch == nil { + break; + } + let logical_location: CGPoint = msg_send![touch, locationInView: nil]; + let touch_type: UITouchType = msg_send![touch, type]; + let force = if os_supports_force { + let trait_collection: id = msg_send![object, traitCollection]; + let touch_capability: UIForceTouchCapability = + msg_send![trait_collection, forceTouchCapability]; + // Both the OS _and_ the device need to be checked for force touch support. + if touch_capability == UIForceTouchCapability::Available { + let force: CGFloat = msg_send![touch, force]; + let max_possible_force: CGFloat = msg_send![touch, maximumPossibleForce]; + let altitude_angle: Option = if touch_type == UITouchType::Pencil { + let angle: CGFloat = msg_send![touch, altitudeAngle]; + Some(angle as _) + } else { + None + }; + Some(Force::Calibrated { + force: force as _, + max_possible_force: max_possible_force as _, + altitude_angle, + }) + } else { + None + } + } else { + None + }; + let touch_id = touch as u64; + let phase: UITouchPhase = msg_send![touch, phase]; + let phase = match phase { + UITouchPhase::Began => TouchPhase::Started, + UITouchPhase::Moved => TouchPhase::Moved, + // 2 is UITouchPhase::Stationary and is not expected here + UITouchPhase::Ended => TouchPhase::Ended, + UITouchPhase::Cancelled => TouchPhase::Cancelled, + _ => panic!("unexpected touch phase: {:?}", phase as i32), + }; + + let physical_location = { + let scale_factor: CGFloat = msg_send![object, contentScaleFactor]; + PhysicalPosition::from_logical::<(f64, f64), f64>( + (logical_location.x as _, logical_location.y as _), + scale_factor, + ) + }; + touch_events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::Touch(Touch { + device_id: RootDeviceId(DeviceId { uiscreen }), + id: touch_id, + location: physical_location, + force, + phase, + }), + })); + } + app_state::handle_nonuser_events(touch_events); + } + } + + let mut decl = ClassDecl::new( + &CString::new(format!("TaoUIView{}", ID)).unwrap(), + root_view_class, + ) + .expect("Failed to declare class `TaoUIView`"); + ID += 1; + decl.add_method(sel!(drawRect:), draw_rect as extern "C" fn(_, _, _)); + decl.add_method(sel!(layoutSubviews), layout_subviews as extern "C" fn(_, _)); + decl.add_method( + sel!(setContentScaleFactor:), + set_content_scale_factor as extern "C" fn(_, _, _), + ); + + decl.add_method( + sel!(touchesBegan:withEvent:), + handle_touches as extern "C" fn(_, _, _, _), + ); + decl.add_method( + sel!(touchesMoved:withEvent:), + handle_touches as extern "C" fn(_, _, _, _), + ); + decl.add_method( + sel!(touchesEnded:withEvent:), + handle_touches as extern "C" fn(_, _, _, _), + ); + decl.add_method( + sel!(touchesCancelled:withEvent:), + handle_touches as extern "C" fn(_, _, _, _), + ); + + decl.register() + }) +} + +// requires main thread +unsafe fn get_view_controller_class() -> &'static Class { + static mut CLASS: Option<&'static Class> = None; + if CLASS.is_none() { + let os_capabilities = app_state::os_capabilities(); + + let uiviewcontroller_class = class!(UIViewController); + + extern "C" fn should_autorotate(_: &Object, _: Sel) -> BOOL { + YES + } + + let mut decl = ClassDecl::new( + CStr::from_bytes_with_nul(b"TaoUIViewController\0").unwrap(), + uiviewcontroller_class, + ) + .expect("Failed to declare class `TaoUIViewController`"); + decl.add_method( + sel!(shouldAutorotate), + should_autorotate as extern "C" fn(_, _) -> _, + ); + add_property! { + decl, + prefers_status_bar_hidden: BOOL, + setPrefersStatusBarHidden: |object| { + unsafe { + let () = msg_send![object, setNeedsStatusBarAppearanceUpdate]; + } + }, + prefersStatusBarHidden, + } + add_property! { + decl, + prefers_home_indicator_auto_hidden: BOOL, + setPrefersHomeIndicatorAutoHidden: + os_capabilities.home_indicator_hidden, + OSCapabilities::home_indicator_hidden_err_msg; + |object| { + unsafe { + let () = msg_send![object, setNeedsUpdateOfHomeIndicatorAutoHidden]; + } + }, + prefersHomeIndicatorAutoHidden, + } + add_property! { + decl, + supported_orientations: UIInterfaceOrientationMask, + setSupportedInterfaceOrientations: |object| { + unsafe { + let () = msg_send![class!(UIViewController), attemptRotationToDeviceOrientation]; + } + }, + supportedInterfaceOrientations, + } + add_property! { + decl, + preferred_screen_edges_deferring_system_gestures: UIRectEdge, + setPreferredScreenEdgesDeferringSystemGestures: + os_capabilities.defer_system_gestures, + OSCapabilities::defer_system_gestures_err_msg; + |object| { + unsafe { + let () = msg_send![object, setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; + } + }, + preferredScreenEdgesDeferringSystemGestures, + } + CLASS = Some(decl.register()); + } + CLASS.unwrap() +} + +// requires main thread +unsafe fn get_window_class() -> &'static Class { + static mut CLASS: Option<&'static Class> = None; + if CLASS.is_none() { + let uiwindow_class = class!(UIWindow); + + extern "C" fn become_key_window(object: &Object, _: Sel) { + unsafe { + app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(object.into()), + event: WindowEvent::Focused(true), + })); + let () = msg_send![super(object, class!(UIWindow)), becomeKeyWindow]; + } + } + + extern "C" fn resign_key_window(object: &Object, _: Sel) { + unsafe { + app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(object.into()), + event: WindowEvent::Focused(false), + })); + let () = msg_send![super(object, class!(UIWindow)), resignKeyWindow]; + } + } + + let mut decl = ClassDecl::new( + CStr::from_bytes_with_nul(b"TaoUIWindow\0").unwrap(), + uiwindow_class, + ) + .expect("Failed to declare class `TaoUIWindow`"); + decl.add_method( + sel!(becomeKeyWindow), + become_key_window as extern "C" fn(_, _), + ); + decl.add_method( + sel!(resignKeyWindow), + resign_key_window as extern "C" fn(_, _), + ); + + CLASS = Some(decl.register()); + } + CLASS.unwrap() +} + +// requires main thread +pub unsafe fn create_view( + _window_attributes: &WindowAttributes, + platform_attributes: &PlatformSpecificWindowBuilderAttributes, + frame: CGRect, +) -> id { + let class = get_view_class(platform_attributes.root_view_class); + + let view: id = msg_send![class, alloc]; + assert!(!view.is_null(), "Failed to create `UIView` instance"); + let view: id = msg_send![view, initWithFrame: frame]; + assert!(!view.is_null(), "Failed to initialize `UIView` instance"); + let () = msg_send![view, setMultipleTouchEnabled: YES]; + if let Some(scale_factor) = platform_attributes.scale_factor { + let () = msg_send![view, setContentScaleFactor: scale_factor as CGFloat]; + } + + view +} + +// requires main thread +pub unsafe fn create_view_controller( + _window_attributes: &WindowAttributes, + platform_attributes: &PlatformSpecificWindowBuilderAttributes, + view: id, +) -> id { + let class = get_view_controller_class(); + + let view_controller: id = msg_send![class, alloc]; + assert!( + !view_controller.is_null(), + "Failed to create `UIViewController` instance" + ); + let view_controller: id = msg_send![view_controller, init]; + assert!( + !view_controller.is_null(), + "Failed to initialize `UIViewController` instance" + ); + let status_bar_hidden = if platform_attributes.prefers_status_bar_hidden { + YES + } else { + NO + }; + let idiom = event_loop::get_idiom(); + let supported_orientations = UIInterfaceOrientationMask::from_valid_orientations_idiom( + platform_attributes.valid_orientations, + idiom, + ); + let prefers_home_indicator_hidden = if platform_attributes.prefers_home_indicator_hidden { + YES + } else { + NO + }; + let edges: UIRectEdge = platform_attributes + .preferred_screen_edges_deferring_system_gestures + .into(); + let () = msg_send![ + view_controller, + setPrefersStatusBarHidden: status_bar_hidden + ]; + let () = msg_send![ + view_controller, + setSupportedInterfaceOrientations: supported_orientations + ]; + let () = msg_send![ + view_controller, + setPrefersHomeIndicatorAutoHidden: prefers_home_indicator_hidden + ]; + let () = msg_send![ + view_controller, + setPreferredScreenEdgesDeferringSystemGestures: edges + ]; + let () = msg_send![view_controller, setView: view]; + view_controller +} + +// requires main thread +pub unsafe fn create_window( + window_attributes: &WindowAttributes, + _platform_attributes: &PlatformSpecificWindowBuilderAttributes, + frame: CGRect, + view_controller: id, +) -> id { + let class = get_window_class(); + + let window: id = msg_send![class, alloc]; + assert!(!window.is_null(), "Failed to create `UIWindow` instance"); + let window: id = msg_send![window, initWithFrame: frame]; + assert!( + !window.is_null(), + "Failed to initialize `UIWindow` instance" + ); + let () = msg_send![window, setRootViewController: view_controller]; + match window_attributes.fullscreen { + Some(Fullscreen::Exclusive(ref video_mode)) => { + let uiscreen = video_mode.monitor().ui_screen() as id; + let () = msg_send![uiscreen, setCurrentMode: video_mode.video_mode.screen_mode.0]; + msg_send![window, setScreen:video_mode.monitor().ui_screen()] + } + Some(Fullscreen::Borderless(ref monitor)) => { + let uiscreen: id = match &monitor { + Some(monitor) => monitor.ui_screen() as id, + None => { + let uiscreen: id = msg_send![window, screen]; + uiscreen + } + }; + + msg_send![window, setScreen: uiscreen] + } + None => (), + } + + window +} + +pub fn create_delegate_class() { + extern "C" fn did_finish_launching(_: &Object, _: Sel, _: id, _: id) -> BOOL { + unsafe { + app_state::did_finish_launching(); + } + YES + } + + fn handle_deep_link(url: id) { + unsafe { + let absolute_url: id = msg_send![url, absoluteString]; + let bytes = { + let bytes: *const c_char = msg_send![absolute_url, UTF8String]; + bytes as *const u8 + }; + + // 4 represents utf8 encoding + let len = msg_send![absolute_url, lengthOfBytesUsingEncoding: 4]; + let bytes = std::slice::from_raw_parts(bytes, len); + + let url = url::Url::parse(std::str::from_utf8(bytes).unwrap()).unwrap(); + + app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Opened { urls: vec![url] })); + } + } + + // custom URL schemes + // https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app + extern "C" fn application_open_url( + _self: &Object, + _cmd: Sel, + _app: id, + url: id, + _options: id, + ) -> BOOL { + handle_deep_link(url); + + YES + } + + // universal links + // https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app + extern "C" fn application_continue( + _: &Object, + _: Sel, + _application: id, + user_activity: id, + _restoration_handler: id, + ) -> BOOL { + unsafe { + let webpage_url: id = msg_send![user_activity, webpageURL]; + if webpage_url == nil { + return NO; + } + + handle_deep_link(webpage_url); + + YES + } + } + + extern "C" fn did_become_active(_: &Object, _: Sel, _: id) { + unsafe { app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Resumed)) } + } + + extern "C" fn will_resign_active(_: &Object, _: Sel, _: id) { + unsafe { app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Suspended)) } + } + + extern "C" fn will_enter_foreground(_: &Object, _: Sel, _: id) {} + extern "C" fn did_enter_background(_: &Object, _: Sel, _: id) {} + + extern "C" fn will_terminate(_: &Object, _: Sel, _: id) { + unsafe { + let app: id = msg_send![class!(UIApplication), sharedApplication]; + let windows: id = msg_send![app, windows]; + let windows_enum: id = msg_send![windows, objectEnumerator]; + let mut events = Vec::new(); + loop { + let window: id = msg_send![windows_enum, nextObject]; + if window == nil { + break; + } + let is_tao_window: bool = msg_send![window, isKindOfClass: class!(TaoUIWindow)]; + if is_tao_window { + events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::Destroyed, + })); + } + } + app_state::handle_nonuser_events(events); + app_state::terminated(); + } + } + + let ui_responder = class!(UIResponder); + let mut decl = ClassDecl::new( + CStr::from_bytes_with_nul(b"AppDelegate\0").unwrap(), + ui_responder, + ) + .expect("Failed to declare class `AppDelegate`"); + + unsafe { + decl.add_method( + sel!(application:didFinishLaunchingWithOptions:), + did_finish_launching as extern "C" fn(_, _, _, _) -> _, + ); + + decl.add_method( + sel!(application:openURL:options:), + application_open_url as extern "C" fn(_, _, _, _, _) -> _, + ); + + decl.add_method( + sel!(application:continueUserActivity:restorationHandler:), + application_continue as extern "C" fn(_, _, _, _, _) -> _, + ); + + decl.add_method( + sel!(applicationDidBecomeActive:), + did_become_active as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(applicationWillResignActive:), + will_resign_active as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(applicationWillEnterForeground:), + will_enter_foreground as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(applicationDidEnterBackground:), + did_enter_background as extern "C" fn(_, _, _), + ); + + decl.add_method( + sel!(applicationWillTerminate:), + will_terminate as extern "C" fn(_, _, _), + ); + + decl.register(); + } +} diff --git a/vendor/tao/src/platform_impl/ios/window.rs b/vendor/tao/src/platform_impl/ios/window.rs new file mode 100644 index 0000000000..812c2b8baa --- /dev/null +++ b/vendor/tao/src/platform_impl/ios/window.rs @@ -0,0 +1,790 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + collections::VecDeque, + ops::{Deref, DerefMut}, +}; + +use objc2::runtime::{AnyClass, AnyObject}; + +use crate::{ + dpi::{self, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, + error::{ExternalError, NotSupportedError, OsError as RootOsError}, + event::{Event, WindowEvent}, + icon::Icon, + monitor::MonitorHandle as RootMonitorHandle, + platform::ios::{MonitorHandleExtIOS, ScreenEdge, ValidOrientations}, + platform_impl::platform::{ + app_state, + event_loop::{self, EventProxy, EventWrapper}, + ffi::{ + id, CGFloat, CGPoint, CGRect, CGSize, UIEdgeInsets, UIInterfaceOrientationMask, UIRectEdge, + UIScreenOverscanCompensation, NO, YES, + }, + monitor, set_badge_count, view, EventLoopWindowTarget, MonitorHandle, + }, + window::{ + CursorIcon, Fullscreen, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowId as RootWindowId, WindowSizeConstraints, + }, +}; + +pub struct Inner { + pub window: id, + pub view_controller: id, + pub view: id, + gl_or_metal_backed: bool, +} + +impl Drop for Inner { + fn drop(&mut self) { + unsafe { + let () = msg_send![self.view, release]; + let () = msg_send![self.view_controller, release]; + let () = msg_send![self.window, release]; + } + } +} + +impl Inner { + pub fn set_title(&self, _title: &str) { + debug!("`Window::set_title` is ignored on iOS") + } + + pub fn title(&self) -> String { + String::new() + } + pub fn set_visible(&self, visible: bool) { + match visible { + true => unsafe { + let () = msg_send![self.window, setHidden: NO]; + }, + false => unsafe { + let () = msg_send![self.window, setHidden: YES]; + }, + } + } + + pub fn set_focus(&self) { + //FIXME: implementation goes here + warn!("set_focus not yet implemented on iOS"); + } + + pub fn set_focusable(&self, _focusable: bool) { + warn!("set_focusable not yet implemented on iOS"); + } + + pub fn is_focused(&self) -> bool { + warn!("`Window::is_focused` is ignored on iOS"); + false + } + + pub fn is_always_on_top(&self) -> bool { + log::warn!("`Window::is_always_on_top` is ignored on iOS"); + false + } + + pub fn request_redraw(&self) { + unsafe { + if self.gl_or_metal_backed { + // `setNeedsDisplay` does nothing on UIViews which are directly backed by CAEAGLLayer or CAMetalLayer. + // Ordinarily the OS sets up a bunch of UIKit state before calling drawRect: on a UIView, but when using + // raw or gl/metal for drawing this work is completely avoided. + // + // The docs for `setNeedsDisplay` don't mention `CAMetalLayer`; however, this has been confirmed via + // testing. + // + // https://developer.apple.com/documentation/uikit/uiview/1622437-setneedsdisplay?language=objc + app_state::queue_gl_or_metal_redraw(self.window); + } else { + let () = msg_send![self.view, setNeedsDisplay]; + } + } + } + + pub fn inner_position(&self) -> Result, NotSupportedError> { + unsafe { + let safe_area = self.safe_area_screen_space(); + let position = LogicalPosition { + x: safe_area.origin.x as f64, + y: safe_area.origin.y as f64, + }; + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) + } + } + + pub fn outer_position(&self) -> Result, NotSupportedError> { + unsafe { + let screen_frame = self.screen_frame(); + let position = LogicalPosition { + x: screen_frame.origin.x as f64, + y: screen_frame.origin.y as f64, + }; + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) + } + } + + pub fn set_outer_position(&self, physical_position: Position) { + unsafe { + let scale_factor = self.scale_factor(); + let position = physical_position.to_logical::(scale_factor); + let screen_frame = self.screen_frame(); + let new_screen_frame = CGRect { + origin: CGPoint { + x: position.x as _, + y: position.y as _, + }, + size: screen_frame.size, + }; + let bounds = self.from_screen_space(new_screen_frame); + let () = msg_send![self.window, setBounds: bounds]; + } + } + + pub fn inner_size(&self) -> PhysicalSize { + unsafe { + let scale_factor = self.scale_factor(); + let safe_area = self.safe_area_screen_space(); + let size = LogicalSize { + width: safe_area.size.width as f64, + height: safe_area.size.height as f64, + }; + size.to_physical(scale_factor) + } + } + + pub fn outer_size(&self) -> PhysicalSize { + unsafe { + let scale_factor = self.scale_factor(); + let screen_frame = self.screen_frame(); + let size = LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + }; + size.to_physical(scale_factor) + } + } + + pub fn set_inner_size(&self, _size: Size) { + warn!("not clear what `Window::set_inner_size` means on iOS"); + } + + pub fn set_min_inner_size(&self, _: Option) { + warn!("`Window::set_min_inner_size` is ignored on iOS") + } + pub fn set_max_inner_size(&self, _: Option) { + warn!("`Window::set_max_inner_size` is ignored on iOS") + } + pub fn set_inner_size_constraints(&self, _: WindowSizeConstraints) { + warn!("`Window::set_inner_size_constraints` is ignored on iOS") + } + + pub fn set_resizable(&self, _resizable: bool) { + warn!("`Window::set_resizable` is ignored on iOS") + } + + pub fn set_minimizable(&self, _minimizable: bool) { + warn!("`Window::set_minimizable` is ignored on iOS") + } + + pub fn set_maximizable(&self, _maximizable: bool) { + warn!("`Window::set_maximizable` is ignored on iOS") + } + + pub fn set_closable(&self, _closable: bool) { + warn!("`Window::set_closable` is ignored on iOS") + } + + pub fn scale_factor(&self) -> f64 { + unsafe { + let hidpi: CGFloat = msg_send![self.view, contentScaleFactor]; + hidpi as _ + } + } + + pub fn set_cursor_icon(&self, _cursor: CursorIcon) { + debug!("`Window::set_cursor_icon` ignored on iOS") + } + + pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn set_cursor_grab(&self, _grab: bool) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn set_cursor_visible(&self, _visible: bool) { + debug!("`Window::set_cursor_visible` is ignored on iOS") + } + + pub fn cursor_position(&self) -> Result, ExternalError> { + debug!("`Window::cursor_position` is ignored on iOS"); + Ok((0, 0).into()) + } + + pub fn drag_window(&self) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn set_ignore_cursor_events(&self, _ignore: bool) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn set_minimized(&self, _minimized: bool) { + warn!("`Window::set_minimized` is ignored on iOS") + } + + pub fn set_maximized(&self, _maximized: bool) { + warn!("`Window::set_maximized` is ignored on iOS") + } + + pub fn is_maximized(&self) -> bool { + warn!("`Window::is_maximized` is ignored on iOS"); + false + } + + pub fn is_minimized(&self) -> bool { + warn!("`Window::is_minimized` is ignored on iOS"); + false + } + + pub fn is_visible(&self) -> bool { + log::warn!("`Window::is_visible` is ignored on iOS"); + false + } + + pub fn is_resizable(&self) -> bool { + warn!("`Window::is_resizable` is ignored on iOS"); + false + } + + pub fn is_minimizable(&self) -> bool { + warn!("`Window::is_minimizable` is ignored on iOS"); + false + } + + pub fn is_maximizable(&self) -> bool { + warn!("`Window::is_maximizable` is ignored on iOS"); + false + } + + pub fn is_closable(&self) -> bool { + warn!("`Window::is_closable` is ignored on iOS"); + false + } + + pub fn is_decorated(&self) -> bool { + warn!("`Window::is_decorated` is ignored on iOS"); + false + } + + pub fn set_fullscreen(&self, monitor: Option) { + unsafe { + let uiscreen = match monitor { + Some(Fullscreen::Exclusive(video_mode)) => { + let uiscreen = video_mode.video_mode.monitor.ui_screen() as id; + let () = msg_send![uiscreen, setCurrentMode: video_mode.video_mode.screen_mode.0]; + uiscreen + } + Some(Fullscreen::Borderless(monitor)) => monitor + .unwrap_or_else(|| self.current_monitor_inner()) + .ui_screen() as id, + None => { + warn!("`Window::set_fullscreen(None)` ignored on iOS"); + return; + } + }; + + // this is pretty slow on iOS, so avoid doing it if we can + let current: id = msg_send![self.window, screen]; + if uiscreen != current { + let () = msg_send![self.window, setScreen: uiscreen]; + } + + let bounds: CGRect = msg_send![uiscreen, bounds]; + let () = msg_send![self.window, setFrame: bounds]; + + // For external displays, we must disable overscan compensation or + // the displayed image will have giant black bars surrounding it on + // each side + let () = msg_send![ + uiscreen, + setOverscanCompensation: UIScreenOverscanCompensation::None + ]; + } + } + + pub fn fullscreen(&self) -> Option { + unsafe { + let monitor = self.current_monitor_inner(); + let uiscreen = monitor.inner.ui_screen(); + let screen_space_bounds = self.screen_frame(); + let screen_bounds: CGRect = msg_send![uiscreen, bounds]; + + // TODO: track fullscreen instead of relying on brittle float comparisons + if screen_space_bounds.origin.x == screen_bounds.origin.x + && screen_space_bounds.origin.y == screen_bounds.origin.y + && screen_space_bounds.size.width == screen_bounds.size.width + && screen_space_bounds.size.height == screen_bounds.size.height + { + Some(Fullscreen::Borderless(Some(monitor))) + } else { + None + } + } + } + + pub fn set_decorations(&self, _decorations: bool) { + warn!("`Window::set_decorations` is ignored on iOS") + } + + pub fn set_always_on_bottom(&self, _always_on_bottom: bool) { + warn!("`Window::set_always_on_bottom` is ignored on iOS") + } + + pub fn set_always_on_top(&self, _always_on_top: bool) { + warn!("`Window::set_always_on_top` is ignored on iOS") + } + + pub fn set_window_icon(&self, _icon: Option) { + warn!("`Window::set_window_icon` is ignored on iOS") + } + + pub fn set_ime_position(&self, _position: Position) { + warn!("`Window::set_ime_position` is ignored on iOS") + } + + pub fn request_user_attention(&self, _request_type: Option) { + warn!("`Window::request_user_attention` is ignored on iOS") + } + + pub fn set_background_color(&self, _color: Option) {} + + // Allow directly accessing the current monitor internally without unwrapping. + fn current_monitor_inner(&self) -> RootMonitorHandle { + unsafe { + let uiscreen: id = msg_send![self.window, screen]; + RootMonitorHandle { + inner: MonitorHandle::retained_new(uiscreen), + } + } + } + + pub fn current_monitor(&self) -> Option { + Some(self.current_monitor_inner()) + } + + pub fn available_monitors(&self) -> VecDeque { + unsafe { monitor::uiscreens() } + } + + #[inline] + pub fn monitor_from_point(&self, _x: f64, _y: f64) -> Option { + warn!("`Window::monitor_from_point` is ignored on iOS"); + None + } + + pub fn primary_monitor(&self) -> Option { + let monitor = unsafe { monitor::main_uiscreen() }; + Some(RootMonitorHandle { inner: monitor }) + } + + pub fn id(&self) -> WindowId { + self.window.into() + } + + #[cfg(feature = "rwh_04")] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::UiKitHandle::empty(); + window_handle.ui_window = self.window as _; + window_handle.ui_view = self.view as _; + window_handle.ui_view_controller = self.view_controller as _; + rwh_04::RawWindowHandle::UiKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::UiKitWindowHandle::empty(); + window_handle.ui_window = self.window as _; + window_handle.ui_view = self.view as _; + window_handle.ui_view_controller = self.view_controller as _; + rwh_05::RawWindowHandle::UiKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::UiKit(rwh_05::UiKitDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let mut window_handle = rwh_06::UiKitWindowHandle::new({ + std::ptr::NonNull::new(self.view as _).expect("self.view should never be null") + }); + window_handle.ui_view_controller = std::ptr::NonNull::new(self.view_controller as _); + Ok(rwh_06::RawWindowHandle::UiKit(window_handle)) + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06(&self) -> Result { + Ok(rwh_06::RawDisplayHandle::UiKit( + rwh_06::UiKitDisplayHandle::new(), + )) + } + + pub fn theme(&self) -> Theme { + Theme::Light + } + + /// Sets badge count on iOS launcher. 0 hides the count + pub fn set_badge_count(&self, count: i32) { + set_badge_count(count); + } +} + +pub struct Window { + pub inner: Inner, +} + +impl Drop for Window { + fn drop(&mut self) { + unsafe { + assert_main_thread!("`Window::drop` can only be run on the main thread on iOS"); + } + } +} + +unsafe impl Send for Window {} +unsafe impl Sync for Window {} + +impl Deref for Window { + type Target = Inner; + + fn deref(&self) -> &Inner { + unsafe { + assert_main_thread!("`Window` methods can only be run on the main thread on iOS"); + } + &self.inner + } +} + +impl DerefMut for Window { + fn deref_mut(&mut self) -> &mut Inner { + unsafe { + assert_main_thread!("`Window` methods can only be run on the main thread on iOS"); + } + &mut self.inner + } +} + +impl Window { + pub fn new( + _event_loop: &EventLoopWindowTarget, + window_attributes: WindowAttributes, + platform_attributes: PlatformSpecificWindowBuilderAttributes, + ) -> Result { + if window_attributes.always_on_top { + warn!("`WindowAttributes::always_on_top` is unsupported on iOS"); + } + // TODO: transparency, visible + + unsafe { + let screen = match window_attributes.fullscreen { + Some(Fullscreen::Exclusive(ref video_mode)) => { + video_mode.video_mode.monitor.ui_screen() as id + } + Some(Fullscreen::Borderless(Some(ref monitor))) => monitor.inner.ui_screen(), + Some(Fullscreen::Borderless(None)) | None => monitor::main_uiscreen().ui_screen() as id, + }; + + let screen_bounds: CGRect = msg_send![screen, bounds]; + + let frame = match window_attributes.inner_size { + Some(dim) => { + let scale_factor = msg_send![screen, scale]; + let size = dim.to_logical::(scale_factor); + CGRect { + origin: screen_bounds.origin, + size: CGSize { + width: size.width as _, + height: size.height as _, + }, + } + } + None => screen_bounds, + }; + + let view = view::create_view(&window_attributes, &platform_attributes, frame); + + let gl_or_metal_backed = { + let view_class: *const AnyClass = msg_send![view, class]; + let layer_class: *const AnyClass = msg_send![view_class, layerClass]; + let is_metal: bool = msg_send![layer_class, isSubclassOfClass: class!(CAMetalLayer)]; + let is_gl: bool = msg_send![layer_class, isSubclassOfClass: class!(CAEAGLLayer)]; + is_metal || is_gl + }; + + let view_controller = + view::create_view_controller(&window_attributes, &platform_attributes, view); + let window = view::create_window( + &window_attributes, + &platform_attributes, + frame, + view_controller, + ); + + let result = Window { + inner: Inner { + window, + view_controller, + view, + gl_or_metal_backed, + }, + }; + app_state::set_key_window(window); + + // Like the Windows and macOS backends, we send a `ScaleFactorChanged` and `Resized` + // event on window creation if the DPI factor != 1.0 + let scale_factor: CGFloat = msg_send![view, contentScaleFactor]; + let scale_factor: f64 = scale_factor.into(); + if scale_factor != 1.0 { + let bounds: CGRect = msg_send![view, bounds]; + let screen: id = msg_send![window, screen]; + let screen_space: id = msg_send![screen, coordinateSpace]; + let screen_frame: CGRect = + msg_send![view, convertRect:bounds, toCoordinateSpace:screen_space]; + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width as _, + height: screen_frame.size.height as _, + }; + app_state::handle_nonuser_events( + std::iter::once(EventWrapper::EventProxy(EventProxy::DpiChangedProxy { + window_id: window, + scale_factor, + suggested_size: size, + })) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), + ); + } + + Ok(result) + } + } +} + +// WindowExtIOS +impl Inner { + pub fn ui_window(&self) -> id { + self.window + } + pub fn ui_view_controller(&self) -> id { + self.view_controller + } + pub fn ui_view(&self) -> id { + self.view + } + + pub fn set_scale_factor(&self, scale_factor: f64) { + unsafe { + assert!( + dpi::validate_scale_factor(scale_factor), + "`WindowExtIOS::set_scale_factor` received an invalid hidpi factor" + ); + let scale_factor = scale_factor as CGFloat; + let () = msg_send![self.view, setContentScaleFactor: scale_factor]; + } + } + + pub fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { + unsafe { + let idiom = event_loop::get_idiom(); + let supported_orientations = + UIInterfaceOrientationMask::from_valid_orientations_idiom(valid_orientations, idiom); + msg_send![ + self.view_controller, + setSupportedInterfaceOrientations: supported_orientations + ] + } + } + + pub fn set_prefers_home_indicator_hidden(&self, hidden: bool) { + unsafe { + let prefers_home_indicator_hidden = if hidden { YES } else { NO }; + let () = msg_send![ + self.view_controller, + setPrefersHomeIndicatorAutoHidden: prefers_home_indicator_hidden + ]; + } + } + + pub fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { + let edges: UIRectEdge = edges.into(); + unsafe { + let () = msg_send![ + self.view_controller, + setPreferredScreenEdgesDeferringSystemGestures: edges + ]; + } + } + + pub fn set_prefers_status_bar_hidden(&self, hidden: bool) { + unsafe { + let status_bar_hidden = if hidden { YES } else { NO }; + let () = msg_send![ + self.view_controller, + setPrefersStatusBarHidden: status_bar_hidden + ]; + } + } +} + +impl Inner { + // requires main thread + unsafe fn screen_frame(&self) -> CGRect { + self.to_screen_space(msg_send![self.window, bounds]) + } + + // requires main thread + unsafe fn to_screen_space(&self, rect: CGRect) -> CGRect { + let screen: id = msg_send![self.window, screen]; + if !screen.is_null() { + let screen_space: id = msg_send![screen, coordinateSpace]; + msg_send![self.window, convertRect:rect, toCoordinateSpace:screen_space] + } else { + rect + } + } + + // requires main thread + unsafe fn from_screen_space(&self, rect: CGRect) -> CGRect { + let screen: id = msg_send![self.window, screen]; + if !screen.is_null() { + let screen_space: id = msg_send![screen, coordinateSpace]; + msg_send![self.window, convertRect:rect, fromCoordinateSpace:screen_space] + } else { + rect + } + } + + // requires main thread + unsafe fn safe_area_screen_space(&self) -> CGRect { + let bounds: CGRect = msg_send![self.window, bounds]; + if app_state::os_capabilities().safe_area { + let safe_area: UIEdgeInsets = msg_send![self.window, safeAreaInsets]; + let safe_bounds = CGRect { + origin: CGPoint { + x: bounds.origin.x + safe_area.left, + y: bounds.origin.y + safe_area.top, + }, + size: CGSize { + width: bounds.size.width - safe_area.left - safe_area.right, + height: bounds.size.height - safe_area.top - safe_area.bottom, + }, + }; + self.to_screen_space(safe_bounds) + } else { + let screen_frame = self.to_screen_space(bounds); + let status_bar_frame: CGRect = { + let app: id = msg_send![class!(UIApplication), sharedApplication]; + assert!( + !app.is_null(), + "`Window::get_inner_position` cannot be called before `EventLoop::run` on iOS" + ); + msg_send![app, statusBarFrame] + }; + let (y, height) = if screen_frame.origin.y > status_bar_frame.size.height { + (screen_frame.origin.y, screen_frame.size.height) + } else { + let y = status_bar_frame.size.height; + let height = + screen_frame.size.height - (status_bar_frame.size.height - screen_frame.origin.y); + (y, height) + }; + CGRect { + origin: CGPoint { + x: screen_frame.origin.x, + y, + }, + size: CGSize { + width: screen_frame.size.width, + height, + }, + } + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId { + window: id, +} + +impl WindowId { + pub unsafe fn dummy() -> Self { + WindowId { + window: std::ptr::null_mut(), + } + } +} + +unsafe impl Send for WindowId {} +unsafe impl Sync for WindowId {} + +impl From<&AnyObject> for WindowId { + fn from(window: &AnyObject) -> WindowId { + WindowId { + window: window as *const _ as _, + } + } +} + +impl From<&mut AnyObject> for WindowId { + fn from(window: &mut AnyObject) -> WindowId { + WindowId { + window: window as _, + } + } +} + +impl From for WindowId { + fn from(window: id) -> WindowId { + WindowId { window } + } +} + +#[derive(Clone)] +pub struct PlatformSpecificWindowBuilderAttributes { + pub root_view_class: &'static AnyClass, + pub scale_factor: Option, + pub valid_orientations: ValidOrientations, + pub prefers_home_indicator_hidden: bool, + pub prefers_status_bar_hidden: bool, + pub preferred_screen_edges_deferring_system_gestures: ScreenEdge, +} + +impl Default for PlatformSpecificWindowBuilderAttributes { + fn default() -> PlatformSpecificWindowBuilderAttributes { + PlatformSpecificWindowBuilderAttributes { + root_view_class: class!(UIView), + scale_factor: None, + valid_orientations: Default::default(), + prefers_home_indicator_hidden: false, + prefers_status_bar_hidden: false, + preferred_screen_edges_deferring_system_gestures: Default::default(), + } + } +} diff --git a/vendor/tao/src/platform_impl/linux/device.rs b/vendor/tao/src/platform_impl/linux/device.rs new file mode 100644 index 0000000000..860860379a --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/device.rs @@ -0,0 +1,80 @@ +use std::{ + os::raw::{c_int, c_uchar}, + ptr, +}; + +use gtk::glib; +use x11_dl::{xinput2, xlib}; + +use crate::event::{DeviceEvent, ElementState, RawKeyEvent}; + +use super::keycode_from_scancode; + +/// Spawn Device event thread. Only works on x11 since wayland doesn't have such global events. +pub fn spawn(device_tx: glib::Sender) { + std::thread::spawn(move || unsafe { + let xlib = xlib::Xlib::open().unwrap(); + let xinput2 = xinput2::XInput2::open().unwrap(); + let display = (xlib.XOpenDisplay)(ptr::null()); + let root = (xlib.XDefaultRootWindow)(display); + // TODO Add more device event mask + let mask = xinput2::XI_RawKeyPressMask | xinput2::XI_RawKeyReleaseMask; + let mut event_mask = xinput2::XIEventMask { + deviceid: xinput2::XIAllMasterDevices, + mask: &mask as *const _ as *mut c_uchar, + mask_len: std::mem::size_of_val(&mask) as c_int, + }; + (xinput2.XISelectEvents)(display, root, &mut event_mask as *mut _, 1); + + #[allow(clippy::uninit_assumed_init)] + let mut event: xlib::XEvent = std::mem::MaybeUninit::uninit().assume_init(); + loop { + (xlib.XNextEvent)(display, &mut event); + + // XFilterEvent tells us when an event has been discarded by the input method. + // Specifically, this involves all of the KeyPress events in compose/pre-edit sequences, + // along with an extra copy of the KeyRelease events. This also prevents backspace and + // arrow keys from being detected twice. + if xlib::True == { + (xlib.XFilterEvent)(&mut event, { + let xev: &xlib::XAnyEvent = event.as_ref(); + xev.window + }) + } { + continue; + } + + let event_type = event.get_type(); + match event_type { + xlib::GenericEvent => { + let mut xev = event.generic_event_cookie; + if (xlib.XGetEventData)(display, &mut xev) == xlib::True { + match xev.evtype { + xinput2::XI_RawKeyPress | xinput2::XI_RawKeyRelease => { + let xev: &xinput2::XIRawEvent = &*(xev.data as *const _); + let physical_key = keycode_from_scancode(xev.detail as u32); + let state = match xev.evtype { + xinput2::XI_RawKeyPress => ElementState::Pressed, + xinput2::XI_RawKeyRelease => ElementState::Released, + _ => unreachable!(), + }; + + let event = RawKeyEvent { + physical_key, + state, + }; + + if let Err(e) = device_tx.send(DeviceEvent::Key(event)) { + log::info!("Failed to send device event {} since receiver is closed. Closing x11 thread along with it", e); + break; + } + } + _ => {} + } + } + } + _ => {} + } + } + }); +} diff --git a/vendor/tao/src/platform_impl/linux/event_loop.rs b/vendor/tao/src/platform_impl/linux/event_loop.rs new file mode 100644 index 0000000000..7409eea7b9 --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/event_loop.rs @@ -0,0 +1,1289 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + cell::RefCell, + collections::{HashSet, VecDeque}, + error::Error, + process, + rc::Rc, + sync::atomic::{AtomicBool, Ordering}, + time::Instant, +}; + +use cairo::{RectangleInt, Region}; +use crossbeam_channel::SendError; +use gdk::{Cursor, CursorType, EventKey, EventMask, ScrollDirection, WindowEdge, WindowState}; +use gio::Cancellable; +use glib::{source::Priority, MainContext}; +use gtk::{ + cairo, gdk, gio, + glib::{self}, + prelude::*, + Settings, +}; + +#[cfg(feature = "x11")] +use crate::platform_impl::platform::device; +use crate::{ + dpi::{LogicalPosition, LogicalSize, PhysicalPosition}, + error::ExternalError, + event::{ + ElementState, Event, MouseButton, MouseScrollDelta, StartCause, TouchPhase, WindowEvent, + }, + event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, + keyboard::ModifiersState, + monitor::MonitorHandle as RootMonitorHandle, + platform_impl::platform::DEVICE_ID, + window::{ + CursorIcon, Fullscreen, ProgressBarState, ResizeDirection, Theme, WindowId as RootWindowId, + }, +}; + +use super::{ + keyboard, + monitor::{self, MonitorHandle}, + taskbar, util, + window::{WindowId, WindowRequest}, +}; + +use taskbar::TaskbarIndicator; + +#[derive(Clone)] +pub struct EventLoopWindowTarget { + /// Gdk display + pub(crate) display: gdk::Display, + /// Gtk application + pub(crate) app: gtk::Application, + /// Window Ids of the application + pub(crate) windows: Rc>>, + /// Window requests sender + pub(crate) window_requests_tx: glib::Sender<(WindowId, WindowRequest)>, + /// Draw event sender + pub(crate) draw_tx: crossbeam_channel::Sender, + _marker: std::marker::PhantomData, +} + +impl EventLoopWindowTarget { + #[inline] + pub fn monitor_from_point(&self, x: f64, y: f64) -> Option { + monitor::from_point(&self.display, x, y) + } + #[inline] + pub fn available_monitors(&self) -> VecDeque { + let mut handles = VecDeque::new(); + let display = &self.display; + let numbers = display.n_monitors(); + + for i in 0..numbers { + let monitor = MonitorHandle::new(display, i); + handles.push_back(monitor); + } + + handles + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + let monitor = self.display.primary_monitor(); + monitor.and_then(|monitor| { + let handle = MonitorHandle { monitor }; + Some(RootMonitorHandle { inner: handle }) + }) + } + + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + if self.is_wayland() { + let mut display_handle = rwh_05::WaylandDisplayHandle::empty(); + display_handle.display = unsafe { + gdk_wayland_sys::gdk_wayland_display_get_wl_display(self.display.as_ptr() as *mut _) + }; + rwh_05::RawDisplayHandle::Wayland(display_handle) + } else { + let mut display_handle = rwh_05::XlibDisplayHandle::empty(); + unsafe { + if let Ok(xlib) = x11_dl::xlib::Xlib::open() { + let display = (xlib.XOpenDisplay)(std::ptr::null()); + display_handle.display = display as _; + display_handle.screen = (xlib.XDefaultScreen)(display) as _; + } + } + + rwh_05::RawDisplayHandle::Xlib(display_handle) + } + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06(&self) -> Result { + if self.is_wayland() { + let display = unsafe { + gdk_wayland_sys::gdk_wayland_display_get_wl_display(self.display.as_ptr() as *mut _) + }; + let display = unsafe { std::ptr::NonNull::new_unchecked(display) }; + let display_handle = rwh_06::WaylandDisplayHandle::new(display); + Ok(rwh_06::RawDisplayHandle::Wayland(display_handle)) + } else { + #[cfg(feature = "x11")] + unsafe { + if let Ok(xlib) = x11_dl::xlib::Xlib::open() { + let display = (xlib.XOpenDisplay)(std::ptr::null()); + let screen = (xlib.XDefaultScreen)(display) as _; + let display = std::ptr::NonNull::new_unchecked(display as _); + let display_handle = rwh_06::XlibDisplayHandle::new(Some(display), screen); + Ok(rwh_06::RawDisplayHandle::Xlib(display_handle)) + } else { + Err(rwh_06::HandleError::Unavailable) + } + } + #[cfg(not(feature = "x11"))] + Err(rwh_06::HandleError::Unavailable) + } + } + + pub fn is_wayland(&self) -> bool { + self.display.backend().is_wayland() + } + + #[cfg(feature = "x11")] + pub fn is_x11(&self) -> bool { + self.display.backend().is_x11() + } + + #[inline] + pub fn cursor_position(&self) -> Result, ExternalError> { + util::cursor_position(self.is_wayland()) + } + + #[inline] + pub fn set_progress_bar(&self, progress: ProgressBarState) { + if let Err(e) = self + .window_requests_tx + .send((WindowId::dummy(), WindowRequest::ProgressBarState(progress))) + { + log::warn!("Fail to send update progress bar request: {e}"); + } + } + + #[inline] + pub fn set_badge_count(&self, count: Option, desktop_filename: Option) { + if let Err(e) = self.window_requests_tx.send(( + WindowId::dummy(), + WindowRequest::BadgeCount(count, desktop_filename), + )) { + log::warn!("Fail to send update progress bar request: {e}"); + } + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + if let Err(e) = self + .window_requests_tx + .send((WindowId::dummy(), WindowRequest::SetTheme(theme))) + { + log::warn!("Fail to send update theme request: {e}"); + } + } +} + +pub struct EventLoop { + /// Window target. + window_target: RootELW, + /// User event sender for EventLoopProxy + pub(crate) user_event_tx: crossbeam_channel::Sender>, + /// Event queue of EventLoop + events: crossbeam_channel::Receiver>, + /// Draw queue of EventLoop + draws: crossbeam_channel::Receiver, + /// Boolean to control device event thread + run_device_thread: Option>, +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) any_thread: bool, + pub(crate) app_id: Option, +} + +impl EventLoop { + pub(crate) fn new(attrs: &PlatformSpecificEventLoopAttributes) -> EventLoop { + if !attrs.any_thread { + assert_is_main_thread("new_any_thread"); + } + + let context = MainContext::default(); + context + .with_thread_default(|| { + EventLoop::new_gtk(attrs.app_id.as_deref()).expect("Failed to initialize gtk backend!") + }) + .expect("Failed to initialize gtk backend!") + } + + fn new_gtk(app_id: Option<&str>) -> Result, Box> { + // This should be done by gtk::Application::new, but does not work properly + gtk::init()?; + let context = MainContext::default(); + let app = gtk::Application::new(app_id, gio::ApplicationFlags::empty()); + let app_ = app.clone(); + let cancellable: Option<&Cancellable> = None; + app.register(cancellable)?; + + // Send StartCause::Init event + let (event_tx, event_rx) = crossbeam_channel::unbounded(); + let (draw_tx, draw_rx) = crossbeam_channel::unbounded(); + let event_tx_ = event_tx.clone(); + app.connect_activate(move |_| { + if let Err(e) = event_tx_.send(Event::NewEvents(StartCause::Init)) { + log::warn!("Failed to send init event to event channel: {}", e); + } + }); + let draw_tx_ = draw_tx.clone(); + let user_event_tx = event_tx.clone(); + + // Create event loop window target. + let (window_requests_tx, window_requests_rx) = glib::MainContext::channel(Priority::default()); + let display = gdk::Display::default() + .expect("GdkDisplay not found. This usually means `gkt_init` hasn't called yet."); + let window_target = EventLoopWindowTarget { + display, + app, + windows: Rc::new(RefCell::new(HashSet::new())), + window_requests_tx, + draw_tx: draw_tx_, + _marker: std::marker::PhantomData, + }; + + // Spawn x11 thread to receive Device events. + #[cfg(feature = "x11")] + let run_device_thread = if window_target.is_x11() { + let (device_tx, device_rx) = glib::MainContext::channel(glib::Priority::default()); + let user_event_tx = user_event_tx.clone(); + let run_device_thread = Rc::new(AtomicBool::new(true)); + let run = run_device_thread.clone(); + device::spawn(device_tx); + device_rx.attach(Some(&context), move |event| { + if let Err(e) = user_event_tx.send(Event::DeviceEvent { + device_id: DEVICE_ID, + event, + }) { + log::warn!("Fail to send device event to event channel: {}", e); + } + if run.load(Ordering::Relaxed) { + glib::ControlFlow::Continue + } else { + glib::ControlFlow::Break + } + }); + Some(run_device_thread) + } else { + None + }; + #[cfg(not(feature = "x11"))] + let run_device_thread = None; + + let mut taskbar = TaskbarIndicator::new(); + let is_wayland = window_target.is_wayland(); + + // Window Request + window_requests_rx.attach(Some(&context), move |(id, request)| { + if let Some(window) = app_.window_by_id(id.0) { + match request { + WindowRequest::Title(title) => window.set_title(&title), + WindowRequest::Position((x, y)) => window.move_(x, y), + WindowRequest::Size((w, h)) => window.resize(w, h), + WindowRequest::SizeConstraints(constraints) => { + util::set_size_constraints(&window, constraints); + } + WindowRequest::Visible(visible) => { + if visible { + window.show_all(); + } else { + window.hide(); + } + } + WindowRequest::Focus => { + window.present_with_time(gdk::ffi::GDK_CURRENT_TIME as _); + } + WindowRequest::Resizable(resizable) => window.set_resizable(resizable), + WindowRequest::Closable(closable) => window.set_deletable(closable), + WindowRequest::Minimized(minimized) => { + if minimized { + window.iconify(); + } else { + window.deiconify(); + } + } + WindowRequest::Maximized(maximized, resizable) => { + if maximized { + let maximize_process = util::WindowMaximizeProcess::new(window.clone(), resizable); + glib::idle_add_local_full(glib::Priority::DEFAULT_IDLE, move || { + let mut maximize_process = maximize_process.borrow_mut(); + maximize_process.next_step() + }); + } else { + window.unmaximize(); + } + } + WindowRequest::DragWindow => { + if let Some(cursor) = window + .display() + .default_seat() + .and_then(|seat| seat.pointer()) + { + let (_, x, y) = cursor.position(); + window.begin_move_drag(1, x, y, 0); + } + } + WindowRequest::DragResizeWindow(direction) => { + if let Some(cursor) = window + .display() + .default_seat() + .and_then(|seat| seat.pointer()) + { + let (_, x, y) = cursor.position(); + window.begin_resize_drag( + direction.to_gtk_edge(), + 1, + x, + y, + gtk::gdk::ffi::GDK_CURRENT_TIME as _, + ); + } + } + WindowRequest::Fullscreen(fullscreen) => match fullscreen { + Some(f) => { + if let Fullscreen::Borderless(m) = f { + if let Some(monitor) = m { + let display = window.display(); + let monitor = monitor.inner; + let monitors = display.n_monitors(); + for i in 0..monitors { + let m = display.monitor(i).unwrap(); + if m == monitor.monitor { + let screen = display.default_screen(); + window.fullscreen_on_monitor(&screen, i); + } + } + } else { + window.fullscreen(); + } + } + } + None => window.unfullscreen(), + }, + WindowRequest::Decorations(decorations) => window.set_decorated(decorations), + WindowRequest::AlwaysOnBottom(always_on_bottom) => { + window.set_keep_below(always_on_bottom) + } + WindowRequest::AlwaysOnTop(always_on_top) => window.set_keep_above(always_on_top), + WindowRequest::WindowIcon(window_icon) => { + if let Some(icon) = window_icon { + window.set_icon(Some(&icon.inner.into())); + } + } + WindowRequest::UserAttention(request_type) => { + window.set_urgency_hint(request_type.is_some()) + } + WindowRequest::SetSkipTaskbar(skip) => { + window.set_skip_taskbar_hint(skip); + window.set_skip_pager_hint(skip) + } + WindowRequest::BackgroundColor(css_provider, color) => { + unsafe { window.set_data("background_color", color) }; + + let style_context = window.style_context(); + style_context.remove_provider(&css_provider); + + if let Some(color) = color { + let theme = format!( + r#" + window {{ + background-color: rgba({},{},{},{}); + }} + "#, + color.0, + color.1, + color.2, + color.3 as f64 / 255.0 + ); + let _ = css_provider.load_from_data(theme.as_bytes()); + style_context.add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION); + }; + } + WindowRequest::SetVisibleOnAllWorkspaces(visible) => { + if visible { + window.stick(); + } else { + window.unstick(); + } + } + WindowRequest::CursorIcon(cursor) => { + if let Some(gdk_window) = window.window() { + let display = window.display(); + match cursor { + Some(cr) => { + gdk_window.set_cursor(Cursor::from_name(&display, cr.to_str()).as_ref()) + } + None => gdk_window + .set_cursor(Cursor::for_display(&display, CursorType::BlankCursor).as_ref()), + } + }; + } + WindowRequest::CursorPosition((x, y)) => { + if let Some(cursor) = window + .display() + .default_seat() + .and_then(|seat| seat.pointer()) + { + if let Some(screen) = GtkWindowExt::screen(&window) { + cursor.warp(&screen, x, y); + } + } + } + WindowRequest::CursorIgnoreEvents(ignore) => { + if ignore { + let empty_region = Region::create_rectangle(&RectangleInt::new(0, 0, 1, 1)); + window + .window() + .unwrap() + .input_shape_combine_region(&empty_region, 0, 0); + } else { + window.input_shape_combine_region(None) + }; + } + WindowRequest::ProgressBarState(_) => unreachable!(), + WindowRequest::BadgeCount(_, _) => unreachable!(), + WindowRequest::SetTheme(_) => unreachable!(), + WindowRequest::WireUpEvents { + transparent, + fullscreen, + cursor_moved, + } => { + window.add_events( + EventMask::POINTER_MOTION_MASK + | EventMask::BUTTON1_MOTION_MASK + | EventMask::BUTTON_PRESS_MASK + | EventMask::TOUCH_MASK + | EventMask::STRUCTURE_MASK + | EventMask::FOCUS_CHANGE_MASK + | EventMask::SCROLL_MASK, + ); + + let fullscreen = Rc::new(AtomicBool::new(fullscreen)); + let fullscreen_ = fullscreen.clone(); + window.connect_window_state_event(move |_window, event| { + let state = event.changed_mask(); + if state.contains(WindowState::FULLSCREEN) { + fullscreen_.store( + event.new_window_state().contains(WindowState::FULLSCREEN), + Ordering::Relaxed, + ); + } + glib::Propagation::Proceed + }); + + // Allow resizing unmaximized non-fullscreen undecorated window + let fullscreen_ = fullscreen.clone(); + window.connect_motion_notify_event(move |window, event| { + if !window.is_decorated() && window.is_resizable() && !window.is_maximized() { + if let Some(window) = window.window() { + let (cx, cy) = event.root(); + let (left, top) = window.position(); + let (w, h) = (window.width(), window.height()); + let (right, bottom) = (left + w, top + h); + let border = window.scale_factor() * 5; + let edge = crate::window::hit_test( + (left, top, right, bottom), + cx as _, + cy as _, + border, + border, + ); + + let edge = match &edge { + Some(e) if !fullscreen_.load(Ordering::Relaxed) => e.to_cursor_str(), + _ => "default", + }; + window.set_cursor(Cursor::from_name(&window.display(), edge).as_ref()); + } + } + glib::Propagation::Proceed + }); + window.connect_button_press_event(move |window, event| { + const LMB: u32 = 1; + if (is_wayland || !window.is_decorated()) + && window.is_resizable() + && !window.is_maximized() + && event.button() == LMB + { + let (cx, cy) = event.root(); + let (left, top) = window.position(); + let (w, h) = window.size(); + let (right, bottom) = (left + w, top + h); + let border = window.scale_factor() * 5; + let edge = crate::window::hit_test( + (left, top, right, bottom), + cx as _, + cy as _, + border, + border, + ) + .map(|d| d.to_gtk_edge()) + // we return `WindowEdge::__Unknown` to be ignored later. + // we must return 8 or bigger, otherwise it will be the same as one of the other 7 variants of `WindowEdge` enum. + .unwrap_or(WindowEdge::__Unknown(8)); + // Ignore the `__Unknown` variant so the window receives the click correctly if it is not on the edges. + match edge { + WindowEdge::__Unknown(_) => (), + _ => { + // FIXME: calling `window.begin_resize_drag` uses the default cursor, it should show a resizing cursor instead + window.begin_resize_drag(edge, LMB as i32, cx as i32, cy as i32, event.time()) + } + } + } + + glib::Propagation::Proceed + }); + window.connect_touch_event(move |window, event| { + if !window.is_decorated() && window.is_resizable() && !window.is_maximized() { + if let Some(window) = window.window() { + if let Some((cx, cy)) = event.root_coords() { + if let Some(device) = event.device() { + let (left, top) = window.position(); + let (w, h) = (window.width(), window.height()); + let (right, bottom) = (left + w, top + h); + let border = window.scale_factor() * 5; + let edge = crate::window::hit_test( + (left, top, right, bottom), + cx as _, + cy as _, + border, + border, + ) + .map(|d| d.to_gtk_edge()) + // we return `WindowEdge::__Unknown` to be ignored later. + // we must return 8 or bigger, otherwise it will be the same as one of the other 7 variants of `WindowEdge` enum. + .unwrap_or(WindowEdge::__Unknown(8)); + + // Ignore the `__Unknown` variant so the window receives the click correctly if it is not on the edges. + match edge { + WindowEdge::__Unknown(_) => (), + _ => window.begin_resize_drag_for_device( + edge, + &device, + 0, + cx as i32, + cy as i32, + event.time(), + ), + } + } + } + } + } + + glib::Propagation::Proceed + }); + + let tx_clone = event_tx.clone(); + window.connect_delete_event(move |_, _| { + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CloseRequested, + }) { + log::warn!("Failed to send window close event to event channel: {}", e); + } + glib::Propagation::Stop + }); + + let tx_clone = event_tx.clone(); + window.connect_configure_event(move |window, event| { + let scale_factor = window.scale_factor(); + + let (x, y) = window + .window() + .map(|w| w.root_origin()) + .unwrap_or_else(|| event.position()); + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Moved( + LogicalPosition::new(x, y).to_physical(scale_factor as f64), + ), + }) { + log::warn!("Failed to send window moved event to event channel: {}", e); + } + + let (w, h) = event.size(); + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Resized( + LogicalSize::new(w, h).to_physical(scale_factor as f64), + ), + }) { + log::warn!( + "Failed to send window resized event to event channel: {}", + e + ); + } + false + }); + + let tx_clone = event_tx.clone(); + window.connect_focus_in_event(move |_, _| { + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(true), + }) { + log::warn!( + "Failed to send window focus-in event to event channel: {}", + e + ); + } + glib::Propagation::Proceed + }); + + let tx_clone = event_tx.clone(); + window.connect_focus_out_event(move |_, _| { + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(false), + }) { + log::warn!( + "Failed to send window focus-out event to event channel: {}", + e + ); + } + glib::Propagation::Proceed + }); + + let tx_clone = event_tx.clone(); + window.connect_destroy(move |_| { + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Destroyed, + }) { + log::warn!( + "Failed to send window destroyed event to event channel: {}", + e + ); + } + }); + + let tx_clone = event_tx.clone(); + window.connect_enter_notify_event(move |_, _| { + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorEntered { + device_id: DEVICE_ID, + }, + }) { + log::warn!( + "Failed to send cursor entered event to event channel: {}", + e + ); + } + glib::Propagation::Proceed + }); + + let tx_clone = event_tx.clone(); + window.connect_motion_notify_event(move |window, motion| { + if cursor_moved { + if let Some(cursor) = motion.device() { + let scale_factor = window.scale_factor(); + let (_, x, y) = cursor.window_at_position(); + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { + position: LogicalPosition::new(x, y).to_physical(scale_factor as f64), + device_id: DEVICE_ID, + // this field is depracted so it is fine to pass empty state + modifiers: ModifiersState::empty(), + }, + }) { + log::warn!("Failed to send cursor moved event to event channel: {}", e); + } + } + } + glib::Propagation::Stop + }); + + let tx_clone = event_tx.clone(); + window.connect_leave_notify_event(move |_, _| { + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorLeft { + device_id: DEVICE_ID, + }, + }) { + log::warn!("Failed to send cursor left event to event channel: {}", e); + } + glib::Propagation::Proceed + }); + + let tx_clone = event_tx.clone(); + window.connect_button_press_event(move |_, event| { + let button = event.button(); + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { + button: match button { + 1 => MouseButton::Left, + 2 => MouseButton::Middle, + 3 => MouseButton::Right, + _ => MouseButton::Other(button as u16), + }, + state: ElementState::Pressed, + device_id: DEVICE_ID, + // this field is depracted so it is fine to pass empty state + modifiers: ModifiersState::empty(), + }, + }) { + log::warn!( + "Failed to send mouse input pressed event to event channel: {}", + e + ); + } + glib::Propagation::Stop + }); + + let tx_clone = event_tx.clone(); + window.connect_button_release_event(move |_, event| { + let button = event.button(); + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { + button: match button { + 1 => MouseButton::Left, + 2 => MouseButton::Middle, + 3 => MouseButton::Right, + _ => MouseButton::Other(button as u16), + }, + state: ElementState::Released, + device_id: DEVICE_ID, + // this field is depracted so it is fine to pass empty state + modifiers: ModifiersState::empty(), + }, + }) { + log::warn!( + "Failed to send mouse input released event to event channel: {}", + e + ); + } + glib::Propagation::Stop + }); + + let tx_clone = event_tx.clone(); + window.connect_scroll_event(move |_, event| { + let (x, y) = event.delta(); + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta: MouseScrollDelta::LineDelta(-x as f32, -y as f32), + phase: match event.direction() { + ScrollDirection::Smooth => TouchPhase::Moved, + _ => TouchPhase::Ended, + }, + modifiers: ModifiersState::empty(), + }, + }) { + log::warn!("Failed to send scroll event to event channel: {}", e); + } + glib::Propagation::Proceed + }); + + let tx_clone = event_tx.clone(); + let keyboard_handler = Rc::new(move |event_key: EventKey, element_state| { + // if we have a modifier lets send it + let mut mods = keyboard::get_modifiers(event_key.clone()); + if !mods.is_empty() { + // if we release the modifier tell the world + if ElementState::Released == element_state { + mods = ModifiersState::empty(); + } + + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(mods), + }) { + log::warn!( + "Failed to send modifiers changed event to event channel: {}", + e + ); + } else { + // stop here we don't want to send the key event + // as we emit the `ModifiersChanged` + return glib::ControlFlow::Continue; + } + } + + // todo: implement repeat? + let event = keyboard::make_key_event(&event_key, false, None, element_state); + + if let Some(event) = event { + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }, + }) { + log::warn!("Failed to send keyboard event to event channel: {}", e); + } + } + glib::ControlFlow::Continue + }); + + let tx_clone = event_tx.clone(); + // TODO Add actual IME from system + let ime = gtk::IMContextSimple::default(); + ime.set_client_window(window.window().as_ref()); + ime.focus_in(); + ime.connect_commit(move |_, s| { + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ReceivedImeText(s.to_string()), + }) { + log::warn!( + "Failed to send received IME text event to event channel: {}", + e + ); + } + }); + + let handler = keyboard_handler.clone(); + window.connect_key_press_event(move |_, event_key| { + handler(event_key.to_owned(), ElementState::Pressed); + ime.filter_keypress(event_key); + + glib::Propagation::Proceed + }); + + let handler = keyboard_handler.clone(); + window.connect_key_release_event(move |_, event_key| { + handler(event_key.to_owned(), ElementState::Released); + glib::Propagation::Proceed + }); + + let tx_clone = event_tx.clone(); + window.connect_window_state_event(move |window, event| { + let state = event.changed_mask(); + if state.contains(WindowState::ICONIFIED) || state.contains(WindowState::MAXIMIZED) { + let scale_factor = window.scale_factor(); + + let (x, y) = window.position(); + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Moved( + LogicalPosition::new(x, y).to_physical(scale_factor as f64), + ), + }) { + log::warn!("Failed to send window moved event to event channel: {}", e); + } + + let (w, h) = window.size(); + if let Err(e) = tx_clone.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Resized( + LogicalSize::new(w, h).to_physical(scale_factor as f64), + ), + }) { + log::warn!( + "Failed to send window resized event to event channel: {}", + e + ); + } + } + glib::Propagation::Proceed + }); + + // Receive draw events of the window. + let draw_clone = draw_tx.clone(); + window.connect_draw(move |window, cr| { + if let Err(e) = draw_clone.send(id) { + log::warn!("Failed to send redraw event to event channel: {}", e); + } + + if transparent { + let background_color = unsafe { + window + .data::>("background_color") + .and_then(|c| c.as_ref().clone()) + }; + + let rgba = background_color + .map(|(r, g, b, a)| (r as f64, g as f64, b as f64, a as f64 / 255.0)) + .unwrap_or((0., 0., 0., 0.)); + + let rect = window + .child() + .map(|c| c.allocation()) + .unwrap_or_else(|| window.allocation()); + + cr.rectangle( + rect.x() as _, + rect.y() as _, + rect.width() as _, + rect.height() as _, + ); + cr.set_source_rgba(rgba.0, rgba.1, rgba.2, rgba.3); + cr.set_operator(cairo::Operator::Source); + let _ = cr.fill(); + cr.set_operator(cairo::Operator::Over); + } + + glib::Propagation::Proceed + }); + } + } + } else if id == WindowId::dummy() { + match request { + WindowRequest::ProgressBarState(state) => { + taskbar.update(state); + } + WindowRequest::BadgeCount(count, desktop_filename) => { + taskbar.update_count(count, desktop_filename); + } + WindowRequest::SetTheme(theme) => { + if let Some(settings) = Settings::default() { + match theme { + Some(Theme::Dark) => settings.set_gtk_application_prefer_dark_theme(true), + Some(Theme::Light) | None => settings.set_gtk_application_prefer_dark_theme(false), + } + } + } + _ => unreachable!(), + } + } + glib::ControlFlow::Continue + }); + + // Create event loop itself. + let event_loop = Self { + window_target: RootELW { + p: window_target, + _marker: std::marker::PhantomData, + }, + user_event_tx, + events: event_rx, + draws: draw_rx, + run_device_thread, + }; + + Ok(event_loop) + } + + #[inline] + pub fn run(mut self, callback: F) -> ! + where + F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow) + 'static, + { + let exit_code = self.run_return(callback); + process::exit(exit_code) + } + + /// This is the core event loop logic. It basically loops on `gtk_main_iteration` and processes one + /// event along with that iteration. Depends on current control flow and what it should do, an + /// event state is defined. The whole state flow chart runs like following: + /// + /// ```ignore + /// Poll/Wait/WaitUntil + /// +-------------------------------------------------------------------------+ + /// | | + /// | Receiving event from event channel | Receiving event from draw channel + /// | +-------+ | +---+ + /// v v | | v | + /// +----------+ Poll/Wait/WaitUntil +------------+ Poll/Wait/WaitUntil +-----------+ | + /// | NewStart | ---------------------> | EventQueue | ---------------------> | DrawQueue | | + /// +----------+ +------------+ +-----------+ | + /// |ExitWithCode |ExitWithCode ExitWithCode| | | + /// +------------------------------------+------------------------------------+ +---+ + /// | + /// v + /// +---------------+ + /// | LoopDestroyed | + /// +---------------+ + /// ``` + /// + /// There are a dew notibale event will sent to callback when state is transisted: + /// - On any state moves to `LoopDestroyed`, a `LoopDestroyed` event is sent. + /// - On `NewStart` to `EventQueue`, a `NewEvents` with corresponding `StartCause` depends on + /// current control flow is sent. + /// - On `EventQueue` to `DrawQueue`, a `MainEventsCleared` event is sent. + /// - On `DrawQueue` back to `NewStart`, a `RedrawEventsCleared` event is sent. + pub(crate) fn run_return(&mut self, mut callback: F) -> i32 + where + F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), + { + enum EventState { + NewStart, + EventQueue, + DrawQueue, + } + + let context = MainContext::default(); + let run_device_thread = self.run_device_thread.clone(); + + context + .with_thread_default(|| { + let mut control_flow = ControlFlow::default(); + let window_target = &self.window_target; + let events = &self.events; + let draws = &self.draws; + + window_target.p.app.activate(); + + let mut state = EventState::NewStart; + let exit_code = loop { + let mut blocking = false; + match state { + EventState::NewStart => match control_flow { + ControlFlow::ExitWithCode(code) => { + callback(Event::LoopDestroyed, window_target, &mut control_flow); + break code; + } + ControlFlow::Wait => { + if !events.is_empty() { + callback( + Event::NewEvents(StartCause::WaitCancelled { + start: Instant::now(), + requested_resume: None, + }), + window_target, + &mut control_flow, + ); + state = EventState::EventQueue; + } else { + blocking = true; + } + } + ControlFlow::WaitUntil(requested_resume) => { + let start = Instant::now(); + if start >= requested_resume { + callback( + Event::NewEvents(StartCause::ResumeTimeReached { + start, + requested_resume, + }), + window_target, + &mut control_flow, + ); + state = EventState::EventQueue; + } else if !events.is_empty() { + callback( + Event::NewEvents(StartCause::WaitCancelled { + start, + requested_resume: Some(requested_resume), + }), + window_target, + &mut control_flow, + ); + state = EventState::EventQueue; + } else { + blocking = true; + } + } + _ => { + callback( + Event::NewEvents(StartCause::Poll), + window_target, + &mut control_flow, + ); + state = EventState::EventQueue; + } + }, + EventState::EventQueue => match control_flow { + ControlFlow::ExitWithCode(code) => { + callback(Event::LoopDestroyed, window_target, &mut control_flow); + break (code); + } + _ => match events.try_recv() { + Ok(event) => match event { + Event::LoopDestroyed => control_flow = ControlFlow::ExitWithCode(1), + _ => callback(event, window_target, &mut control_flow), + }, + Err(_) => { + callback(Event::MainEventsCleared, window_target, &mut control_flow); + state = EventState::DrawQueue; + } + }, + }, + EventState::DrawQueue => match control_flow { + ControlFlow::ExitWithCode(code) => { + callback(Event::LoopDestroyed, window_target, &mut control_flow); + break code; + } + _ => { + if let Ok(id) = draws.try_recv() { + callback( + Event::RedrawRequested(RootWindowId(id)), + window_target, + &mut control_flow, + ); + } + callback(Event::RedrawEventsCleared, window_target, &mut control_flow); + state = EventState::NewStart; + } + }, + } + gtk::main_iteration_do(blocking); + }; + if let Some(run_device_thread) = run_device_thread { + run_device_thread.store(false, Ordering::Relaxed); + } + exit_code + }) + .unwrap_or(1) + } + + #[inline] + pub fn window_target(&self) -> &RootELW { + &self.window_target + } + + /// Creates an `EventLoopProxy` that can be used to dispatch user events to the main event loop. + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + user_event_tx: self.user_event_tx.clone(), + } + } +} + +/// Used to send custom events to `EventLoop`. +#[derive(Debug)] +pub struct EventLoopProxy { + user_event_tx: crossbeam_channel::Sender>, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { + user_event_tx: self.user_event_tx.clone(), + } + } +} + +impl EventLoopProxy { + /// Send an event to the `EventLoop` from which this proxy was created. This emits a + /// `UserEvent(event)` event in the event loop, where `event` is the value passed to this + /// function. + /// + /// Returns an `Err` if the associated `EventLoop` no longer exists. + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self + .user_event_tx + .send(Event::UserEvent(event)) + .map_err(|SendError(event)| { + if let Event::UserEvent(error) = event { + EventLoopClosed(error) + } else { + unreachable!(); + } + })?; + + let context = MainContext::default(); + context.wakeup(); + + Ok(()) + } +} + +fn assert_is_main_thread(suggested_method: &str) { + assert!( + is_main_thread(), + "Initializing the event loop outside of the main thread is a significant \ + cross-platform compatibility hazard. If you really, absolutely need to create an \ + EventLoop on a different thread, please use the `EventLoopExtUnix::{suggested_method}` function." + ); +} + +#[cfg(target_os = "linux")] +fn is_main_thread() -> bool { + use libc::{c_long, getpid, syscall, SYS_gettid}; + + unsafe { syscall(SYS_gettid) == getpid() as c_long } +} + +#[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd"))] +fn is_main_thread() -> bool { + use libc::pthread_main_np; + + unsafe { pthread_main_np() == 1 } +} + +#[cfg(target_os = "netbsd")] +fn is_main_thread() -> bool { + std::thread::current().name() == Some("main") +} + +impl CursorIcon { + fn to_str(&self) -> &str { + match self { + CursorIcon::Crosshair => "crosshair", + CursorIcon::Hand => "pointer", + CursorIcon::Arrow => "arrow", + CursorIcon::Move => "move", + CursorIcon::Text => "text", + CursorIcon::Wait => "wait", + CursorIcon::Help => "help", + CursorIcon::Progress => "progress", + CursorIcon::NotAllowed => "not-allowed", + CursorIcon::ContextMenu => "context-menu", + CursorIcon::Cell => "cell", + CursorIcon::VerticalText => "vertical-text", + CursorIcon::Alias => "alias", + CursorIcon::Copy => "copy", + CursorIcon::NoDrop => "no-drop", + CursorIcon::Grab => "grab", + CursorIcon::Grabbing => "grabbing", + CursorIcon::AllScroll => "all-scroll", + CursorIcon::ZoomIn => "zoom-in", + CursorIcon::ZoomOut => "zoom-out", + CursorIcon::EResize => "e-resize", + CursorIcon::NResize => "n-resize", + CursorIcon::NeResize => "ne-resize", + CursorIcon::NwResize => "nw-resize", + CursorIcon::SResize => "s-resize", + CursorIcon::SeResize => "se-resize", + CursorIcon::SwResize => "sw-resize", + CursorIcon::WResize => "w-resize", + CursorIcon::EwResize => "ew-resize", + CursorIcon::NsResize => "ns-resize", + CursorIcon::NeswResize => "nesw-resize", + CursorIcon::NwseResize => "nwse-resize", + CursorIcon::ColResize => "col-resize", + CursorIcon::RowResize => "row-resize", + CursorIcon::Default => "default", + } + } +} + +impl ResizeDirection { + fn to_cursor_str(&self) -> &str { + match self { + ResizeDirection::East => "e-resize", + ResizeDirection::North => "n-resize", + ResizeDirection::NorthEast => "ne-resize", + ResizeDirection::NorthWest => "nw-resize", + ResizeDirection::South => "s-resize", + ResizeDirection::SouthEast => "se-resize", + ResizeDirection::SouthWest => "sw-resize", + ResizeDirection::West => "w-resize", + } + } + + fn to_gtk_edge(&self) -> WindowEdge { + match self { + ResizeDirection::East => WindowEdge::East, + ResizeDirection::North => WindowEdge::North, + ResizeDirection::NorthEast => WindowEdge::NorthEast, + ResizeDirection::NorthWest => WindowEdge::NorthWest, + ResizeDirection::South => WindowEdge::South, + ResizeDirection::SouthEast => WindowEdge::SouthEast, + ResizeDirection::SouthWest => WindowEdge::SouthWest, + ResizeDirection::West => WindowEdge::West, + } + } +} diff --git a/vendor/tao/src/platform_impl/linux/icon.rs b/vendor/tao/src/platform_impl/linux/icon.rs new file mode 100644 index 0000000000..bc1c18df19 --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/icon.rs @@ -0,0 +1,47 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use gtk::gdk_pixbuf::{Colorspace, Pixbuf}; + +use crate::window::BadIcon; + +/// An icon used for the window titlebar, taskbar, etc. +#[derive(Debug, Clone)] +pub struct PlatformIcon { + raw: Vec, + width: i32, + height: i32, + row_stride: i32, +} + +impl From for Pixbuf { + fn from(icon: PlatformIcon) -> Self { + Pixbuf::from_mut_slice( + icon.raw, + Colorspace::Rgb, + true, + 8, + icon.width, + icon.height, + icon.row_stride, + ) + } +} + +impl PlatformIcon { + /// Creates an `Icon` from 32bpp RGBA data. + /// + /// The length of `rgba` must be divisible by 4, and `width * height` must equal + /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error. + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + let row_stride = + Pixbuf::calculate_rowstride(Colorspace::Rgb, true, 8, width as i32, height as i32); + Ok(Self { + raw: rgba, + width: width as i32, + height: height as i32, + row_stride, + }) + } +} diff --git a/vendor/tao/src/platform_impl/linux/keyboard.rs b/vendor/tao/src/platform_impl/linux/keyboard.rs new file mode 100644 index 0000000000..ea86312ade --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/keyboard.rs @@ -0,0 +1,288 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use super::KeyEventExtra; +use crate::{ + event::{ElementState, KeyEvent}, + keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NativeKeyCode}, +}; +use gtk::{ + gdk::{self, keys::constants::*, EventKey}, + glib, +}; +use std::{ + collections::HashSet, + ffi::c_void, + os::raw::{c_int, c_uint}, + ptr, slice, + sync::Mutex, +}; + +pub type RawKey = gdk::keys::Key; + +lazy_static! { + static ref KEY_STRINGS: Mutex> = Mutex::new(HashSet::new()); +} + +fn insert_or_get_key_str(string: String) -> &'static str { + let mut string_set = KEY_STRINGS.lock().unwrap(); + if let Some(contained) = string_set.get(string.as_str()) { + return contained; + } + let static_str = Box::leak(string.into_boxed_str()); + string_set.insert(static_str); + static_str +} + +#[allow(clippy::just_underscores_and_digits, non_upper_case_globals)] +pub(crate) fn raw_key_to_key(gdk_key: RawKey) -> Option> { + match gdk_key { + Escape => Some(Key::Escape), + BackSpace => Some(Key::Backspace), + Tab | ISO_Left_Tab => Some(Key::Tab), + Return => Some(Key::Enter), + Control_L | Control_R => Some(Key::Control), + Alt_L | Alt_R => Some(Key::Alt), + Shift_L | Shift_R => Some(Key::Shift), + // TODO: investigate mapping. Map Meta_[LR]? + Super_L | Super_R => Some(Key::Super), + Caps_Lock => Some(Key::CapsLock), + F1 => Some(Key::F1), + F2 => Some(Key::F2), + F3 => Some(Key::F3), + F4 => Some(Key::F4), + F5 => Some(Key::F5), + F6 => Some(Key::F6), + F7 => Some(Key::F7), + F8 => Some(Key::F8), + F9 => Some(Key::F9), + F10 => Some(Key::F10), + F11 => Some(Key::F11), + F12 => Some(Key::F12), + F13 => Some(Key::F13), + F14 => Some(Key::F14), + F15 => Some(Key::F15), + F16 => Some(Key::F16), + F17 => Some(Key::F17), + F18 => Some(Key::F18), + F19 => Some(Key::F19), + F20 => Some(Key::F20), + F21 => Some(Key::F21), + F22 => Some(Key::F22), + F23 => Some(Key::F23), + F24 => Some(Key::F24), + + Print => Some(Key::PrintScreen), + Scroll_Lock => Some(Key::ScrollLock), + // Pause/Break not audio. + Pause => Some(Key::Pause), + + Insert => Some(Key::Insert), + Delete => Some(Key::Delete), + Home => Some(Key::Home), + End => Some(Key::End), + Page_Up => Some(Key::PageUp), + Page_Down => Some(Key::PageDown), + Num_Lock => Some(Key::NumLock), + + Up => Some(Key::ArrowUp), + Down => Some(Key::ArrowDown), + Left => Some(Key::ArrowLeft), + Right => Some(Key::ArrowRight), + Clear => Some(Key::Clear), + + Menu => Some(Key::ContextMenu), + WakeUp => Some(Key::WakeUp), + Launch0 => Some(Key::LaunchApplication1), + Launch1 => Some(Key::LaunchApplication2), + ISO_Level3_Shift => Some(Key::AltGraph), + + KP_Begin => Some(Key::Clear), + KP_Delete => Some(Key::Delete), + KP_Down => Some(Key::ArrowDown), + KP_End => Some(Key::End), + KP_Enter => Some(Key::Enter), + KP_F1 => Some(Key::F1), + KP_F2 => Some(Key::F2), + KP_F3 => Some(Key::F3), + KP_F4 => Some(Key::F4), + KP_Home => Some(Key::Home), + KP_Insert => Some(Key::Insert), + KP_Left => Some(Key::ArrowLeft), + KP_Page_Down => Some(Key::PageDown), + KP_Page_Up => Some(Key::PageUp), + KP_Right => Some(Key::ArrowRight), + // KP_Separator? What does it map to? + KP_Tab => Some(Key::Tab), + KP_Up => Some(Key::ArrowUp), + // TODO: more mappings (media etc) + _ => None, + } +} + +#[allow(clippy::just_underscores_and_digits, non_upper_case_globals)] +pub(crate) fn raw_key_to_location(raw: RawKey) -> KeyLocation { + match raw { + Control_L | Shift_L | Alt_L | Super_L | Meta_L => KeyLocation::Left, + Control_R | Shift_R | Alt_R | Super_R | Meta_R => KeyLocation::Right, + KP_0 | KP_1 | KP_2 | KP_3 | KP_4 | KP_5 | KP_6 | KP_7 | KP_8 | KP_9 | KP_Add | KP_Begin + | KP_Decimal | KP_Delete | KP_Divide | KP_Down | KP_End | KP_Enter | KP_Equal | KP_F1 + | KP_F2 | KP_F3 | KP_F4 | KP_Home | KP_Insert | KP_Left | KP_Multiply | KP_Page_Down + | KP_Page_Up | KP_Right | KP_Separator | KP_Space | KP_Subtract | KP_Tab | KP_Up => { + KeyLocation::Numpad + } + _ => KeyLocation::Standard, + } +} + +const MODIFIER_MAP: &[(Key<'static>, ModifiersState)] = &[ + (Key::Shift, ModifiersState::SHIFT), + (Key::Alt, ModifiersState::ALT), + (Key::Control, ModifiersState::CONTROL), + (Key::Super, ModifiersState::SUPER), +]; + +// we use the EventKey to extract the modifier mainly because +// we need to have the modifier before the second key is entered to follow +// other os' logic -- this way we can emit the new `ModifiersState` before +// we receive the next key, if needed the developer can update his local state. +pub(crate) fn get_modifiers(key: EventKey) -> ModifiersState { + // a keycode (scancode in Windows) is a code that refers to a physical keyboard key. + let scancode = key.hardware_keycode(); + // a keyval (keysym in X) is a "logical" key name, such as GDK_Enter, GDK_a, GDK_space, etc. + let keyval = key.keyval(); + // unicode value + let unicode = keyval.to_unicode(); + // translate to tao::keyboard::Key + let key_from_code = raw_key_to_key(keyval).unwrap_or_else(|| { + if let Some(key) = unicode { + if key >= ' ' && key != '\x7f' { + Key::Character(insert_or_get_key_str(key.to_string())) + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + }); + // start with empty state + let mut result = ModifiersState::empty(); + // loop trough our modifier map + for (gdk_mod, modifier) in MODIFIER_MAP { + if key_from_code == *gdk_mod { + result |= *modifier; + } + } + result +} + +pub(crate) fn make_key_event( + key: &EventKey, + is_repeat: bool, + key_override: Option, + state: ElementState, +) -> Option { + // a keycode (scancode in Windows) is a code that refers to a physical keyboard key. + let scancode = key.hardware_keycode(); + // a keyval (keysym in X) is a "logical" key name, such as GDK_Enter, GDK_a, GDK_space, etc. + let keyval_without_modifiers = key.keyval(); + let keyval_with_modifiers = + hardware_keycode_to_keyval(scancode).unwrap_or_else(|| keyval_without_modifiers.clone()); + // get unicode value, with and without modifiers + let text_without_modifiers = keyval_with_modifiers.to_unicode(); + let text_with_modifiers = keyval_without_modifiers.to_unicode(); + // get physical key from the scancode (keycode) + let physical_key = key_override.unwrap_or_else(|| KeyCode::from_scancode(scancode as u32)); + + // extract key without modifier + let key_without_modifiers = raw_key_to_key(keyval_with_modifiers.clone()).unwrap_or_else(|| { + if let Some(key) = text_without_modifiers { + if key >= ' ' && key != '\x7f' { + Key::Character(insert_or_get_key_str(key.to_string())) + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + }); + + // extract the logical key + let logical_key = raw_key_to_key(keyval_without_modifiers).unwrap_or_else(|| { + if let Some(key) = text_with_modifiers { + if key >= ' ' && key != '\x7f' { + Key::Character(insert_or_get_key_str(key.to_string())) + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + }); + + // make sure we have a valid key + if !matches!(key_without_modifiers, Key::Unidentified(_)) { + let location = raw_key_to_location(keyval_with_modifiers); + let text_with_all_modifiers = + text_without_modifiers.map(|text| insert_or_get_key_str(text.to_string())); + return Some(KeyEvent { + location, + logical_key, + physical_key, + repeat: is_repeat, + state, + text: text_with_all_modifiers, + platform_specific: KeyEventExtra { + text_with_all_modifiers, + key_without_modifiers, + }, + }); + } else { + #[cfg(debug_assertions)] + eprintln!("Couldn't get key from code: {physical_key:?}"); + } + None +} + +/// Map a hardware keycode to a keyval by performing a lookup in the keymap and finding the +/// keyval with the lowest group and level +fn hardware_keycode_to_keyval(keycode: u16) -> Option { + use glib::translate::FromGlib; + unsafe { + let keymap = gdk::ffi::gdk_keymap_get_default(); + + let mut nkeys = 0; + let mut keys: *mut gdk::ffi::GdkKeymapKey = ptr::null_mut(); + let mut keyvals: *mut c_uint = ptr::null_mut(); + + // call into gdk to retrieve the keyvals and keymap keys + gdk::ffi::gdk_keymap_get_entries_for_keycode( + keymap, + c_uint::from(keycode), + &mut keys as *mut *mut gdk::ffi::GdkKeymapKey, + &mut keyvals as *mut *mut c_uint, + &mut nkeys as *mut c_int, + ); + + if nkeys > 0 { + let keyvals_slice = slice::from_raw_parts(keyvals, nkeys as usize); + let keys_slice = slice::from_raw_parts(keys, nkeys as usize); + + let resolved_keyval = keys_slice.iter().enumerate().find_map(|(id, gdk_keymap)| { + if gdk_keymap.group == 0 && gdk_keymap.level == 0 { + Some(RawKey::from_glib(keyvals_slice[id])) + } else { + None + } + }); + + // notify glib to free the allocated arrays + glib::ffi::g_free(keyvals as *mut c_void); + glib::ffi::g_free(keys as *mut c_void); + + return resolved_keyval; + } + } + None +} diff --git a/vendor/tao/src/platform_impl/linux/keycode.rs b/vendor/tao/src/platform_impl/linux/keycode.rs new file mode 100644 index 0000000000..e63e6c9515 --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/keycode.rs @@ -0,0 +1,325 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use crate::keyboard::{KeyCode, NativeKeyCode}; + +pub fn keycode_to_scancode(code: KeyCode) -> Option { + // See `from_scancode` for more info + match code { + KeyCode::Backquote => Some(0x0031), + KeyCode::Backslash => Some(0x0033), + KeyCode::Backspace => Some(0x0016), + KeyCode::BracketLeft => Some(0x0022), + KeyCode::BracketRight => Some(0x001B), + KeyCode::Comma => Some(0x003B), + KeyCode::Digit0 => Some(0x0013), + KeyCode::Digit1 => Some(0x000A), + KeyCode::Digit2 => Some(0x000B), + KeyCode::Digit3 => Some(0x000C), + KeyCode::Digit4 => Some(0x000D), + KeyCode::Digit5 => Some(0x000E), + KeyCode::Digit6 => Some(0x000F), + KeyCode::Digit7 => Some(0x0010), + KeyCode::Digit8 => Some(0x0011), + KeyCode::Digit9 => Some(0x0012), + KeyCode::Equal => Some(0x0015), + KeyCode::IntlBackslash => Some(0x005E), + KeyCode::IntlRo => Some(0x0061), + KeyCode::IntlYen => Some(0x0084), + KeyCode::KeyA => Some(0x0026), + KeyCode::KeyB => Some(0x0038), + KeyCode::KeyC => Some(0x0036), + KeyCode::KeyD => Some(0x0028), + KeyCode::KeyE => Some(0x001A), + KeyCode::KeyF => Some(0x0029), + KeyCode::KeyG => Some(0x002A), + KeyCode::KeyH => Some(0x002B), + KeyCode::KeyI => Some(0x001F), + KeyCode::KeyJ => Some(0x002C), + KeyCode::KeyK => Some(0x002D), + KeyCode::KeyL => Some(0x002E), + KeyCode::KeyM => Some(0x002E), + KeyCode::KeyN => Some(0x0039), + KeyCode::KeyO => Some(0x0020), + KeyCode::KeyP => Some(0x0021), + KeyCode::KeyQ => Some(0x0018), + KeyCode::KeyR => Some(0x001B), + KeyCode::KeyS => Some(0x0027), + KeyCode::KeyT => Some(0x001C), + KeyCode::KeyU => Some(0x001E), + KeyCode::KeyV => Some(0x0037), + KeyCode::KeyW => Some(0x0019), + KeyCode::KeyX => Some(0x0035), + KeyCode::KeyY => Some(0x001D), + KeyCode::KeyZ => Some(0x0034), + KeyCode::Minus => Some(0x0014), + KeyCode::Period => Some(0x003C), + KeyCode::Quote => Some(0x0030), + KeyCode::Semicolon => Some(0x002F), + KeyCode::Slash => Some(0x003D), + KeyCode::AltLeft => Some(0x0040), + KeyCode::AltRight => Some(0x006C), + KeyCode::CapsLock => Some(0x0042), + KeyCode::ContextMenu => Some(0x0087), + KeyCode::ControlLeft => Some(0x0025), + KeyCode::ControlRight => Some(0x0069), + KeyCode::Enter => Some(0x0024), + KeyCode::SuperLeft => Some(0x0085), + KeyCode::SuperRight => Some(0x0086), + KeyCode::ShiftLeft => Some(0x0032), + KeyCode::ShiftRight => Some(0x003E), + KeyCode::Space => Some(0x0041), + KeyCode::Tab => Some(0x0017), + KeyCode::Convert => Some(0x0064), + KeyCode::Lang1 => Some(0x0082), + KeyCode::Lang2 => Some(0x0083), + KeyCode::KanaMode => Some(0x0065), + KeyCode::NonConvert => Some(0x0066), + KeyCode::Delete => Some(0x0077), + KeyCode::End => Some(0x0073), + KeyCode::Home => Some(0x006E), + KeyCode::Insert => Some(0x0076), + KeyCode::PageDown => Some(0x0075), + KeyCode::PageUp => Some(0x0070), + KeyCode::ArrowDown => Some(0x0074), + KeyCode::ArrowLeft => Some(0x0071), + KeyCode::ArrowRight => Some(0x0072), + KeyCode::ArrowUp => Some(0x006F), + KeyCode::NumLock => Some(0x004D), + KeyCode::Numpad0 => Some(0x005A), + KeyCode::Numpad1 => Some(0x0057), + KeyCode::Numpad2 => Some(0x0058), + KeyCode::Numpad3 => Some(0x0059), + KeyCode::Numpad4 => Some(0x0058), + KeyCode::Numpad5 => Some(0x0053), + KeyCode::Numpad6 => Some(0x0054), + KeyCode::Numpad7 => Some(0x0055), + KeyCode::Numpad8 => Some(0x0050), + KeyCode::Numpad9 => Some(0x0051), + KeyCode::NumpadAdd => Some(0x0056), + KeyCode::NumpadComma => Some(0x0081), + KeyCode::NumpadDecimal => Some(0x005B), + KeyCode::NumpadDivide => Some(0x006A), + KeyCode::NumpadEnter => Some(0x0068), + KeyCode::NumpadEqual => Some(0x007D), + KeyCode::NumpadMultiply => Some(0x003F), + KeyCode::NumpadSubtract => Some(0x0052), + KeyCode::Escape => Some(0x0009), + KeyCode::F1 => Some(0x0043), + KeyCode::F2 => Some(0x0044), + KeyCode::F3 => Some(0x0045), + KeyCode::F4 => Some(0x0046), + KeyCode::F5 => Some(0x0047), + KeyCode::F6 => Some(0x0048), + KeyCode::F7 => Some(0x0049), + KeyCode::F8 => Some(0x004A), + KeyCode::F9 => Some(0x004B), + KeyCode::F10 => Some(0x004C), + KeyCode::F11 => Some(0x005F), + KeyCode::F12 => Some(0x0060), + KeyCode::F13 => Some(0x00BF), + KeyCode::F14 => Some(0x00C0), + KeyCode::F15 => Some(0x00C1), + KeyCode::F16 => Some(0x00C2), + KeyCode::F17 => Some(0x00C3), + KeyCode::F18 => Some(0x00C4), + KeyCode::F19 => Some(0x00C5), + KeyCode::F20 => Some(0x00C6), + KeyCode::F21 => Some(0x00C7), + KeyCode::F22 => Some(0x00C8), + KeyCode::F23 => Some(0x00C9), + KeyCode::F24 => Some(0x00CA), + KeyCode::PrintScreen => Some(0x006B), + KeyCode::ScrollLock => Some(0x004E), + KeyCode::Pause => Some(0x007F), + KeyCode::BrowserBack => Some(0x00A6), + KeyCode::BrowserFavorites => Some(0x00A4), + KeyCode::BrowserForward => Some(0x00A7), + KeyCode::BrowserHome => Some(0x00B4), + KeyCode::BrowserRefresh => Some(0x00B5), + KeyCode::BrowserSearch => Some(0x00E1), + KeyCode::BrowserStop => Some(0x0088), + + KeyCode::LaunchApp1 => Some(0x0098), + KeyCode::LaunchApp2 => Some(0x0094), + KeyCode::LaunchMail => Some(0x00A3), + KeyCode::MediaPlayPause => Some(0x00AC), + KeyCode::MediaSelect => Some(0x00B3), + KeyCode::MediaStop => Some(0x00AE), + KeyCode::MediaTrackNext => Some(0x00AB), + KeyCode::MediaTrackPrevious => Some(0x00AD), + KeyCode::AudioVolumeDown => Some(0x007A), + KeyCode::AudioVolumeMute => Some(0x0079), + KeyCode::AudioVolumeUp => Some(0x007B), + KeyCode::Unidentified(NativeKeyCode::Gtk(scancode)) => Some(scancode as u32), + _ => None, + } +} + +pub fn keycode_from_scancode(scancode: u32) -> KeyCode { + // See: https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html + // and: https://www.w3.org/TR/uievents-code/ + // and: The widget/NativeKeyToDOMCodeName.h file in the firefox source + match scancode { + 0x0009 => KeyCode::Escape, + 0x000A => KeyCode::Digit1, + 0x000B => KeyCode::Digit2, + 0x000C => KeyCode::Digit3, + 0x000D => KeyCode::Digit4, + 0x000E => KeyCode::Digit5, + 0x000F => KeyCode::Digit6, + 0x0010 => KeyCode::Digit7, + 0x0011 => KeyCode::Digit8, + 0x0012 => KeyCode::Digit9, + 0x0013 => KeyCode::Digit0, + 0x0014 => KeyCode::Minus, + 0x0015 => KeyCode::Equal, + 0x0016 => KeyCode::Backspace, + 0x0017 => KeyCode::Tab, + 0x0018 => KeyCode::KeyQ, + 0x0019 => KeyCode::KeyW, + 0x001A => KeyCode::KeyE, + 0x001B => KeyCode::KeyR, + 0x001C => KeyCode::KeyT, + 0x001D => KeyCode::KeyY, + 0x001E => KeyCode::KeyU, + 0x001F => KeyCode::KeyI, + 0x0020 => KeyCode::KeyO, + 0x0021 => KeyCode::KeyP, + 0x0022 => KeyCode::BracketLeft, + 0x0023 => KeyCode::BracketRight, + 0x0024 => KeyCode::Enter, + 0x0025 => KeyCode::ControlLeft, + 0x0026 => KeyCode::KeyA, + 0x0027 => KeyCode::KeyS, + 0x0028 => KeyCode::KeyD, + 0x0029 => KeyCode::KeyF, + 0x002A => KeyCode::KeyG, + 0x002B => KeyCode::KeyH, + 0x002C => KeyCode::KeyJ, + 0x002D => KeyCode::KeyK, + 0x002E => KeyCode::KeyL, + 0x002F => KeyCode::Semicolon, + 0x0030 => KeyCode::Quote, + 0x0031 => KeyCode::Backquote, + 0x0032 => KeyCode::ShiftLeft, + 0x0033 => KeyCode::Backslash, + 0x0034 => KeyCode::KeyZ, + 0x0035 => KeyCode::KeyX, + 0x0036 => KeyCode::KeyC, + 0x0037 => KeyCode::KeyV, + 0x0038 => KeyCode::KeyB, + 0x0039 => KeyCode::KeyN, + 0x003A => KeyCode::KeyM, + 0x003B => KeyCode::Comma, + 0x003C => KeyCode::Period, + 0x003D => KeyCode::Slash, + 0x003E => KeyCode::ShiftRight, + 0x003F => KeyCode::NumpadMultiply, + 0x0040 => KeyCode::AltLeft, + 0x0041 => KeyCode::Space, + 0x0042 => KeyCode::CapsLock, + 0x0043 => KeyCode::F1, + 0x0044 => KeyCode::F2, + 0x0045 => KeyCode::F3, + 0x0046 => KeyCode::F4, + 0x0047 => KeyCode::F5, + 0x0048 => KeyCode::F6, + 0x0049 => KeyCode::F7, + 0x004A => KeyCode::F8, + 0x004B => KeyCode::F9, + 0x004C => KeyCode::F10, + 0x004D => KeyCode::NumLock, + 0x004E => KeyCode::ScrollLock, + 0x004F => KeyCode::Numpad7, + 0x0050 => KeyCode::Numpad8, + 0x0051 => KeyCode::Numpad9, + 0x0052 => KeyCode::NumpadSubtract, + 0x0053 => KeyCode::Numpad4, + 0x0054 => KeyCode::Numpad5, + 0x0055 => KeyCode::Numpad6, + 0x0056 => KeyCode::NumpadAdd, + 0x0057 => KeyCode::Numpad1, + 0x0058 => KeyCode::Numpad2, + 0x0059 => KeyCode::Numpad3, + 0x005A => KeyCode::Numpad0, + 0x005B => KeyCode::NumpadDecimal, + 0x005E => KeyCode::IntlBackslash, + 0x005F => KeyCode::F11, + 0x0060 => KeyCode::F12, + 0x0061 => KeyCode::IntlRo, + 0x0064 => KeyCode::Convert, + 0x0065 => KeyCode::KanaMode, + 0x0066 => KeyCode::NonConvert, + 0x0068 => KeyCode::NumpadEnter, + 0x0069 => KeyCode::ControlRight, + 0x006A => KeyCode::NumpadDivide, + 0x006B => KeyCode::PrintScreen, + 0x006C => KeyCode::AltRight, + 0x006E => KeyCode::Home, + 0x006F => KeyCode::ArrowUp, + 0x0070 => KeyCode::PageUp, + 0x0071 => KeyCode::ArrowLeft, + 0x0072 => KeyCode::ArrowRight, + 0x0073 => KeyCode::End, + 0x0074 => KeyCode::ArrowDown, + 0x0075 => KeyCode::PageDown, + 0x0076 => KeyCode::Insert, + 0x0077 => KeyCode::Delete, + 0x0079 => KeyCode::AudioVolumeMute, + 0x007A => KeyCode::AudioVolumeDown, + 0x007B => KeyCode::AudioVolumeUp, + 0x007D => KeyCode::NumpadEqual, + 0x007F => KeyCode::Pause, + 0x0081 => KeyCode::NumpadComma, + 0x0082 => KeyCode::Lang1, + 0x0083 => KeyCode::Lang2, + 0x0084 => KeyCode::IntlYen, + 0x0085 => KeyCode::SuperLeft, + 0x0086 => KeyCode::SuperRight, + 0x0087 => KeyCode::ContextMenu, + 0x0088 => KeyCode::BrowserStop, + 0x0089 => KeyCode::Again, + 0x008A => KeyCode::Props, + 0x008B => KeyCode::Undo, + 0x008C => KeyCode::Select, + 0x008D => KeyCode::Copy, + 0x008E => KeyCode::Open, + 0x008F => KeyCode::Paste, + 0x0090 => KeyCode::Find, + 0x0091 => KeyCode::Cut, + 0x0092 => KeyCode::Help, + 0x0094 => KeyCode::LaunchApp2, + 0x0097 => KeyCode::WakeUp, + 0x0098 => KeyCode::LaunchApp1, + // key to right of volume controls on T430s produces 0x9C + // but no documentation of what it should map to :/ + 0x00A3 => KeyCode::LaunchMail, + 0x00A4 => KeyCode::BrowserFavorites, + 0x00A6 => KeyCode::BrowserBack, + 0x00A7 => KeyCode::BrowserForward, + 0x00A9 => KeyCode::Eject, + 0x00AB => KeyCode::MediaTrackNext, + 0x00AC => KeyCode::MediaPlayPause, + 0x00AD => KeyCode::MediaTrackPrevious, + 0x00AE => KeyCode::MediaStop, + 0x00B3 => KeyCode::MediaSelect, + 0x00B4 => KeyCode::BrowserHome, + 0x00B5 => KeyCode::BrowserRefresh, + 0x00BF => KeyCode::F13, + 0x00C0 => KeyCode::F14, + 0x00C1 => KeyCode::F15, + 0x00C2 => KeyCode::F16, + 0x00C3 => KeyCode::F17, + 0x00C4 => KeyCode::F18, + 0x00C5 => KeyCode::F19, + 0x00C6 => KeyCode::F20, + 0x00C7 => KeyCode::F21, + 0x00C8 => KeyCode::F22, + 0x00C9 => KeyCode::F23, + 0x00CA => KeyCode::F24, + 0x00E1 => KeyCode::BrowserSearch, + _ => KeyCode::Unidentified(NativeKeyCode::Gtk(scancode as u16)), + } +} diff --git a/vendor/tao/src/platform_impl/linux/mod.rs b/vendor/tao/src/platform_impl/linux/mod.rs new file mode 100644 index 0000000000..704d3cf540 --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/mod.rs @@ -0,0 +1,97 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "x11")] +mod device; +mod event_loop; +mod icon; +mod keyboard; +mod keycode; +mod monitor; +mod util; +mod window; + +pub mod taskbar; +pub mod wayland; +#[cfg(feature = "x11")] +pub mod x11; + +pub use self::keycode::{keycode_from_scancode, keycode_to_scancode}; +pub(crate) use event_loop::PlatformSpecificEventLoopAttributes; +pub use event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; +pub use icon::PlatformIcon; +pub use monitor::{MonitorHandle, VideoMode}; +pub use window::{Window, WindowId}; + +use crate::{event::DeviceId as RootDeviceId, keyboard::Key}; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option<&'static str>, + pub key_without_modifiers: Key<'static>, +} + +#[non_exhaustive] +#[derive(Clone)] +pub enum Parent { + None, + ChildOf(gtk::Window), +} + +impl Default for Parent { + fn default() -> Self { + Parent::None + } +} + +#[derive(Clone)] +pub struct PlatformSpecificWindowBuilderAttributes { + pub parent: Parent, + pub skip_taskbar: bool, + pub auto_transparent: bool, + pub double_buffered: bool, + pub app_paintable: bool, + pub rgba_visual: bool, + pub cursor_moved: bool, + pub default_vbox: bool, +} + +impl Default for PlatformSpecificWindowBuilderAttributes { + fn default() -> Self { + Self { + parent: Default::default(), + skip_taskbar: Default::default(), + auto_transparent: true, + double_buffered: true, + app_paintable: false, + rgba_visual: false, + cursor_moved: true, + default_vbox: true, + } + } +} + +unsafe impl Send for PlatformSpecificWindowBuilderAttributes {} +unsafe impl Sync for PlatformSpecificWindowBuilderAttributes {} + +#[derive(Debug, Clone)] +pub struct OsError; + +impl std::fmt::Display for OsError { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + Ok(()) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(usize); + +impl DeviceId { + pub unsafe fn dummy() -> Self { + Self(0) + } +} + +// FIXME: currently we use a dummy device id, find if we can get device id from gtk +pub(crate) const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId(0)); diff --git a/vendor/tao/src/platform_impl/linux/monitor.rs b/vendor/tao/src/platform_impl/linux/monitor.rs new file mode 100644 index 0000000000..ceeea1265a --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/monitor.rs @@ -0,0 +1,96 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use gtk::gdk::{self, prelude::MonitorExt, Display}; + +use crate::{ + dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, +}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct MonitorHandle { + pub(crate) monitor: gdk::Monitor, +} + +impl MonitorHandle { + pub fn new(display: &gdk::Display, number: i32) -> Self { + let monitor = display.monitor(number).unwrap(); + Self { monitor } + } + + #[inline] + pub fn name(&self) -> Option { + self.monitor.model().map(|s| s.as_str().to_string()) + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + let rect = self.monitor.geometry(); + LogicalSize { + width: rect.width() as u32, + height: rect.height() as u32, + } + .to_physical(self.scale_factor()) + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + let rect = self.monitor.geometry(); + LogicalPosition { + x: rect.x(), + y: rect.y(), + } + .to_physical(self.scale_factor()) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.monitor.scale_factor() as f64 + } + + #[inline] + pub fn video_modes(&self) -> Box> { + Box::new(Vec::new().into_iter()) + } +} + +unsafe impl Send for MonitorHandle {} +unsafe impl Sync for MonitorHandle {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VideoMode; + +impl VideoMode { + #[inline] + pub fn size(&self) -> PhysicalSize { + panic!("VideoMode is unsupported on Linux.") + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + panic!("VideoMode is unsupported on Linux.") + } + + #[inline] + pub fn refresh_rate(&self) -> u16 { + panic!("VideoMode is unsupported on Linux.") + } + + #[inline] + pub fn monitor(&self) -> RootMonitorHandle { + panic!("VideoMode is unsupported on Linux.") + } +} + +pub fn from_point(display: &Display, x: f64, y: f64) -> Option { + if let Some(monitor) = display.monitor_at_point(x as i32, y as i32) { + (0..display.n_monitors()) + .map(|i| (i, display.monitor(i).unwrap())) + .find(|cur| cur.1.geometry() == monitor.geometry()) + .map(|x| MonitorHandle::new(display, x.0)) + } else { + None + } +} diff --git a/vendor/tao/src/platform_impl/linux/taskbar.rs b/vendor/tao/src/platform_impl/linux/taskbar.rs new file mode 100644 index 0000000000..ae7b98584f --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/taskbar.rs @@ -0,0 +1,161 @@ +use std::ffi::{c_char, CString}; + +use dlopen2::wrapper::{Container, WrapperApi}; + +use crate::window::{ProgressBarState, ProgressState}; + +#[derive(WrapperApi)] +struct UnityLib { + unity_launcher_entry_get_for_desktop_id: unsafe extern "C" fn(id: *const c_char) -> *const isize, + unity_inspector_get_default: unsafe extern "C" fn() -> *const isize, + unity_inspector_get_unity_running: unsafe extern "C" fn(inspector: *const isize) -> i32, + unity_launcher_entry_set_progress: unsafe extern "C" fn(entry: *const isize, value: f64) -> i32, + unity_launcher_entry_set_progress_visible: + unsafe extern "C" fn(entry: *const isize, value: i32) -> i32, + unity_launcher_entry_set_count: unsafe extern "C" fn(entry: *const isize, value: i64) -> i32, + unity_launcher_entry_set_count_visible: + unsafe extern "C" fn(entry: *const isize, value: bool) -> bool, +} + +pub struct TaskbarIndicator { + desktop_filename: Option, + desktop_filename_c_str: Option, + + unity_lib: Option>, + attempted_load: bool, + + unity_inspector: Option<*const isize>, + unity_entry: Option<*const isize>, +} + +impl TaskbarIndicator { + pub fn new() -> Self { + Self { + desktop_filename: None, + desktop_filename_c_str: None, + + unity_lib: None, + attempted_load: false, + + unity_inspector: None, + unity_entry: None, + } + } + + fn ensure_lib_load(&mut self) { + if self.attempted_load { + return; + } + + self.attempted_load = true; + + self.unity_lib = unsafe { + Container::load("libunity.so.4") + .or_else(|_| Container::load("libunity.so.6")) + .or_else(|_| Container::load("libunity.so.9")) + .ok() + }; + + if let Some(unity_lib) = &self.unity_lib { + let handle = unsafe { unity_lib.unity_inspector_get_default() }; + if !handle.is_null() { + self.unity_inspector = Some(handle); + } + } + } + + fn ensure_entry_load(&mut self) { + if let Some(unity_lib) = &self.unity_lib { + if let Some(id) = &self.desktop_filename_c_str { + let handle = unsafe { unity_lib.unity_launcher_entry_get_for_desktop_id(id.as_ptr()) }; + if !handle.is_null() { + self.unity_entry = Some(handle); + } + } + } + } + + fn is_unity_running(&self) -> bool { + if let Some(inspector) = self.unity_inspector { + if let Some(unity_lib) = &self.unity_lib { + return unsafe { unity_lib.unity_inspector_get_unity_running(inspector) } == 1; + } + } + + false + } + + pub fn update(&mut self, progress: ProgressBarState) { + if let Some(uri) = progress.desktop_filename { + self.desktop_filename = Some(uri); + } + + self.ensure_lib_load(); + + if !self.is_unity_running() { + return; + } + + if let Some(uri) = &self.desktop_filename { + self.desktop_filename_c_str = Some(CString::new(uri.as_str()).unwrap_or_default()); + } + + if self.unity_entry.is_none() { + self.ensure_entry_load(); + } + if let Some(unity_lib) = &self.unity_lib { + if let Some(unity_entry) = &self.unity_entry { + if let Some(progress) = progress.progress { + let progress = if progress > 100 { 100 } else { progress }; + let progress = progress as f64 / 100.0; + unsafe { (unity_lib.unity_launcher_entry_set_progress)(*unity_entry, progress) }; + } + + if let Some(state) = progress.state { + let is_visible = !matches!(state, ProgressState::None); + unsafe { + (unity_lib.unity_launcher_entry_set_progress_visible)( + *unity_entry, + if is_visible { 1 } else { 0 }, + ) + }; + } + } + } + } + + pub fn update_count(&mut self, count: Option, desktop_filename: Option) { + if let Some(uri) = desktop_filename { + self.desktop_filename = Some(uri); + } + + self.ensure_lib_load(); + + if !self.is_unity_running() { + return; + } + + if let Some(uri) = &self.desktop_filename { + self.desktop_filename_c_str = Some(CString::new(uri.as_str()).unwrap_or_default()); + } + + if self.unity_entry.is_none() { + self.ensure_entry_load(); + } + + if let Some(unity_lib) = &self.unity_lib { + if let Some(unity_entry) = &self.unity_entry { + // Sets count + if let Some(count) = count { + unsafe { (unity_lib.unity_launcher_entry_set_count)(*unity_entry, count) }; + unsafe { (unity_lib.unity_launcher_entry_set_count_visible)(*unity_entry, true) }; + } + // removes the count + else { + unsafe { (unity_lib.unity_launcher_entry_set_count)(*unity_entry, 0) }; + unsafe { (unity_lib.unity_launcher_entry_set_count_visible)(*unity_entry, false) }; + } + } + } + } +} diff --git a/vendor/tao/src/platform_impl/linux/util.rs b/vendor/tao/src/platform_impl/linux/util.rs new file mode 100644 index 0000000000..30cd8523bd --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/util.rs @@ -0,0 +1,111 @@ +use crate::{ + dpi::{LogicalPosition, LogicalSize, PhysicalPosition}, + error::ExternalError, + window::WindowSizeConstraints, +}; +use gtk::{ + gdk::{ + self, + prelude::{DeviceExt, SeatExt}, + Display, + }, + glib::{self}, + traits::{GtkWindowExt, WidgetExt}, +}; +use std::{cell::RefCell, rc::Rc}; + +#[inline] +pub fn cursor_position(is_wayland: bool) -> Result, ExternalError> { + if is_wayland { + Ok((0, 0).into()) + } else { + Display::default() + .map(|d| { + ( + d.default_seat().and_then(|s| s.pointer()), + d.default_group(), + ) + }) + .map(|(p, g)| { + p.map(|p| { + let (_, x, y) = p.position_double(); + LogicalPosition::new(x, y).to_physical(g.scale_factor() as _) + }) + }) + .map(|p| p.ok_or(ExternalError::Os(os_error!(super::OsError)))) + .ok_or(ExternalError::Os(os_error!(super::OsError)))? + } +} + +pub fn set_size_constraints( + window: &W, + constraints: WindowSizeConstraints, +) { + let mut geom_mask = gdk::WindowHints::empty(); + if constraints.has_min() { + geom_mask |= gdk::WindowHints::MIN_SIZE; + } + if constraints.has_max() { + geom_mask |= gdk::WindowHints::MAX_SIZE; + } + + let scale_factor = window.scale_factor() as f64; + + let min_size: LogicalSize = constraints.min_size_logical(scale_factor); + let max_size: LogicalSize = constraints.max_size_logical(scale_factor); + + let picky_none: Option<>k::Window> = None; + window.set_geometry_hints( + picky_none, + Some(&gdk::Geometry::new( + min_size.width, + min_size.height, + max_size.width, + max_size.height, + 0, + 0, + 0, + 0, + 0f64, + 0f64, + gdk::Gravity::Center, + )), + geom_mask, + ) +} + +pub struct WindowMaximizeProcess { + window: W, + resizable: bool, + step: u8, +} + +impl WindowMaximizeProcess { + pub fn new(window: W, resizable: bool) -> Rc> { + Rc::new(RefCell::new(Self { + window, + resizable, + step: 0, + })) + } + + pub fn next_step(&mut self) -> glib::ControlFlow { + match self.step { + 0 => { + self.window.set_resizable(true); + self.step += 1; + glib::ControlFlow::Continue + } + 1 => { + self.window.maximize(); + self.step += 1; + glib::ControlFlow::Continue + } + 2 => { + self.window.set_resizable(self.resizable); + glib::ControlFlow::Break + } + _ => glib::ControlFlow::Break, + } + } +} diff --git a/vendor/tao/src/platform_impl/linux/wayland/header.rs b/vendor/tao/src/platform_impl/linux/wayland/header.rs new file mode 100644 index 0000000000..480eb4c85f --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/wayland/header.rs @@ -0,0 +1,36 @@ +use gtk::{prelude::*, ApplicationWindow, EventBox, HeaderBar}; + +pub struct WlHeader; + +impl WlHeader { + pub fn setup(window: &ApplicationWindow, title: &str) { + let header = HeaderBar::builder() + .show_close_button(true) + .decoration_layout("menu:minimize,maximize,close") + .title(title) + .build(); + + let event_box = EventBox::new(); + event_box.set_above_child(true); + event_box.set_visible(true); + event_box.set_can_focus(false); + event_box.add(&header); + + window.set_titlebar(Some(&event_box)); + Self::connect_resize_window(&header, window); + } + + fn connect_resize_window(header: &HeaderBar, window: &ApplicationWindow) { + let header_weak = header.downgrade(); + window.connect_resizable_notify(move |window| { + if let Some(header) = header_weak.upgrade() { + let is_resizable = window.is_resizable(); + header.set_decoration_layout(if !is_resizable { + Some("menu:minimize,close") + } else { + Some("menu:minimize,maximize,close") + }); + } + }); + } +} diff --git a/vendor/tao/src/platform_impl/linux/wayland/mod.rs b/vendor/tao/src/platform_impl/linux/wayland/mod.rs new file mode 100644 index 0000000000..a200d26035 --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/wayland/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +pub mod header; diff --git a/vendor/tao/src/platform_impl/linux/window.rs b/vendor/tao/src/platform_impl/linux/window.rs new file mode 100644 index 0000000000..1f55e86969 --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/window.rs @@ -0,0 +1,1103 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + cell::RefCell, + collections::VecDeque, + rc::Rc, + sync::{ + atomic::{AtomicBool, AtomicI32, Ordering}, + Arc, + }, +}; + +use gtk::{ + gdk::WindowState, + glib::{self, translate::ToGlibPtr}, + prelude::*, + CssProvider, Settings, +}; + +use crate::{ + dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, + error::{ExternalError, NotSupportedError, OsError as RootOsError}, + icon::Icon, + monitor::MonitorHandle as RootMonitorHandle, + platform_impl::wayland::header::WlHeader, + window::{ + CursorIcon, Fullscreen, ProgressBarState, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowSizeConstraints, RGBA, + }, +}; + +use super::{ + event_loop::EventLoopWindowTarget, + monitor::{self, MonitorHandle}, + util, Parent, PlatformSpecificWindowBuilderAttributes, +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(pub(crate) u32); + +impl WindowId { + pub fn dummy() -> Self { + WindowId(u32::MAX) + } +} + +// Currently GTK doesn't provide feature for detect theme, so we need to check theme manually. +// ref: https://github.com/WebKit/WebKit/blob/e44ffaa0d999a9807f76f1805943eea204cfdfbc/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp#L587 +const GTK_THEME_SUFFIX_LIST: [&'static str; 3] = ["-dark", "-Dark", "-Darker"]; + +pub struct Window { + /// Window id. + pub(crate) window_id: WindowId, + /// Gtk application window. + pub(crate) window: gtk::ApplicationWindow, + pub(crate) default_vbox: Option, + /// Window requests sender + pub(crate) window_requests_tx: glib::Sender<(WindowId, WindowRequest)>, + scale_factor: Rc, + inner_position: Rc<(AtomicI32, AtomicI32)>, + outer_position: Rc<(AtomicI32, AtomicI32)>, + outer_size: Rc<(AtomicI32, AtomicI32)>, + inner_size: Rc<(AtomicI32, AtomicI32)>, + maximized: Rc, + is_always_on_top: Rc, + minimized: Rc, + fullscreen: RefCell>, + inner_size_constraints: RefCell, + /// Draw event Sender + draw_tx: crossbeam_channel::Sender, + preferred_theme: RefCell>, + css_provider: CssProvider, +} + +impl Window { + pub(crate) fn new( + event_loop_window_target: &EventLoopWindowTarget, + attributes: WindowAttributes, + pl_attribs: PlatformSpecificWindowBuilderAttributes, + ) -> Result { + let app = &event_loop_window_target.app; + let window_requests_tx = event_loop_window_target.window_requests_tx.clone(); + let draw_tx = event_loop_window_target.draw_tx.clone(); + let is_wayland = event_loop_window_target.is_wayland(); + + let mut window_builder = gtk::ApplicationWindow::builder() + .application(app) + .accept_focus(attributes.focusable && attributes.focused); + if let Parent::ChildOf(parent) = pl_attribs.parent { + window_builder = window_builder.transient_for(&parent); + } + + let window = window_builder.build(); + + if is_wayland { + WlHeader::setup(&window, &attributes.title); + } + + let window_id = WindowId(window.id()); + event_loop_window_target + .windows + .borrow_mut() + .insert(window_id); + + // Set Width/Height & Resizable + let win_scale_factor = window.scale_factor(); + let (width, height) = attributes + .inner_size + .map(|size| size.to_logical::(win_scale_factor as f64).into()) + .unwrap_or((800, 600)); + window.set_default_size(1, 1); + window.resize(width, height); + + if attributes.maximized { + let maximize_process = util::WindowMaximizeProcess::new(window.clone(), attributes.resizable); + glib::idle_add_local_full(glib::Priority::HIGH_IDLE, move || { + let mut maximize_process = maximize_process.borrow_mut(); + maximize_process.next_step() + }); + } else { + window.set_resizable(attributes.resizable); + } + + window.set_deletable(attributes.closable); + + // Set Min/Max Size + util::set_size_constraints(&window, attributes.inner_size_constraints); + + // Set Position + if let Some(position) = attributes.position { + let (x, y): (i32, i32) = position.to_logical::(win_scale_factor as f64).into(); + window.move_(x, y); + } + + // Set GDK Visual + if pl_attribs.rgba_visual || attributes.transparent { + if let Some(screen) = GtkWindowExt::screen(&window) { + if let Some(visual) = screen.rgba_visual() { + window.set_visual(Some(&visual)); + } + } + } + + if pl_attribs.app_paintable || attributes.transparent { + // Set a few attributes to make the window can be painted. + // See Gtk drawing model for more info: + // https://docs.gtk.org/gtk3/drawing-model.html + window.set_app_paintable(true); + } + + if !pl_attribs.double_buffered { + let widget = window.upcast_ref::(); + if !event_loop_window_target.is_wayland() { + unsafe { + gtk::ffi::gtk_widget_set_double_buffered(widget.to_glib_none().0, 0); + } + } + } + + let default_vbox = if pl_attribs.default_vbox { + let box_ = gtk::Box::new(gtk::Orientation::Vertical, 0); + window.add(&box_); + Some(box_) + } else { + None + }; + + // Rest attributes + window.set_title(&attributes.title); + if let Some(Fullscreen::Borderless(m)) = &attributes.fullscreen { + if let Some(monitor) = m { + let display = window.display(); + let monitor = &monitor.inner; + let monitors = display.n_monitors(); + for i in 0..monitors { + let m = display.monitor(i).unwrap(); + if m == monitor.monitor { + let screen = display.default_screen(); + window.fullscreen_on_monitor(&screen, i); + } + } + } else { + window.fullscreen(); + } + } + window.set_visible(attributes.visible); + window.set_decorated(attributes.decorations); + + if attributes.always_on_bottom { + window.set_keep_below(attributes.always_on_bottom); + } + + if attributes.always_on_top { + window.set_keep_above(attributes.always_on_top); + } + + if attributes.visible_on_all_workspaces { + window.stick(); + } + + let preferred_theme = if let Some(settings) = Settings::default() { + if let Some(preferred_theme) = attributes.preferred_theme { + match preferred_theme { + Theme::Dark => settings.set_gtk_application_prefer_dark_theme(true), + Theme::Light => { + if let Some(theme) = settings.gtk_theme_name() { + let theme = theme.as_str(); + // Remove dark variant. + if let Some(theme) = GTK_THEME_SUFFIX_LIST + .iter() + .find(|t| theme.ends_with(*t)) + .map(|v| theme.strip_suffix(v)) + { + settings.set_gtk_theme_name(theme); + } + } + } + } + } + attributes.preferred_theme + } else { + None + }; + + if attributes.visible { + window.show_all(); + } else { + window.hide(); + } + + // restore accept-focus after the window has been drawn + // if the window was initially created without focus and is supposed to be focusable + if attributes.focusable && !attributes.focused { + let signal_id = Arc::new(RefCell::new(None)); + let signal_id_ = signal_id.clone(); + let id = window.connect_draw(move |window, _| { + if let Some(id) = signal_id_.take() { + window.set_accept_focus(true); + window.disconnect(id); + } + glib::Propagation::Proceed + }); + signal_id.borrow_mut().replace(id); + } + + // Check if we should paint the transparent background ourselves. + let mut transparent = false; + if attributes.transparent && pl_attribs.auto_transparent { + transparent = true; + } + let cursor_moved = pl_attribs.cursor_moved; + if let Err(e) = window_requests_tx.send(( + window_id, + WindowRequest::WireUpEvents { + transparent, + fullscreen: attributes.fullscreen.is_some(), + cursor_moved, + }, + )) { + log::warn!("Fail to send wire up events request: {}", e); + } + + let ( + scale_factor, + outer_position, + inner_position, + outer_size, + inner_size, + maximized, + minimized, + is_always_on_top, + ) = Self::setup_signals(&window, Some(&attributes)); + + if let Some(icon) = attributes.window_icon { + window.set_icon(Some(&icon.inner.into())); + } + + let win = Self { + window_id, + window, + default_vbox, + window_requests_tx, + draw_tx, + scale_factor, + outer_position, + inner_position, + outer_size, + inner_size, + maximized, + minimized, + is_always_on_top, + fullscreen: RefCell::new(attributes.fullscreen), + inner_size_constraints: RefCell::new(attributes.inner_size_constraints), + preferred_theme: RefCell::new(preferred_theme), + css_provider: CssProvider::new(), + }; + + let _ = win.set_skip_taskbar(pl_attribs.skip_taskbar); + win.set_background_color(attributes.background_color); + + Ok(win) + } + + fn setup_signals( + window: >k::ApplicationWindow, + attributes: Option<&WindowAttributes>, + ) -> ( + Rc, + Rc<(AtomicI32, AtomicI32)>, + Rc<(AtomicI32, AtomicI32)>, + Rc<(AtomicI32, AtomicI32)>, + Rc<(AtomicI32, AtomicI32)>, + Rc, + Rc, + Rc, + ) { + let win_scale_factor = window.scale_factor(); + + let w_pos = window.position(); + let inner_position: Rc<(AtomicI32, AtomicI32)> = Rc::new((w_pos.0.into(), w_pos.1.into())); + let inner_position_clone = inner_position.clone(); + + let o_pos = window.window().map(|w| w.root_origin()).unwrap_or(w_pos); + let outer_position: Rc<(AtomicI32, AtomicI32)> = Rc::new((o_pos.0.into(), o_pos.1.into())); + let outer_position_clone = outer_position.clone(); + + let w_size = window.size(); + let inner_size: Rc<(AtomicI32, AtomicI32)> = Rc::new((w_size.0.into(), w_size.1.into())); + let inner_size_clone = inner_size.clone(); + + let o_size = window.window().map(|w| w.root_origin()).unwrap_or(w_pos); + let outer_size: Rc<(AtomicI32, AtomicI32)> = Rc::new((o_size.0.into(), o_size.1.into())); + let outer_size_clone = outer_size.clone(); + + window.connect_configure_event(move |window, event| { + let (x, y) = event.position(); + inner_position_clone.0.store(x, Ordering::Release); + inner_position_clone.1.store(y, Ordering::Release); + + let (w, h) = event.size(); + inner_size_clone.0.store(w as i32, Ordering::Release); + inner_size_clone.1.store(h as i32, Ordering::Release); + + let (x, y, w, h) = window + .window() + .map(|w| { + let rect = w.frame_extents(); + (rect.x(), rect.y(), rect.width(), rect.height()) + }) + .unwrap_or((x, y, w as i32, h as i32)); + + outer_position_clone.0.store(x, Ordering::Release); + outer_position_clone.1.store(y, Ordering::Release); + + outer_size_clone.0.store(w, Ordering::Release); + outer_size_clone.1.store(h, Ordering::Release); + + false + }); + + let w_max = window.is_maximized(); + let maximized: Rc = Rc::new(w_max.into()); + let max_clone = maximized.clone(); + let minimized = Rc::new(AtomicBool::new(false)); + let minimized_clone = minimized.clone(); + let is_always_on_top = Rc::new(AtomicBool::new( + attributes.map(|a| a.always_on_top).unwrap_or(false), + )); + let is_always_on_top_clone = is_always_on_top.clone(); + + window.connect_window_state_event(move |_, event| { + let state = event.new_window_state(); + max_clone.store(state.contains(WindowState::MAXIMIZED), Ordering::Release); + minimized_clone.store(state.contains(WindowState::ICONIFIED), Ordering::Release); + is_always_on_top_clone.store(state.contains(WindowState::ABOVE), Ordering::Release); + glib::Propagation::Proceed + }); + + let scale_factor: Rc = Rc::new(win_scale_factor.into()); + let scale_factor_clone = scale_factor.clone(); + window.connect_scale_factor_notify(move |window| { + scale_factor_clone.store(window.scale_factor(), Ordering::Release); + }); + + ( + scale_factor, + outer_position, + inner_position, + outer_size, + inner_size, + maximized, + minimized, + is_always_on_top, + ) + } + + pub(crate) fn new_from_gtk_window( + event_loop_window_target: &EventLoopWindowTarget, + window: gtk::ApplicationWindow, + ) -> Result { + let window_requests_tx = event_loop_window_target.window_requests_tx.clone(); + let draw_tx = event_loop_window_target.draw_tx.clone(); + + let window_id = WindowId(window.id()); + event_loop_window_target + .windows + .borrow_mut() + .insert(window_id); + + let ( + scale_factor, + outer_position, + inner_position, + outer_size, + inner_size, + maximized, + minimized, + is_always_on_top, + ) = Self::setup_signals(&window, None); + + let win = Self { + window_id, + window, + default_vbox: None, + window_requests_tx, + draw_tx, + scale_factor, + outer_position, + inner_position, + outer_size, + inner_size, + maximized, + minimized, + is_always_on_top, + fullscreen: RefCell::new(None), + inner_size_constraints: RefCell::new(WindowSizeConstraints::default()), + preferred_theme: RefCell::new(None), + css_provider: CssProvider::new(), + }; + + Ok(win) + } + + pub fn id(&self) -> WindowId { + self.window_id + } + + pub fn scale_factor(&self) -> f64 { + self.scale_factor.load(Ordering::Acquire) as f64 + } + + pub fn request_redraw(&self) { + if let Err(e) = self.draw_tx.send(self.window_id) { + log::warn!("Failed to send redraw event to event channel: {}", e); + } + } + + pub fn inner_position(&self) -> Result, NotSupportedError> { + let (x, y) = &*self.inner_position; + Ok( + LogicalPosition::new(x.load(Ordering::Acquire), y.load(Ordering::Acquire)) + .to_physical(self.scale_factor.load(Ordering::Acquire) as f64), + ) + } + + pub fn outer_position(&self) -> Result, NotSupportedError> { + let (x, y) = &*self.outer_position; + Ok( + LogicalPosition::new(x.load(Ordering::Acquire), y.load(Ordering::Acquire)) + .to_physical(self.scale_factor.load(Ordering::Acquire) as f64), + ) + } + + pub fn set_outer_position>(&self, position: P) { + let (x, y): (i32, i32) = position + .into() + .to_logical::(self.scale_factor()) + .into(); + + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::Position((x, y)))) + { + log::warn!("Fail to send position request: {}", e); + } + } + + pub fn set_background_color(&self, color: Option) { + if let Err(e) = self.window_requests_tx.send(( + self.window_id, + WindowRequest::BackgroundColor(self.css_provider.clone(), color), + )) { + log::warn!("Fail to send size request: {}", e); + } + } + + pub fn inner_size(&self) -> PhysicalSize { + let (width, height) = &*self.inner_size; + + LogicalSize::new( + width.load(Ordering::Acquire) as u32, + height.load(Ordering::Acquire) as u32, + ) + .to_physical(self.scale_factor.load(Ordering::Acquire) as f64) + } + + pub fn set_inner_size>(&self, size: S) { + let (width, height) = size.into().to_logical::(self.scale_factor()).into(); + + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::Size((width, height)))) + { + log::warn!("Fail to send size request: {}", e); + } + } + + pub fn outer_size(&self) -> PhysicalSize { + let (width, height) = &*self.outer_size; + + LogicalSize::new( + width.load(Ordering::Acquire) as u32, + height.load(Ordering::Acquire) as u32, + ) + .to_physical(self.scale_factor.load(Ordering::Acquire) as f64) + } + + fn set_size_constraints(&self, constraints: WindowSizeConstraints) { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::SizeConstraints(constraints))) + { + log::warn!("Fail to send size constraint request: {}", e); + } + } + + pub fn set_min_inner_size(&self, size: Option) { + let (width, height) = size.map(crate::extract_width_height).unzip(); + let mut size_constraints = self.inner_size_constraints.borrow_mut(); + size_constraints.min_width = width; + size_constraints.min_height = height; + self.set_size_constraints(*size_constraints) + } + + pub fn set_max_inner_size(&self, size: Option) { + let (width, height) = size.map(crate::extract_width_height).unzip(); + let mut size_constraints = self.inner_size_constraints.borrow_mut(); + size_constraints.max_width = width; + size_constraints.max_height = height; + self.set_size_constraints(*size_constraints) + } + + pub fn set_inner_size_constraints(&self, constraints: WindowSizeConstraints) { + *self.inner_size_constraints.borrow_mut() = constraints; + self.set_size_constraints(constraints) + } + + pub fn set_title(&self, title: &str) { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::Title(title.to_string()))) + { + log::warn!("Fail to send title request: {}", e); + } + } + + pub fn title(&self) -> String { + self + .window + .title() + .map(|t| t.as_str().to_string()) + .unwrap_or_default() + } + + pub fn set_visible(&self, visible: bool) { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::Visible(visible))) + { + log::warn!("Fail to send visible request: {}", e); + } + } + + pub fn set_focus(&self) { + if !self.minimized.load(Ordering::Acquire) && self.window.get_visible() { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::Focus)) + { + log::warn!("Fail to send visible request: {}", e); + } + } + } + + pub fn set_focusable(&self, focusable: bool) { + self.window.set_accept_focus(focusable); + } + + pub fn is_focused(&self) -> bool { + self.window.is_active() + } + + pub fn set_resizable(&self, resizable: bool) { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::Resizable(resizable))) + { + log::warn!("Fail to send resizable request: {}", e); + } + } + + pub fn set_minimizable(&self, _minimizable: bool) {} + + pub fn set_maximizable(&self, _maximizable: bool) {} + + pub fn set_closable(&self, closable: bool) { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::Closable(closable))) + { + log::warn!("Fail to send closable request: {}", e); + } + } + + pub fn set_minimized(&self, minimized: bool) { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::Minimized(minimized))) + { + log::warn!("Fail to send minimized request: {}", e); + } + } + + pub fn set_maximized(&self, maximized: bool) { + let resizable = self.is_resizable(); + + if let Err(e) = self.window_requests_tx.send(( + self.window_id, + WindowRequest::Maximized(maximized, resizable), + )) { + log::warn!("Fail to send maximized request: {}", e); + } + } + + pub fn is_always_on_top(&self) -> bool { + self.is_always_on_top.load(Ordering::Acquire) + } + + pub fn is_maximized(&self) -> bool { + self.maximized.load(Ordering::Acquire) + } + + pub fn is_minimized(&self) -> bool { + self.minimized.load(Ordering::Acquire) + } + + pub fn is_resizable(&self) -> bool { + self.window.is_resizable() + } + + pub fn is_minimizable(&self) -> bool { + true + } + + pub fn is_maximizable(&self) -> bool { + true + } + pub fn is_closable(&self) -> bool { + self.window.is_deletable() + } + + pub fn is_decorated(&self) -> bool { + self.window.is_decorated() + } + + #[inline] + pub fn is_visible(&self) -> bool { + self.window.is_visible() + } + + pub fn drag_window(&self) -> Result<(), ExternalError> { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::DragWindow)) + { + log::warn!("Fail to send drag window request: {}", e); + } + Ok(()) + } + + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::DragResizeWindow(direction))) + { + log::warn!("Fail to send drag window request: {}", e); + } + Ok(()) + } + + pub fn set_fullscreen(&self, fullscreen: Option) { + self.fullscreen.replace(fullscreen.clone()); + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::Fullscreen(fullscreen))) + { + log::warn!("Fail to send fullscreen request: {}", e); + } + } + + pub fn fullscreen(&self) -> Option { + self.fullscreen.borrow().clone() + } + + pub fn set_decorations(&self, decorations: bool) { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::Decorations(decorations))) + { + log::warn!("Fail to send decorations request: {}", e); + } + } + + pub fn set_always_on_bottom(&self, always_on_bottom: bool) { + if let Err(e) = self.window_requests_tx.send(( + self.window_id, + WindowRequest::AlwaysOnBottom(always_on_bottom), + )) { + log::warn!("Fail to send always on bottom request: {}", e); + } + } + + pub fn set_always_on_top(&self, always_on_top: bool) { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::AlwaysOnTop(always_on_top))) + { + log::warn!("Fail to send always on top request: {}", e); + } + } + + pub fn set_window_icon(&self, window_icon: Option) { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::WindowIcon(window_icon))) + { + log::warn!("Fail to send window icon request: {}", e); + } + } + + pub fn set_ime_position>(&self, _position: P) { + //TODO + } + + pub fn request_user_attention(&self, request_type: Option) { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::UserAttention(request_type))) + { + log::warn!("Fail to send user attention request: {}", e); + } + } + + pub fn set_visible_on_all_workspaces(&self, visible: bool) { + if let Err(e) = self.window_requests_tx.send(( + self.window_id, + WindowRequest::SetVisibleOnAllWorkspaces(visible), + )) { + log::warn!("Fail to send visible on all workspaces request: {}", e); + } + } + pub fn set_cursor_icon(&self, cursor: CursorIcon) { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::CursorIcon(Some(cursor)))) + { + log::warn!("Fail to send cursor icon request: {}", e); + } + } + + pub fn set_cursor_position>(&self, position: P) -> Result<(), ExternalError> { + let inner_pos = self.inner_position().unwrap_or_default(); + let (x, y): (i32, i32) = position + .into() + .to_logical::(self.scale_factor()) + .into(); + + if let Err(e) = self.window_requests_tx.send(( + self.window_id, + WindowRequest::CursorPosition((x + inner_pos.x, y + inner_pos.y)), + )) { + log::warn!("Fail to send cursor position request: {}", e); + } + + Ok(()) + } + + pub fn set_cursor_grab(&self, _grab: bool) -> Result<(), ExternalError> { + Ok(()) + } + + pub fn set_ignore_cursor_events(&self, ignore: bool) -> Result<(), ExternalError> { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::CursorIgnoreEvents(ignore))) + { + log::warn!("Fail to send cursor position request: {}", e); + } + + Ok(()) + } + + pub fn set_cursor_visible(&self, visible: bool) { + let cursor = if visible { + Some(CursorIcon::Default) + } else { + None + }; + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::CursorIcon(cursor))) + { + log::warn!("Fail to send cursor visibility request: {}", e); + } + } + + #[inline] + pub fn cursor_position(&self) -> Result, ExternalError> { + util::cursor_position(self.is_wayland()) + } + + pub fn current_monitor(&self) -> Option { + let display = self.window.display(); + // `.window()` returns `None` if the window is invisible; + // we fallback to the primary monitor + let monitor = self + .window + .window() + .and_then(|window| display.monitor_at_window(&window)) + .or_else(|| display.primary_monitor()); + + monitor.map(|monitor| RootMonitorHandle { + inner: MonitorHandle { monitor }, + }) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + let mut handles = VecDeque::new(); + let display = self.window.display(); + let numbers = display.n_monitors(); + + for i in 0..numbers { + let monitor = MonitorHandle::new(&display, i); + handles.push_back(monitor); + } + + handles + } + + pub fn primary_monitor(&self) -> Option { + let display = self.window.display(); + display.primary_monitor().map(|monitor| { + let handle = MonitorHandle { monitor }; + RootMonitorHandle { inner: handle } + }) + } + + #[inline] + pub fn monitor_from_point(&self, x: f64, y: f64) -> Option { + let display = &self.window.display(); + monitor::from_point(display, x, y).map(|inner| RootMonitorHandle { inner }) + } + + fn is_wayland(&self) -> bool { + self.window.display().backend().is_wayland() + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + if self.is_wayland() { + let mut window_handle = rwh_04::WaylandHandle::empty(); + if let Some(window) = self.window.window() { + window_handle.surface = + unsafe { gdk_wayland_sys::gdk_wayland_window_get_wl_surface(window.as_ptr() as *mut _) }; + } + + rwh_04::RawWindowHandle::Wayland(window_handle) + } else { + let mut window_handle = rwh_04::XlibHandle::empty(); + unsafe { + if let Some(window) = self.window.window() { + window_handle.window = gdk_x11_sys::gdk_x11_window_get_xid(window.as_ptr() as *mut _); + } + } + rwh_04::RawWindowHandle::Xlib(window_handle) + } + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + if self.is_wayland() { + let mut window_handle = rwh_05::WaylandWindowHandle::empty(); + if let Some(window) = self.window.window() { + window_handle.surface = + unsafe { gdk_wayland_sys::gdk_wayland_window_get_wl_surface(window.as_ptr() as *mut _) }; + } + + rwh_05::RawWindowHandle::Wayland(window_handle) + } else { + let mut window_handle = rwh_05::XlibWindowHandle::empty(); + unsafe { + if let Some(window) = self.window.window() { + window_handle.window = gdk_x11_sys::gdk_x11_window_get_xid(window.as_ptr() as *mut _); + } + } + rwh_05::RawWindowHandle::Xlib(window_handle) + } + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + if self.is_wayland() { + let mut display_handle = rwh_05::WaylandDisplayHandle::empty(); + display_handle.display = unsafe { + gdk_wayland_sys::gdk_wayland_display_get_wl_display(self.window.display().as_ptr() as *mut _) + }; + rwh_05::RawDisplayHandle::Wayland(display_handle) + } else { + let mut display_handle = rwh_05::XlibDisplayHandle::empty(); + unsafe { + if let Ok(xlib) = x11_dl::xlib::Xlib::open() { + let display = (xlib.XOpenDisplay)(std::ptr::null()); + display_handle.display = display as _; + display_handle.screen = (xlib.XDefaultScreen)(display) as _; + } + } + + rwh_05::RawDisplayHandle::Xlib(display_handle) + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + if let Some(window) = self.window.window() { + if self.is_wayland() { + let surface = + unsafe { gdk_wayland_sys::gdk_wayland_window_get_wl_surface(window.as_ptr() as *mut _) }; + let surface = unsafe { std::ptr::NonNull::new_unchecked(surface) }; + let window_handle = rwh_06::WaylandWindowHandle::new(surface); + Ok(rwh_06::RawWindowHandle::Wayland(window_handle)) + } else { + #[cfg(feature = "x11")] + { + let xid = unsafe { gdk_x11_sys::gdk_x11_window_get_xid(window.as_ptr() as *mut _) }; + let window_handle = rwh_06::XlibWindowHandle::new(xid); + Ok(rwh_06::RawWindowHandle::Xlib(window_handle)) + } + #[cfg(not(feature = "x11"))] + Err(rwh_06::HandleError::Unavailable) + } + } else { + Err(rwh_06::HandleError::Unavailable) + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06(&self) -> Result { + if self.is_wayland() { + let display = unsafe { + gdk_wayland_sys::gdk_wayland_display_get_wl_display(self.window.display().as_ptr() as *mut _) + }; + let display = unsafe { std::ptr::NonNull::new_unchecked(display) }; + let display_handle = rwh_06::WaylandDisplayHandle::new(display); + Ok(rwh_06::RawDisplayHandle::Wayland(display_handle)) + } else { + #[cfg(feature = "x11")] + if let Ok(xlib) = x11_dl::xlib::Xlib::open() { + unsafe { + let display = (xlib.XOpenDisplay)(std::ptr::null()); + let screen = (xlib.XDefaultScreen)(display) as _; + let display = std::ptr::NonNull::new_unchecked(display as _); + let display_handle = rwh_06::XlibDisplayHandle::new(Some(display), screen); + Ok(rwh_06::RawDisplayHandle::Xlib(display_handle)) + } + } else { + Err(rwh_06::HandleError::Unavailable) + } + #[cfg(not(feature = "x11"))] + Err(rwh_06::HandleError::Unavailable) + } + } + + pub fn set_skip_taskbar(&self, skip: bool) -> Result<(), ExternalError> { + if let Err(e) = self + .window_requests_tx + .send((self.window_id, WindowRequest::SetSkipTaskbar(skip))) + { + log::warn!("Fail to send skip taskbar request: {}", e); + } + + Ok(()) + } + + pub fn set_progress_bar(&self, progress: ProgressBarState) { + if let Err(e) = self + .window_requests_tx + .send((WindowId::dummy(), WindowRequest::ProgressBarState(progress))) + { + log::warn!("Fail to send update progress bar request: {}", e); + } + } + + pub fn set_badge_count(&self, count: Option, desktop_filename: Option) { + if let Err(e) = self.window_requests_tx.send(( + WindowId::dummy(), + WindowRequest::BadgeCount(count, desktop_filename), + )) { + log::warn!("Fail to send update badge count request: {}", e); + } + } + + pub fn theme(&self) -> Theme { + if let Some(theme) = *self.preferred_theme.borrow() { + return theme; + } + + if let Some(theme) = Settings::default().and_then(|s| s.gtk_theme_name()) { + let theme = theme.as_str(); + if GTK_THEME_SUFFIX_LIST.iter().any(|t| theme.ends_with(t)) { + return Theme::Dark; + } + } + + Theme::Light + } + + pub fn set_theme(&self, theme: Option) { + *self.preferred_theme.borrow_mut() = theme; + if let Err(e) = self + .window_requests_tx + .send((WindowId::dummy(), WindowRequest::SetTheme(theme))) + { + log::warn!("Fail to send set theme request: {e}"); + } + } +} + +// We need GtkWindow to initialize WebView, so we have to keep it in the field. +// It is called on any method. +unsafe impl Send for Window {} +unsafe impl Sync for Window {} + +#[non_exhaustive] +pub enum WindowRequest { + Title(String), + Position((i32, i32)), + Size((i32, i32)), + SizeConstraints(WindowSizeConstraints), + Visible(bool), + Focus, + Resizable(bool), + Closable(bool), + Minimized(bool), + Maximized(bool, bool), + DragWindow, + DragResizeWindow(ResizeDirection), + Fullscreen(Option), + Decorations(bool), + AlwaysOnBottom(bool), + AlwaysOnTop(bool), + WindowIcon(Option), + UserAttention(Option), + SetSkipTaskbar(bool), + CursorIcon(Option), + CursorPosition((i32, i32)), + CursorIgnoreEvents(bool), + WireUpEvents { + transparent: bool, + fullscreen: bool, + cursor_moved: bool, + }, + SetVisibleOnAllWorkspaces(bool), + ProgressBarState(ProgressBarState), + BadgeCount(Option, Option), + SetTheme(Option), + BackgroundColor(CssProvider, Option), +} + +impl Drop for Window { + fn drop(&mut self) { + unsafe { + self.window.destroy(); + } + } +} diff --git a/vendor/tao/src/platform_impl/linux/x11/ffi.rs b/vendor/tao/src/platform_impl/linux/x11/ffi.rs new file mode 100644 index 0000000000..5d5a2cd722 --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/x11/ffi.rs @@ -0,0 +1,8 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +pub use x11_dl::{ + error::OpenError, keysym::*, xcursor::*, xinput::*, xinput2::*, xlib::*, xlib_xcb::*, xrandr::*, + xrender::*, +}; diff --git a/vendor/tao/src/platform_impl/linux/x11/mod.rs b/vendor/tao/src/platform_impl/linux/x11/mod.rs new file mode 100644 index 0000000000..0d5524d570 --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/x11/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +pub mod ffi; +pub mod xdisplay; + +pub use xdisplay::XConnection; diff --git a/vendor/tao/src/platform_impl/linux/x11/xdisplay.rs b/vendor/tao/src/platform_impl/linux/x11/xdisplay.rs new file mode 100644 index 0000000000..b8f40b3170 --- /dev/null +++ b/vendor/tao/src/platform_impl/linux/x11/xdisplay.rs @@ -0,0 +1,169 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{collections::HashMap, error::Error, fmt, os::raw::c_int, ptr}; + +use libc; +use parking_lot::Mutex; + +use crate::window::CursorIcon; + +use super::ffi; + +/// A connection to an X server. +pub struct XConnection { + pub xlib: ffi::Xlib, + /// Exposes XRandR functions from version < 1.5 + pub xrandr: ffi::Xrandr_2_2_0, + /// Exposes XRandR functions from version = 1.5 + pub xrandr_1_5: Option, + pub xcursor: ffi::Xcursor, + pub xinput2: ffi::XInput2, + pub xlib_xcb: ffi::Xlib_xcb, + pub xrender: ffi::Xrender, + pub display: *mut ffi::Display, + pub x11_fd: c_int, + pub latest_error: Mutex>, + pub cursor_cache: Mutex, ffi::Cursor>>, +} + +unsafe impl Send for XConnection {} +unsafe impl Sync for XConnection {} + +pub type XErrorHandler = + Option libc::c_int>; + +impl XConnection { + pub fn new(error_handler: XErrorHandler) -> Result { + // opening the libraries + let xlib = ffi::Xlib::open()?; + let xcursor = ffi::Xcursor::open()?; + let xrandr = ffi::Xrandr_2_2_0::open()?; + let xrandr_1_5 = ffi::Xrandr::open().ok(); + let xinput2 = ffi::XInput2::open()?; + let xlib_xcb = ffi::Xlib_xcb::open()?; + let xrender = ffi::Xrender::open()?; + + unsafe { (xlib.XInitThreads)() }; + unsafe { (xlib.XSetErrorHandler)(error_handler) }; + + // calling XOpenDisplay + let display = unsafe { + let display = (xlib.XOpenDisplay)(ptr::null()); + if display.is_null() { + return Err(XNotSupported::XOpenDisplayFailed); + } + display + }; + + // Get X11 socket file descriptor + let fd = unsafe { (xlib.XConnectionNumber)(display) }; + + Ok(XConnection { + xlib, + xrandr, + xrandr_1_5, + xcursor, + xinput2, + xlib_xcb, + xrender, + display, + x11_fd: fd, + latest_error: Mutex::new(None), + cursor_cache: Default::default(), + }) + } + + /// Checks whether an error has been triggered by the previous function calls. + #[inline] + pub fn check_errors(&self) -> Result<(), XError> { + let error = self.latest_error.lock().take(); + if let Some(error) = error { + Err(error) + } else { + Ok(()) + } + } + + /// Ignores any previous error. + #[inline] + pub fn ignore_error(&self) { + *self.latest_error.lock() = None; + } +} + +impl fmt::Debug for XConnection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display.fmt(f) + } +} + +impl Drop for XConnection { + #[inline] + fn drop(&mut self) { + unsafe { (self.xlib.XCloseDisplay)(self.display) }; + } +} + +/// Error triggered by xlib. +#[derive(Debug, Clone)] +pub struct XError { + pub description: String, + pub error_code: u8, + pub request_code: u8, + pub minor_code: u8, +} + +impl Error for XError {} + +impl fmt::Display for XError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + formatter, + "X error: {} (code: {}, request code: {}, minor code: {})", + self.description, self.error_code, self.request_code, self.minor_code + ) + } +} + +/// Error returned if this system doesn't have XLib or can't create an X connection. +#[derive(Clone, Debug)] +pub enum XNotSupported { + /// Failed to load one or several shared libraries. + LibraryOpenError(ffi::OpenError), + /// Connecting to the X server with `XOpenDisplay` failed. + XOpenDisplayFailed, // TODO: add better message +} + +impl From for XNotSupported { + #[inline] + fn from(err: ffi::OpenError) -> XNotSupported { + XNotSupported::LibraryOpenError(err) + } +} + +impl XNotSupported { + fn description(&self) -> &'static str { + match self { + XNotSupported::LibraryOpenError(_) => "Failed to load one of xlib's shared libraries", + XNotSupported::XOpenDisplayFailed => "Failed to open connection to X server", + } + } +} + +impl Error for XNotSupported { + #[inline] + fn source(&self) -> Option<&(dyn Error + 'static)> { + match *self { + XNotSupported::LibraryOpenError(ref err) => Some(err), + _ => None, + } + } +} + +impl fmt::Display for XNotSupported { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + formatter.write_str(self.description()) + } +} diff --git a/vendor/tao/src/platform_impl/macos/app.rs b/vendor/tao/src/platform_impl/macos/app.rs new file mode 100644 index 0000000000..c9c3219d51 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/app.rs @@ -0,0 +1,126 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{collections::VecDeque, ffi::CStr}; + +use objc2::runtime::{AnyClass as Class, ClassBuilder as ClassDecl, Sel}; +use objc2_app_kit::{self as appkit, NSApplication, NSEvent, NSEventType}; + +use super::{app_state::AppState, event::EventWrapper, util, DEVICE_ID}; +use crate::event::{DeviceEvent, ElementState, Event}; + +pub struct AppClass(pub *const Class); +unsafe impl Send for AppClass {} +unsafe impl Sync for AppClass {} + +lazy_static! { + pub static ref APP_CLASS: AppClass = unsafe { + let superclass = class!(NSApplication); + let mut decl = + ClassDecl::new(CStr::from_bytes_with_nul(b"TaoApp\0").unwrap(), superclass).unwrap(); + + decl.add_method(sel!(sendEvent:), send_event as extern "C" fn(_, _, _)); + + AppClass(decl.register()) + }; +} + +// Normally, holding Cmd + any key never sends us a `keyUp` event for that key. +// Overriding `sendEvent:` like this fixes that. (https://stackoverflow.com/a/15294196) +// Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553) +extern "C" fn send_event(this: &NSApplication, _sel: Sel, event: &NSEvent) { + unsafe { + // For posterity, there are some undocumented event types + // (https://github.com/servo/cocoa-rs/issues/155) + // but that doesn't really matter here. + let event_type = event.r#type(); + let modifier_flags = event.modifierFlags(); + if event_type == appkit::NSKeyUp + && util::has_flag(modifier_flags, appkit::NSEventModifierFlags::Command) + { + if let Some(key_window) = this.keyWindow() { + key_window.sendEvent(event); + } else { + log::debug!("skip sending CMD keyEvent - app has no keyWindow"); + } + } else { + maybe_dispatch_device_event(event); + let superclass = util::superclass(this); + let _: () = msg_send![super(this, superclass), sendEvent: event]; + } + } +} + +unsafe fn maybe_dispatch_device_event(event: &NSEvent) { + let event_type = event.r#type(); + match event_type { + NSEventType::MouseMoved + | NSEventType::LeftMouseDragged + | NSEventType::OtherMouseDragged + | NSEventType::RightMouseDragged => { + let mut events = VecDeque::with_capacity(3); + + let delta_x = event.deltaX() as f64; + let delta_y = event.deltaY() as f64; + + if delta_x != 0.0 { + events.push_back(EventWrapper::StaticEvent(Event::DeviceEvent { + device_id: DEVICE_ID, + event: DeviceEvent::Motion { + axis: 0, + value: delta_x, + }, + })); + } + + if delta_y != 0.0 { + events.push_back(EventWrapper::StaticEvent(Event::DeviceEvent { + device_id: DEVICE_ID, + event: DeviceEvent::Motion { + axis: 1, + value: delta_y, + }, + })); + } + + if delta_x != 0.0 || delta_y != 0.0 { + events.push_back(EventWrapper::StaticEvent(Event::DeviceEvent { + device_id: DEVICE_ID, + event: DeviceEvent::MouseMotion { + delta: (delta_x, delta_y), + }, + })); + } + + AppState::queue_events(events); + } + NSEventType::LeftMouseDown | NSEventType::RightMouseDown | NSEventType::OtherMouseDown => { + let mut events = VecDeque::with_capacity(1); + + events.push_back(EventWrapper::StaticEvent(Event::DeviceEvent { + device_id: DEVICE_ID, + event: DeviceEvent::Button { + button: event.buttonNumber() as u32, + state: ElementState::Pressed, + }, + })); + + AppState::queue_events(events); + } + NSEventType::LeftMouseUp | NSEventType::RightMouseUp | NSEventType::OtherMouseUp => { + let mut events = VecDeque::with_capacity(1); + + events.push_back(EventWrapper::StaticEvent(Event::DeviceEvent { + device_id: DEVICE_ID, + event: DeviceEvent::Button { + button: event.buttonNumber() as u32, + state: ElementState::Released, + }, + })); + + AppState::queue_events(events); + } + _ => (), + } +} diff --git a/vendor/tao/src/platform_impl/macos/app_delegate.rs b/vendor/tao/src/platform_impl/macos/app_delegate.rs new file mode 100644 index 0000000000..2f962a2d9e --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/app_delegate.rs @@ -0,0 +1,223 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + platform::macos::ActivationPolicy, + platform_impl::platform::{ + app_state::AppState, + ffi::{id, BOOL, YES}, + }, +}; + +use objc2::runtime::{ + AnyClass as Class, AnyObject as Object, Bool, ClassBuilder as ClassDecl, Sel, +}; +use objc2_foundation::{ + NSArray, NSError, NSString, NSUserActivity, NSUserActivityTypeBrowsingWeb, NSURL, +}; +use std::{ + cell::{RefCell, RefMut}, + ffi::{CStr, CString}, + os::raw::c_void, + sync::Mutex, + time::Instant, +}; + +const AUX_DELEGATE_STATE_NAME: &str = "auxState"; + +pub struct AuxDelegateState { + /// We store this value in order to be able to defer setting the activation policy until + /// after the app has finished launching. If the activation policy is set earlier, the + /// menubar is initially unresponsive on macOS 10.15 for example. + pub activation_policy: ActivationPolicy, + + /// Whether the application is visible in the dock. + pub dock_visibility: bool, + pub last_dock_show: Mutex>, + + pub activate_ignoring_other_apps: bool, +} + +pub struct AppDelegateClass(pub *const Class); +unsafe impl Send for AppDelegateClass {} +unsafe impl Sync for AppDelegateClass {} + +lazy_static! { + pub static ref APP_DELEGATE_CLASS: AppDelegateClass = unsafe { + let superclass = class!(NSResponder); + let mut decl = ClassDecl::new( + CStr::from_bytes_with_nul(b"TaoAppDelegateParent\0").unwrap(), + superclass, + ) + .unwrap(); + + decl.add_class_method(sel!(new), new as extern "C" fn(_, _) -> _); + decl.add_method(sel!(dealloc), dealloc as extern "C" fn(_, _)); + + decl.add_method( + sel!(applicationDidFinishLaunching:), + did_finish_launching as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(applicationWillTerminate:), + application_will_terminate as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(application:openURLs:), + application_open_urls as extern "C" fn(_, _, _, _), + ); + decl.add_method( + sel!(application:willContinueUserActivityWithType:), + application_will_continue_user_activity_with_type as extern "C" fn(_, _, _, _) -> _, + ); + decl.add_method( + sel!(application:continueUserActivity:restorationHandler:), + application_continue_user_activity as extern "C" fn(_, _, _, _, _) -> _, + ); + decl.add_method( + sel!(applicationShouldHandleReopen:hasVisibleWindows:), + application_should_handle_reopen as extern "C" fn(_, _, _, _) -> _, + ); + decl.add_method( + sel!(applicationSupportsSecureRestorableState:), + application_supports_secure_restorable_state as extern "C" fn(_, _, _) -> _, + ); + decl.add_ivar::<*mut c_void>(&CString::new(AUX_DELEGATE_STATE_NAME).unwrap()); + + AppDelegateClass(decl.register()) + }; +} + +/// Safety: Assumes that Object is an instance of APP_DELEGATE_CLASS +#[allow(deprecated)] // TODO: Use define_class! +pub unsafe fn get_aux_state_mut(this: &Object) -> RefMut<'_, AuxDelegateState> { + let ptr: *mut c_void = *this.get_ivar(AUX_DELEGATE_STATE_NAME); + // Watch out that this needs to be the correct type + (*(ptr as *mut RefCell)).borrow_mut() +} + +extern "C" fn new(class: &Class, _: Sel) -> id { + #[allow(deprecated)] // TODO: Use define_class! + unsafe { + let this: id = msg_send![class, alloc]; + let this: id = msg_send![this, init]; + *(*this).get_mut_ivar(AUX_DELEGATE_STATE_NAME) = + Box::into_raw(Box::new(RefCell::new(AuxDelegateState { + activation_policy: ActivationPolicy::Regular, + activate_ignoring_other_apps: true, + dock_visibility: true, + last_dock_show: Mutex::new(None), + }))) as *mut c_void; + this + } +} + +extern "C" fn dealloc(this: &Object, _: Sel) { + #[allow(deprecated)] // TODO: Use define_class! + unsafe { + let state_ptr: *mut c_void = *(this.get_ivar(AUX_DELEGATE_STATE_NAME)); + // As soon as the box is constructed it is immediately dropped, releasing the underlying + // memory + drop(Box::from_raw(state_ptr as *mut RefCell)); + } +} + +extern "C" fn did_finish_launching(this: &Object, _: Sel, _: id) { + trace!("Triggered `applicationDidFinishLaunching`"); + AppState::launched(this); + trace!("Completed `applicationDidFinishLaunching`"); +} + +extern "C" fn application_will_terminate(_: &Object, _: Sel, _: id) { + trace!("Triggered `applicationWillTerminate`"); + AppState::exit(); + trace!("Completed `applicationWillTerminate`"); +} + +extern "C" fn application_open_urls(_: &Object, _: Sel, _: id, urls: &NSArray) { + trace!("Trigger `application:openURLs:`"); + + let urls = unsafe { + (0..urls.count()) + .flat_map(|i| url::Url::parse(&urls.objectAtIndex(i).absoluteString().unwrap().to_string())) + .collect::>() + }; + trace!("Get `application:openURLs:` URLs: {:?}", urls); + AppState::open_urls(urls); + trace!("Completed `application:openURLs:`"); +} + +extern "C" fn application_will_continue_user_activity_with_type( + _: &Object, + _: Sel, + _: id, + user_activity_type: &NSString, +) -> Bool { + trace!("Trigger `application:willContinueUserActivityWithType:`"); + let result = unsafe { Bool::new(user_activity_type == NSUserActivityTypeBrowsingWeb) }; + trace!("Completed `application:willContinueUserActivityWithType:`"); + result +} + +extern "C" fn application_continue_user_activity( + _: &Object, + _: Sel, + _: id, + user_activity: &NSUserActivity, + _restoration_handler: &block2::Block, +) -> Bool { + trace!("Trigger `application:continueUserActivity:restorationHandler:`"); + let url = unsafe { + if user_activity + .activityType() + .isEqualToString(NSUserActivityTypeBrowsingWeb) + { + match user_activity + .webpageURL() + .and_then(|url| url.absoluteString()) + .and_then(|s| Some(s.to_string())) + { + None => { + error!( + "`application:continueUserActivity:restorationHandler:`: restore webbrowsing activity but url is empty" + ); + return Bool::new(false); + } + Some(url_string) => match url::Url::parse(&url_string) { + Ok(url) => url, + Err(err) => { + error!( + "`application:continueUserActivity:restorationHandler:`: failed to parse url {err}" + ); + return Bool::new(false); + } + }, + } + } else { + return Bool::new(false); + } + }; + + AppState::open_urls(vec![url]); + trace!("Completed `application:continueUserActivity:restorationHandler:`"); + return Bool::new(true); +} + +extern "C" fn application_should_handle_reopen( + _: &Object, + _: Sel, + _: id, + has_visible_windows: BOOL, +) -> BOOL { + trace!("Triggered `applicationShouldHandleReopen`"); + AppState::reopen(has_visible_windows.as_bool()); + trace!("Completed `applicationShouldHandleReopen`"); + has_visible_windows +} + +extern "C" fn application_supports_secure_restorable_state(_: &Object, _: Sel, _: id) -> BOOL { + trace!("Triggered `applicationSupportsSecureRestorableState`"); + trace!("Completed `applicationSupportsSecureRestorableState`"); + YES +} diff --git a/vendor/tao/src/platform_impl/macos/app_state.rs b/vendor/tao/src/platform_impl/macos/app_state.rs new file mode 100644 index 0000000000..ad90125098 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/app_state.rs @@ -0,0 +1,470 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + cell::{RefCell, RefMut}, + collections::VecDeque, + fmt::{self, Debug}, + hint::unreachable_unchecked, + mem, + rc::{Rc, Weak}, + sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, MutexGuard, + }, + time::Instant, +}; + +use objc2::{msg_send, rc::Retained, runtime::AnyObject as Object}; +use objc2_app_kit::{NSApp, NSApplication, NSApplicationActivationPolicy, NSWindow}; +use objc2_foundation::{MainThreadMarker, NSAutoreleasePool, NSSize}; + +use crate::{ + dpi::LogicalSize, + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoopWindowTarget as RootWindowTarget}, + platform::macos::ActivationPolicy, + platform_impl::{ + get_aux_state_mut, + platform::{ + event::{EventProxy, EventWrapper}, + event_loop::{post_dummy_event, PanicInfo}, + ffi::{id, nil}, + observer::{CFRunLoopGetMain, CFRunLoopWakeUp, EventLoopWaker}, + util::{self, Never}, + window::get_window_id, + }, + }, + window::WindowId, +}; + +use super::set_dock_visibility; + +lazy_static! { + static ref HANDLER: Handler = Default::default(); +} + +impl<'a, Never> Event<'a, Never> { + fn userify(self) -> Event<'a, T> { + self + .map_nonuser_event() + // `Never` can't be constructed, so the `UserEvent` variant can't + // be present here. + .unwrap_or_else(|_| unsafe { unreachable_unchecked() }) + } +} + +pub trait EventHandler: Debug { + // Not sure probably it should accept Event<'static, Never> + fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow); + fn handle_user_events(&mut self, control_flow: &mut ControlFlow); +} + +struct EventLoopHandler { + callback: Weak, &RootWindowTarget, &mut ControlFlow)>>, + window_target: Rc>, +} + +impl EventLoopHandler { + fn with_callback(&mut self, f: F) + where + F: FnOnce( + &mut EventLoopHandler, + RefMut<'_, dyn FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow)>, + ), + { + if let Some(callback) = self.callback.upgrade() { + let callback = callback.borrow_mut(); + (f)(self, callback); + } else { + panic!( + "Tried to dispatch an event, but the event loop that \ + owned the event handler callback seems to be destroyed" + ); + } + } +} + +impl Debug for EventLoopHandler { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("EventLoopHandler") + .field("window_target", &self.window_target) + .finish() + } +} + +impl EventHandler for EventLoopHandler { + fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow) { + self.with_callback(|this, mut callback| { + if let ControlFlow::ExitWithCode(code) = *control_flow { + let dummy = &mut ControlFlow::ExitWithCode(code); + (callback)(event.userify(), &this.window_target, dummy); + } else { + (callback)(event.userify(), &this.window_target, control_flow); + } + }); + } + + fn handle_user_events(&mut self, control_flow: &mut ControlFlow) { + self.with_callback(|this, mut callback| { + for event in this.window_target.p.receiver.try_iter() { + if let ControlFlow::ExitWithCode(code) = *control_flow { + let dummy = &mut ControlFlow::ExitWithCode(code); + (callback)(Event::UserEvent(event), &this.window_target, dummy); + } else { + (callback)(Event::UserEvent(event), &this.window_target, control_flow); + } + } + }); + } +} + +#[derive(Default)] +struct Handler { + ready: AtomicBool, + in_callback: AtomicBool, + control_flow: Mutex, + control_flow_prev: Mutex, + start_time: Mutex>, + callback: Mutex>>, + pending_events: Mutex>, + pending_redraw: Mutex>, + waker: Mutex, +} + +unsafe impl Send for Handler {} +unsafe impl Sync for Handler {} + +impl Handler { + fn events(&self) -> MutexGuard<'_, VecDeque> { + self.pending_events.lock().unwrap() + } + + fn redraw<'a>(&'a self) -> MutexGuard<'a, Vec> { + self.pending_redraw.lock().unwrap() + } + + fn waker(&self) -> MutexGuard<'_, EventLoopWaker> { + self.waker.lock().unwrap() + } + + fn is_ready(&self) -> bool { + self.ready.load(Ordering::Acquire) + } + + fn set_ready(&self) { + self.ready.store(true, Ordering::Release); + } + + fn should_exit(&self) -> bool { + matches!( + *self.control_flow.lock().unwrap(), + ControlFlow::ExitWithCode(_) + ) + } + + fn get_control_flow_and_update_prev(&self) -> ControlFlow { + let control_flow = self.control_flow.lock().unwrap(); + *self.control_flow_prev.lock().unwrap() = *control_flow; + *control_flow + } + + fn get_old_and_new_control_flow(&self) -> (ControlFlow, ControlFlow) { + let old = *self.control_flow_prev.lock().unwrap(); + let new = *self.control_flow.lock().unwrap(); + (old, new) + } + + fn get_start_time(&self) -> Option { + *self.start_time.lock().unwrap() + } + + fn update_start_time(&self) { + *self.start_time.lock().unwrap() = Some(Instant::now()); + } + + fn take_events(&self) -> VecDeque { + mem::take(&mut *self.events()) + } + + fn should_redraw(&self) -> Vec { + mem::take(&mut *self.redraw()) + } + + fn get_in_callback(&self) -> bool { + self.in_callback.load(Ordering::Acquire) + } + + fn set_in_callback(&self, in_callback: bool) { + self.in_callback.store(in_callback, Ordering::Release); + } + + fn handle_nonuser_event(&self, wrapper: EventWrapper) { + if let Some(ref mut callback) = *self.callback.lock().unwrap() { + match wrapper { + EventWrapper::StaticEvent(event) => { + callback.handle_nonuser_event(event, &mut self.control_flow.lock().unwrap()) + } + EventWrapper::EventProxy(proxy) => self.handle_proxy(proxy, callback), + } + } + } + + fn handle_user_events(&self) { + if let Some(ref mut callback) = *self.callback.lock().unwrap() { + callback.handle_user_events(&mut self.control_flow.lock().unwrap()); + } + } + + fn handle_scale_factor_changed_event( + &self, + callback: &mut Box, + ns_window: &NSWindow, + suggested_size: LogicalSize, + scale_factor: f64, + ) { + let mut size = suggested_size.to_physical(scale_factor); + let old_size = size; + let event = Event::WindowEvent { + window_id: WindowId(get_window_id(ns_window)), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size: &mut size, + }, + }; + + callback.handle_nonuser_event(event, &mut self.control_flow.lock().unwrap()); + + if old_size != size { + let logical_size = size.to_logical(scale_factor); + let size = NSSize::new(logical_size.width, logical_size.height); + NSWindow::setContentSize(ns_window, size); + } + } + + fn handle_proxy(&self, proxy: EventProxy, callback: &mut Box) { + match proxy { + EventProxy::DpiChangedProxy { + ns_window, + suggested_size, + scale_factor, + } => { + self.handle_scale_factor_changed_event(callback, &ns_window, suggested_size, scale_factor) + } + } + } +} + +pub enum AppState {} + +impl AppState { + pub fn set_callback( + callback: Weak, &RootWindowTarget, &mut ControlFlow)>>, + window_target: Rc>, + ) { + *HANDLER.callback.lock().unwrap() = Some(Box::new(EventLoopHandler { + callback, + window_target, + })); + } + + pub fn exit() -> i32 { + HANDLER.set_in_callback(true); + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::LoopDestroyed)); + HANDLER.set_in_callback(false); + HANDLER.callback.lock().unwrap().take(); + if let ControlFlow::ExitWithCode(code) = HANDLER.get_old_and_new_control_flow().1 { + code + } else { + 0 + } + } + + pub fn launched(app_delegate: &Object) { + apply_activation_policy(app_delegate); + + unsafe { + let mtm = MainThreadMarker::new().unwrap(); + let ns_app = NSApp(mtm); + window_activation_hack(&ns_app); + let ignore = get_aux_state_mut(app_delegate).activate_ignoring_other_apps; + #[allow(deprecated)] + ns_app.activateIgnoringOtherApps(ignore); + + let dock_visible = get_aux_state_mut(app_delegate).dock_visibility; + if !dock_visible { + set_dock_visibility(app_delegate, dock_visible); + } + }; + HANDLER.set_ready(); + HANDLER.waker().start(); + HANDLER.set_in_callback(true); + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents( + StartCause::Init, + ))); + HANDLER.set_in_callback(false); + } + + pub fn open_urls(urls: Vec) { + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::Opened { urls })); + } + + pub fn reopen(has_visible_windows: bool) { + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::Reopen { + has_visible_windows, + })); + } + + pub fn wakeup(panic_info: Weak) { + let panic_info = panic_info + .upgrade() + .expect("The panic info must exist here. This failure indicates a developer error."); + // Return when in callback due to https://github.com/rust-windowing/winit/issues/1779 + if panic_info.is_panicking() || !HANDLER.is_ready() || HANDLER.get_in_callback() { + return; + } + let start = HANDLER.get_start_time().unwrap(); + let cause = match HANDLER.get_control_flow_and_update_prev() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + ControlFlow::WaitUntil(requested_resume) => { + if Instant::now() >= requested_resume { + StartCause::ResumeTimeReached { + start, + requested_resume, + } + } else { + StartCause::WaitCancelled { + start, + requested_resume: Some(requested_resume), + } + } + } + ControlFlow::ExitWithCode(_) => StartCause::Poll, //panic!("unexpected `ControlFlow::Exit`"), + }; + HANDLER.set_in_callback(true); + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents(cause))); + HANDLER.set_in_callback(false); + } + + // This is called from multiple threads at present + pub fn queue_redraw(window_id: WindowId) { + let mut pending_redraw = HANDLER.redraw(); + if !pending_redraw.contains(&window_id) { + pending_redraw.push(window_id); + } + unsafe { + let rl = CFRunLoopGetMain(); + CFRunLoopWakeUp(rl); + } + } + + pub fn handle_redraw(window_id: WindowId) { + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawRequested(window_id))); + } + + pub fn queue_event(wrapper: EventWrapper) { + if !util::is_main_thread() { + panic!("Event queued from different thread: {:#?}", wrapper); + } + HANDLER.events().push_back(wrapper); + } + + pub fn queue_events(mut wrappers: VecDeque) { + if !util::is_main_thread() { + panic!("Events queued from different thread: {:#?}", wrappers); + } + HANDLER.events().append(&mut wrappers); + } + + pub fn cleared(panic_info: Weak) { + let panic_info = panic_info + .upgrade() + .expect("The panic info must exist here. This failure indicates a developer error."); + // Return when in callback due to https://github.com/rust-windowing/winit/issues/1779 + if panic_info.is_panicking() || !HANDLER.is_ready() || HANDLER.get_in_callback() { + return; + } + HANDLER.set_in_callback(true); + HANDLER.handle_user_events(); + for event in HANDLER.take_events() { + HANDLER.handle_nonuser_event(event); + } + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::MainEventsCleared)); + for window_id in HANDLER.should_redraw() { + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawRequested(window_id))); + } + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawEventsCleared)); + HANDLER.set_in_callback(false); + if HANDLER.should_exit() { + unsafe { + let mtm = MainThreadMarker::new().unwrap(); + let app = NSApp(mtm); + let _pool = NSAutoreleasePool::new(); + let () = msg_send![&app, stop: nil]; + // To stop event loop immediately, we need to post some event here. + post_dummy_event(&app); + }; + } + HANDLER.update_start_time(); + match HANDLER.get_old_and_new_control_flow() { + (ControlFlow::ExitWithCode(_), _) | (_, ControlFlow::ExitWithCode(_)) => (), + (old, new) if old == new => (), + (_, ControlFlow::Wait) => HANDLER.waker().stop(), + (_, ControlFlow::WaitUntil(instant)) => HANDLER.waker().start_at(instant), + (_, ControlFlow::Poll) => HANDLER.waker().start(), + } + } +} + +/// A hack to make activation of multiple windows work when creating them before +/// `applicationDidFinishLaunching:` / `Event::Event::NewEvents(StartCause::Init)`. +/// +/// Alternative to this would be the user calling `window.set_visible(true)` in +/// `StartCause::Init`. +/// +/// If this becomes too bothersome to maintain, it can probably be removed +/// without too much damage. +unsafe fn window_activation_hack(ns_app: &NSApplication) { + // Get the application's windows + // TODO: Proper ordering of the windows + let ns_windows: id = msg_send![ns_app, windows]; + let ns_enumerator: id = msg_send![ns_windows, objectEnumerator]; + loop { + // Enumerate over the windows + let ns_window: Option> = msg_send![ns_enumerator, nextObject]; + if ns_window.is_none() { + break; + } + let ns_window = ns_window.unwrap(); + // And call `makeKeyAndOrderFront` if it was called on the window in `UnownedWindow::new` + // This way we preserve the user's desired initial visiblity status + // TODO: Also filter on the type/"level" of the window, and maybe other things? + if ns_window.isVisible() { + trace!("Activating visible window"); + ns_window.makeKeyAndOrderFront(None); + } else { + trace!("Skipping activating invisible window"); + } + } +} +fn apply_activation_policy(app_delegate: &Object) { + unsafe { + let mtm = MainThreadMarker::new().unwrap(); + let ns_app = NSApp(mtm); + // We need to delay setting the activation policy and activating the app + // until `applicationDidFinishLaunching` has been called. Otherwise the + // menu bar won't be interactable. + let act_pol = get_aux_state_mut(app_delegate).activation_policy; + ns_app.setActivationPolicy(match act_pol { + ActivationPolicy::Regular => NSApplicationActivationPolicy::Regular, + ActivationPolicy::Accessory => NSApplicationActivationPolicy::Accessory, + ActivationPolicy::Prohibited => NSApplicationActivationPolicy::Prohibited, + }); + } +} diff --git a/vendor/tao/src/platform_impl/macos/badge.rs b/vendor/tao/src/platform_impl/macos/badge.rs new file mode 100644 index 0000000000..5c8933b982 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/badge.rs @@ -0,0 +1,13 @@ +use super::ffi::id; +use objc2_app_kit::NSApp; +use objc2_foundation::{MainThreadMarker, NSString}; + +pub fn set_badge_label(label: Option) { + // SAFETY: TODO + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + unsafe { + let label = label.map(|label| NSString::from_str(&label)); + let dock_tile: id = msg_send![&NSApp(mtm), dockTile]; + let _: () = msg_send![dock_tile, setBadgeLabel: label.as_deref()]; + } +} diff --git a/vendor/tao/src/platform_impl/macos/dock.rs b/vendor/tao/src/platform_impl/macos/dock.rs new file mode 100644 index 0000000000..31a1f25df0 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/dock.rs @@ -0,0 +1,91 @@ +#![allow(non_snake_case, non_upper_case_globals)] + +use std::{ + sync::Mutex, + time::{Duration, Instant}, +}; + +use objc2::{runtime::AnyObject, MainThreadMarker}; +use objc2_app_kit::NSApplication; + +use super::get_aux_state_mut; + +const DOCK_SHOW_TIMEOUT: Duration = Duration::from_secs(1); + +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + fn TransformProcessType(psn: *const ProcessSerialNumber, transformState: i32) -> i32; +} + +#[repr(C)] +struct ProcessSerialNumber { + highLongOfPSN: u32, + lowLongOfPSN: u32, +} + +/// https://developer.apple.com/documentation/applicationservices/1501096-anonymous/kcurrentprocess?language=objc +pub const kCurrentProcess: u32 = 2; +/// https://developer.apple.com/documentation/applicationservices/1501117-anonymous/kprocesstransformtouielementapplication?language=objc +pub const kProcessTransformToUIElementApplication: i32 = 4; +/// https://developer.apple.com/documentation/applicationservices/1501117-anonymous/kprocesstransformtoforegroundapplication?language=objc +pub const kProcessTransformToForegroundApplication: i32 = 1; + +pub fn set_dock_visibility(app_delegate: &AnyObject, visible: bool) { + let last_dock_show = unsafe { &get_aux_state_mut(app_delegate).last_dock_show }; + if visible { + set_dock_show(last_dock_show); + } else { + set_dock_hide(last_dock_show); + } +} + +fn set_dock_hide(last_dock_show: &Mutex>) { + // Transforming application state from UIElement to Foreground is an + // asynchronous operation, and unfortunately there is currently no way to know + // when it is finished. + // So if we call DockHide => DockShow => DockHide => DockShow in a very short + // time, we would trigger a bug of macOS that, there would be multiple dock + // icons of the app left in system. + // To work around this, we make sure DockHide does nothing if it is called + // immediately after DockShow. After some experiments, 1 second seems to be + // a proper interval. + let now = Instant::now(); + let last_dock_show = last_dock_show.lock().unwrap(); + if let Some(last_dock_show_time) = *last_dock_show { + if now.duration_since(last_dock_show_time) < DOCK_SHOW_TIMEOUT { + return; + } + } + + unsafe { + // TODO: Safety. + let mtm = MainThreadMarker::new_unchecked(); + let app = NSApplication::sharedApplication(mtm); + let windows = app.windows(); + + for window in windows { + window.setCanHide(false); + } + + let psn = ProcessSerialNumber { + highLongOfPSN: 0, + lowLongOfPSN: kCurrentProcess, + }; + TransformProcessType(&psn, kProcessTransformToUIElementApplication); + } +} + +fn set_dock_show(last_dock_show: &Mutex>) { + let now = Instant::now(); + let mut last_dock_show = last_dock_show.lock().unwrap(); + *last_dock_show = Some(now); + + unsafe { + let psn = ProcessSerialNumber { + highLongOfPSN: 0, + lowLongOfPSN: kCurrentProcess, + }; + + TransformProcessType(&psn, kProcessTransformToForegroundApplication); + } +} diff --git a/vendor/tao/src/platform_impl/macos/event.rs b/vendor/tao/src/platform_impl/macos/event.rs new file mode 100644 index 0000000000..d3c1671ccd --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/event.rs @@ -0,0 +1,335 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{collections::HashSet, ffi::c_void, os::raw::c_ushort, sync::Mutex}; + +use objc2::{msg_send, rc::Retained}; +use objc2_app_kit::{NSEvent, NSEventModifierFlags, NSWindow}; + +use core_foundation::{base::CFRelease, data::CFDataGetBytePtr}; +use objc2_foundation::NSString; + +use crate::{ + dpi::LogicalSize, + event::{ElementState, Event, KeyEvent}, + keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NativeKeyCode}, + platform_impl::platform::{ffi, util::Never}, +}; + +lazy_static! { + static ref KEY_STRINGS: Mutex> = Mutex::new(HashSet::new()); +} + +fn insert_or_get_key_str(string: String) -> &'static str { + let mut string_set = KEY_STRINGS.lock().unwrap(); + if let Some(contained) = string_set.get(string.as_str()) { + return contained; + } + let static_str = Box::leak(string.into_boxed_str()); + string_set.insert(static_str); + static_str +} + +#[non_exhaustive] +#[derive(Debug)] +pub enum EventWrapper { + StaticEvent(Event<'static, Never>), + EventProxy(EventProxy), +} + +#[non_exhaustive] +#[derive(Debug, PartialEq)] +pub enum EventProxy { + #[non_exhaustive] + DpiChangedProxy { + ns_window: Retained, + suggested_size: LogicalSize, + scale_factor: f64, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option<&'static str>, + pub key_without_modifiers: Key<'static>, +} + +pub fn get_modifierless_char(scancode: u16) -> Key<'static> { + let mut string = [0; 16]; + let input_source; + let layout; + unsafe { + input_source = ffi::TISCopyCurrentKeyboardLayoutInputSource(); + if input_source.is_null() { + log::error!("`TISCopyCurrentKeyboardLayoutInputSource` returned null ptr"); + return Key::Unidentified(NativeKeyCode::MacOS(scancode)); + } + let layout_data = + ffi::TISGetInputSourceProperty(input_source, ffi::kTISPropertyUnicodeKeyLayoutData); + if layout_data.is_null() { + CFRelease(input_source as *mut c_void); + log::error!("`TISGetInputSourceProperty` returned null ptr"); + return Key::Unidentified(NativeKeyCode::MacOS(scancode)); + } + layout = CFDataGetBytePtr(layout_data) as *const ffi::UCKeyboardLayout; + } + let keyboard_type = unsafe { ffi::LMGetKbdType() }; + + let mut result_len = 0; + let mut dead_keys = 0; + let modifiers = 0; + let translate_result = unsafe { + ffi::UCKeyTranslate( + layout, + scancode, + ffi::kUCKeyActionDisplay, + modifiers, + keyboard_type as u32, + ffi::kUCKeyTranslateNoDeadKeysMask, + &mut dead_keys, + string.len() as ffi::UniCharCount, + &mut result_len, + string.as_mut_ptr(), + ) + }; + unsafe { + CFRelease(input_source as *mut c_void); + } + if translate_result != 0 { + log::error!( + "`UCKeyTranslate` returned with the non-zero value: {}", + translate_result + ); + return Key::Unidentified(NativeKeyCode::MacOS(scancode)); + } + if result_len == 0 { + log::error!("`UCKeyTranslate` was succesful but gave a string of 0 length."); + return Key::Unidentified(NativeKeyCode::MacOS(scancode)); + } + let chars = String::from_utf16_lossy(&string[0..result_len as usize]); + Key::Character(insert_or_get_key_str(chars)) +} + +fn get_logical_key_char(ns_event: &NSEvent, modifierless_chars: &str) -> Key<'static> { + let characters: Retained = unsafe { msg_send![ns_event, charactersIgnoringModifiers] }; + let string = characters.to_string(); + if string.is_empty() { + // Probably a dead key + let first_char = modifierless_chars.chars().next(); + return Key::Dead(first_char); + } + Key::Character(insert_or_get_key_str(string)) +} + +#[allow(clippy::unnecessary_unwrap)] +pub fn create_key_event( + ns_event: &NSEvent, + is_press: bool, + is_repeat: bool, + in_ime: bool, + key_override: Option, +) -> KeyEvent { + use ElementState::{Pressed, Released}; + let state = if is_press { Pressed } else { Released }; + + let scancode = get_scancode(ns_event); + let mut physical_key = key_override.unwrap_or_else(|| KeyCode::from_scancode(scancode as u32)); + + let text_with_all_modifiers: Option<&'static str> = { + if key_override.is_some() { + None + } else { + let characters: Retained = unsafe { msg_send![ns_event, characters] }; + let characters = characters.to_string(); + if characters.is_empty() { + None + } else { + if matches!(physical_key, KeyCode::Unidentified(_)) { + // The key may be one of the funky function keys + physical_key = extra_function_key_to_code(scancode, &characters); + } + Some(insert_or_get_key_str(characters)) + } + } + }; + let key_from_code = code_to_key(physical_key, scancode); + let logical_key; + let key_without_modifiers; + if !matches!(key_from_code, Key::Unidentified(_)) { + logical_key = key_from_code.clone(); + key_without_modifiers = key_from_code; + } else { + //#[cfg(debug_assertions)] println!("Couldn't get key from code: {:?}", physical_key); + key_without_modifiers = get_modifierless_char(scancode); + + let modifiers = unsafe { NSEvent::modifierFlags(ns_event) }; + let has_alt = modifiers.contains(NSEventModifierFlags::Option); + let has_ctrl = modifiers.contains(NSEventModifierFlags::Control); + if has_alt || has_ctrl || text_with_all_modifiers.is_none() || !is_press { + let modifierless_chars = match key_without_modifiers.clone() { + Key::Character(ch) => ch, + _ => "", + }; + logical_key = get_logical_key_char(ns_event, modifierless_chars); + } else { + logical_key = Key::Character(text_with_all_modifiers.unwrap()); + } + } + let text = if in_ime || !is_press { + None + } else { + logical_key.to_text() + }; + KeyEvent { + location: code_to_location(physical_key), + logical_key, + physical_key, + repeat: is_repeat, + state, + text, + platform_specific: KeyEventExtra { + text_with_all_modifiers, + key_without_modifiers, + }, + } +} + +pub fn code_to_key(code: KeyCode, scancode: u16) -> Key<'static> { + match code { + KeyCode::Enter => Key::Enter, + KeyCode::Tab => Key::Tab, + KeyCode::Space => Key::Space, + KeyCode::Backspace => Key::Backspace, + KeyCode::Escape => Key::Escape, + KeyCode::SuperRight => Key::Super, + KeyCode::SuperLeft => Key::Super, + KeyCode::ShiftLeft => Key::Shift, + KeyCode::AltLeft => Key::Alt, + KeyCode::ControlLeft => Key::Control, + KeyCode::ShiftRight => Key::Shift, + KeyCode::AltRight => Key::Alt, + KeyCode::ControlRight => Key::Control, + + KeyCode::NumLock => Key::NumLock, + KeyCode::AudioVolumeUp => Key::AudioVolumeUp, + KeyCode::AudioVolumeDown => Key::AudioVolumeDown, + + // Other numpad keys all generate text on macOS (if I understand correctly) + KeyCode::NumpadEnter => Key::Enter, + + KeyCode::F1 => Key::F1, + KeyCode::F2 => Key::F2, + KeyCode::F3 => Key::F3, + KeyCode::F4 => Key::F4, + KeyCode::F5 => Key::F5, + KeyCode::F6 => Key::F6, + KeyCode::F7 => Key::F7, + KeyCode::F8 => Key::F8, + KeyCode::F9 => Key::F9, + KeyCode::F10 => Key::F10, + KeyCode::F11 => Key::F11, + KeyCode::F12 => Key::F12, + KeyCode::F13 => Key::F13, + KeyCode::F14 => Key::F14, + KeyCode::F15 => Key::F15, + KeyCode::F16 => Key::F16, + KeyCode::F17 => Key::F17, + KeyCode::F18 => Key::F18, + KeyCode::F19 => Key::F19, + KeyCode::F20 => Key::F20, + + KeyCode::Insert => Key::Insert, + KeyCode::Home => Key::Home, + KeyCode::PageUp => Key::PageUp, + KeyCode::Delete => Key::Delete, + KeyCode::End => Key::End, + KeyCode::PageDown => Key::PageDown, + KeyCode::ArrowLeft => Key::ArrowLeft, + KeyCode::ArrowRight => Key::ArrowRight, + KeyCode::ArrowDown => Key::ArrowDown, + KeyCode::ArrowUp => Key::ArrowUp, + _ => Key::Unidentified(NativeKeyCode::MacOS(scancode)), + } +} + +fn code_to_location(code: KeyCode) -> KeyLocation { + match code { + KeyCode::SuperRight => KeyLocation::Right, + KeyCode::SuperLeft => KeyLocation::Left, + KeyCode::ShiftLeft => KeyLocation::Left, + KeyCode::AltLeft => KeyLocation::Left, + KeyCode::ControlLeft => KeyLocation::Left, + KeyCode::ShiftRight => KeyLocation::Right, + KeyCode::AltRight => KeyLocation::Right, + KeyCode::ControlRight => KeyLocation::Right, + + KeyCode::NumLock => KeyLocation::Numpad, + KeyCode::NumpadDecimal => KeyLocation::Numpad, + KeyCode::NumpadMultiply => KeyLocation::Numpad, + KeyCode::NumpadAdd => KeyLocation::Numpad, + KeyCode::NumpadDivide => KeyLocation::Numpad, + KeyCode::NumpadEnter => KeyLocation::Numpad, + KeyCode::NumpadSubtract => KeyLocation::Numpad, + KeyCode::NumpadEqual => KeyLocation::Numpad, + KeyCode::Numpad0 => KeyLocation::Numpad, + KeyCode::Numpad1 => KeyLocation::Numpad, + KeyCode::Numpad2 => KeyLocation::Numpad, + KeyCode::Numpad3 => KeyLocation::Numpad, + KeyCode::Numpad4 => KeyLocation::Numpad, + KeyCode::Numpad5 => KeyLocation::Numpad, + KeyCode::Numpad6 => KeyLocation::Numpad, + KeyCode::Numpad7 => KeyLocation::Numpad, + KeyCode::Numpad8 => KeyLocation::Numpad, + KeyCode::Numpad9 => KeyLocation::Numpad, + + _ => KeyLocation::Standard, + } +} + +// While F1-F20 have scancodes we can match on, we have to check against UTF-16 +// constants for the rest. +// https://developer.apple.com/documentation/appkit/1535851-function-key_unicodes?preferredLanguage=occ +pub fn extra_function_key_to_code(scancode: u16, string: &str) -> KeyCode { + if let Some(ch) = string.encode_utf16().next() { + match ch { + 0xf718 => KeyCode::F21, + 0xf719 => KeyCode::F22, + 0xf71a => KeyCode::F23, + 0xf71b => KeyCode::F24, + _ => KeyCode::Unidentified(NativeKeyCode::MacOS(scancode)), + } + } else { + KeyCode::Unidentified(NativeKeyCode::MacOS(scancode)) + } +} + +pub fn event_mods(event: &NSEvent) -> ModifiersState { + let flags = unsafe { NSEvent::modifierFlags(event) }; + let mut m = ModifiersState::empty(); + m.set( + ModifiersState::SHIFT, + flags.contains(NSEventModifierFlags::Shift), + ); + m.set( + ModifiersState::CONTROL, + flags.contains(NSEventModifierFlags::Control), + ); + m.set( + ModifiersState::ALT, + flags.contains(NSEventModifierFlags::Option), + ); + m.set( + ModifiersState::SUPER, + flags.contains(NSEventModifierFlags::Command), + ); + m +} + +pub fn get_scancode(event: &NSEvent) -> c_ushort { + // In AppKit, `keyCode` refers to the position (scancode) of a key rather than its character, + // and there is no easy way to navtively retrieve the layout-dependent character. + // In tao, we use keycode to refer to the key's character, and so this function aligns + // AppKit's terminology with ours. + unsafe { msg_send![event, keyCode] } +} diff --git a/vendor/tao/src/platform_impl/macos/event_loop.rs b/vendor/tao/src/platform_impl/macos/event_loop.rs new file mode 100644 index 0000000000..8179ea91fd --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/event_loop.rs @@ -0,0 +1,356 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + any::Any, + cell::{Cell, RefCell}, + collections::VecDeque, + marker::PhantomData, + mem, + os::raw::c_void, + panic::{catch_unwind, resume_unwind, RefUnwindSafe, UnwindSafe}, + process, ptr, + rc::{Rc, Weak}, +}; + +use crossbeam_channel::{self as channel, Receiver, Sender}; +use objc2::{msg_send, rc::Retained}; +use objc2_app_kit::{NSApp, NSApplication, NSEventModifierFlags, NSEventSubtype, NSEventType}; +use objc2_foundation::{MainThreadMarker, NSAutoreleasePool, NSInteger, NSPoint, NSTimeInterval}; + +use crate::{ + dpi::PhysicalPosition, + error::ExternalError, + event::Event, + event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootWindowTarget}, + monitor::MonitorHandle as RootMonitorHandle, + platform_impl::{ + platform::{ + app::APP_CLASS, + app_delegate::APP_DELEGATE_CLASS, + app_state::AppState, + ffi::{id, nil, YES}, + monitor::{self, MonitorHandle}, + observer::*, + util::{self, IdRef}, + }, + set_badge_label, set_progress_indicator, + }, + window::{ProgressBarState, Theme}, +}; + +use super::window::set_ns_theme; + +#[derive(Default)] +pub struct PanicInfo { + inner: Cell>>, +} + +// WARNING: +// As long as this struct is used through its `impl`, it is UnwindSafe. +// (If `get_mut` is called on `inner`, unwind safety may get broken.) +impl UnwindSafe for PanicInfo {} +impl RefUnwindSafe for PanicInfo {} +impl PanicInfo { + pub fn is_panicking(&self) -> bool { + let inner = self.inner.take(); + let result = inner.is_some(); + self.inner.set(inner); + result + } + /// Overwrites the curret state if the current state is not panicking + pub fn set_panic(&self, p: Box) { + if !self.is_panicking() { + self.inner.set(Some(p)); + } + } + pub fn take(&self) -> Option> { + self.inner.take() + } +} + +#[derive(Clone)] +pub struct EventLoopWindowTarget { + pub sender: Sender, // this is only here to be cloned elsewhere + pub receiver: Receiver, +} + +impl Default for EventLoopWindowTarget { + fn default() -> Self { + let (sender, receiver) = channel::unbounded(); + EventLoopWindowTarget { sender, receiver } + } +} + +impl EventLoopWindowTarget { + #[inline] + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + #[inline] + pub fn monitor_from_point(&self, x: f64, y: f64) -> Option { + monitor::from_point(x, y) + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(RootMonitorHandle { inner: monitor }) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06(&self) -> Result { + Ok(rwh_06::RawDisplayHandle::AppKit( + rwh_06::AppKitDisplayHandle::new(), + )) + } + #[inline] + pub fn cursor_position(&self) -> Result, ExternalError> { + util::cursor_position() + } + + #[inline] + pub fn set_progress_bar(&self, progress: ProgressBarState) { + set_progress_indicator(progress); + } + + #[inline] + pub fn set_badge_count(&self, count: Option, _desktop_filename: Option) { + set_badge_label(count.map(|c| c.to_string())); + } + + #[inline] + pub fn set_badge_label(&self, label: Option) { + set_badge_label(label); + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + set_ns_theme(theme) + } +} + +pub struct EventLoop { + pub(crate) delegate: IdRef, + + window_target: Rc>, + panic_info: Rc, + + /// We make sure that the callback closure is dropped during a panic + /// by making the event loop own it. + /// + /// Every other reference should be a Weak reference which is only upgraded + /// into a strong reference in order to call the callback but then the + /// strong reference should be dropped as soon as possible. + _callback: Option, &RootWindowTarget, &mut ControlFlow)>>>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +impl EventLoop { + pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Self { + let panic_info: Rc = Default::default(); + setup_control_flow_observers(Rc::downgrade(&panic_info)); + + let delegate = unsafe { + if !util::is_main_thread() { + panic!("On macOS, `EventLoop` must be created on the main thread!"); + } + + // This must be done before `NSApp()` (equivalent to sending + // `sharedApplication`) is called anywhere else, or we'll end up + // with the wrong `NSApplication` class and the wrong thread could + // be marked as main. + let app: id = msg_send![APP_CLASS.0, sharedApplication]; + + let delegate = IdRef::new(msg_send![APP_DELEGATE_CLASS.0, new]); + let _pool = NSAutoreleasePool::new(); + let _: () = msg_send![app, setDelegate:*delegate]; + delegate + }; + + EventLoop { + delegate, + window_target: Rc::new(RootWindowTarget { + p: Default::default(), + _marker: PhantomData, + }), + panic_info, + _callback: None, + } + } + + pub fn window_target(&self) -> &RootWindowTarget { + &self.window_target + } + + pub fn run(mut self, callback: F) -> ! + where + F: 'static + FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), + { + let exit_code = self.run_return(callback); + process::exit(exit_code); + } + + pub fn run_return(&mut self, callback: F) -> i32 + where + F: FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), + { + // This transmute is always safe, in case it was reached through `run`, since our + // lifetime will be already 'static. In other cases caller should ensure that all data + // they passed to callback will actually outlive it, some apps just can't move + // everything to event loop, so this is something that they should care about. + let callback = unsafe { + mem::transmute::< + Rc, &RootWindowTarget, &mut ControlFlow)>>, + Rc, &RootWindowTarget, &mut ControlFlow)>>, + >(Rc::new(RefCell::new(callback))) + }; + + self._callback = Some(Rc::clone(&callback)); + + let mtm = MainThreadMarker::new().unwrap(); + + let exit_code = unsafe { + let _pool = NSAutoreleasePool::new(); + let app = NSApp(mtm); + + // A bit of juggling with the callback references to make sure + // that `self.callback` is the only owner of the callback. + let weak_cb: Weak<_> = Rc::downgrade(&callback); + mem::drop(callback); + + AppState::set_callback(weak_cb, Rc::clone(&self.window_target)); + let () = msg_send![&app, run]; + + if let Some(panic) = self.panic_info.take() { + drop(self._callback.take()); + resume_unwind(panic); + } + AppState::exit() + }; + drop(self._callback.take()); + + exit_code + } + + pub fn create_proxy(&self) -> Proxy { + Proxy::new(self.window_target.p.sender.clone()) + } +} + +#[inline] +pub unsafe fn post_dummy_event(target: &NSApplication) { + let event_class = class!(NSEvent); + let dummy_event: id = msg_send![ + event_class, + otherEventWithType: NSEventType::ApplicationDefined, + location: NSPoint::new(0.0, 0.0), + modifierFlags: NSEventModifierFlags::empty(), + timestamp: 0 as NSTimeInterval, + windowNumber: 0 as NSInteger, + context: nil, + subtype: NSEventSubtype::WindowExposed, + data1: 0 as NSInteger, + data2: 0 as NSInteger, + ]; + let () = msg_send![target, postEvent: dummy_event, atStart: YES]; +} + +/// Catches panics that happen inside `f` and when a panic +/// happens, stops the `sharedApplication` +#[inline] +pub fn stop_app_on_panic R + UnwindSafe, R>( + panic_info: Weak, + f: F, +) -> Option { + match catch_unwind(f) { + Ok(r) => Some(r), + Err(e) => { + // It's important that we set the panic before requesting a `stop` + // because some callback are still called during the `stop` message + // and we need to know in those callbacks if the application is currently + // panicking + { + let panic_info = panic_info.upgrade().unwrap(); + panic_info.set_panic(e); + } + unsafe { + let app_class = class!(NSApplication); + let app: Retained = msg_send![app_class, sharedApplication]; + let () = msg_send![&app, stop: nil]; + + // Posting a dummy event to get `stop` to take effect immediately. + // See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752 + post_dummy_event(&app); + } + None + } + } +} + +pub struct Proxy { + sender: Sender, + source: CFRunLoopSourceRef, +} + +unsafe impl Send for Proxy {} +unsafe impl Sync for Proxy {} + +impl Drop for Proxy { + fn drop(&mut self) { + unsafe { + CFRelease(self.source as _); + } + } +} + +impl Clone for Proxy { + fn clone(&self) -> Self { + Proxy::new(self.sender.clone()) + } +} + +impl Proxy { + fn new(sender: Sender) -> Self { + unsafe { + // just wake up the eventloop + extern "C" fn event_loop_proxy_handler(_: *mut c_void) {} + + // adding a Source to the main CFRunLoop lets us wake it up and + // process user events through the normal OS EventLoop mechanisms. + let rl = CFRunLoopGetMain(); + let mut context: CFRunLoopSourceContext = mem::zeroed(); + context.perform = Some(event_loop_proxy_handler); + let source = CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::MAX - 1, &mut context); + CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes); + CFRunLoopWakeUp(rl); + + Proxy { sender, source } + } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self + .sender + .send(event) + .map_err(|channel::SendError(x)| EventLoopClosed(x))?; + unsafe { + // let the main thread know there's a new event + CFRunLoopSourceSignal(self.source); + let rl = CFRunLoopGetMain(); + CFRunLoopWakeUp(rl); + } + Ok(()) + } +} diff --git a/vendor/tao/src/platform_impl/macos/ffi.rs b/vendor/tao/src/platform_impl/macos/ffi.rs new file mode 100644 index 0000000000..3b4a364214 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/ffi.rs @@ -0,0 +1,273 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +// TODO: Upstream these + +#![allow( + dead_code, + non_snake_case, + non_upper_case_globals, + clippy::enum_variant_names +)] + +use std::{ffi::c_void, ptr}; + +use core_foundation::{ + array::CFArrayRef, data::CFDataRef, dictionary::CFDictionaryRef, string::CFStringRef, + uuid::CFUUIDRef, +}; +use core_graphics::{ + base::CGError, + display::{boolean_t, CGDirectDisplayID, CGDisplayConfigRef}, + geometry::{CGPoint, CGRect}, +}; +use objc2::{ + encode::{Encode, Encoding}, + runtime::{AnyObject, Bool}, +}; +use objc2_foundation::NSInteger; + +#[allow(non_camel_case_types)] +pub type id = *mut AnyObject; +pub const nil: id = ptr::null_mut(); + +#[allow(non_camel_case_types)] +pub type BOOL = Bool; +#[allow(deprecated)] +pub const YES: Bool = Bool::YES; +#[allow(deprecated)] +pub const NO: Bool = Bool::NO; + +pub const NSNotFound: NSInteger = NSInteger::MAX; + +pub const kCGBaseWindowLevelKey: NSInteger = 0; +pub const kCGMinimumWindowLevelKey: NSInteger = 1; +pub const kCGDesktopWindowLevelKey: NSInteger = 2; +pub const kCGBackstopMenuLevelKey: NSInteger = 3; +pub const kCGNormalWindowLevelKey: NSInteger = 4; +pub const kCGFloatingWindowLevelKey: NSInteger = 5; +pub const kCGTornOffMenuWindowLevelKey: NSInteger = 6; +pub const kCGDockWindowLevelKey: NSInteger = 7; +pub const kCGMainMenuWindowLevelKey: NSInteger = 8; +pub const kCGStatusWindowLevelKey: NSInteger = 9; +pub const kCGModalPanelWindowLevelKey: NSInteger = 10; +pub const kCGPopUpMenuWindowLevelKey: NSInteger = 11; +pub const kCGDraggingWindowLevelKey: NSInteger = 12; +pub const kCGScreenSaverWindowLevelKey: NSInteger = 13; +pub const kCGMaximumWindowLevelKey: NSInteger = 14; +pub const kCGOverlayWindowLevelKey: NSInteger = 15; +pub const kCGHelpWindowLevelKey: NSInteger = 16; +pub const kCGUtilityWindowLevelKey: NSInteger = 17; +pub const kCGDesktopIconWindowLevelKey: NSInteger = 18; +pub const kCGCursorWindowLevelKey: NSInteger = 19; +pub const kCGNumberOfWindowLevelKeys: NSInteger = 20; + +#[derive(Debug, Clone, Copy)] +#[repr(isize)] +pub enum NSWindowLevel { + BelowNormalWindowLevel = (kCGBaseWindowLevelKey - 1) as _, + NSNormalWindowLevel = kCGBaseWindowLevelKey as _, + NSFloatingWindowLevel = kCGFloatingWindowLevelKey as _, + NSTornOffMenuWindowLevel = kCGTornOffMenuWindowLevelKey as _, + NSModalPanelWindowLevel = kCGModalPanelWindowLevelKey as _, + NSMainMenuWindowLevel = kCGMainMenuWindowLevelKey as _, + NSStatusWindowLevel = kCGStatusWindowLevelKey as _, + NSPopUpMenuWindowLevel = kCGPopUpMenuWindowLevelKey as _, + NSScreenSaverWindowLevel = kCGScreenSaverWindowLevelKey as _, +} + +unsafe impl Encode for NSWindowLevel { + const ENCODING: Encoding = isize::ENCODING; +} + +pub type CGDisplayFadeInterval = f32; +pub type CGDisplayReservationInterval = f32; +pub type CGDisplayBlendFraction = f32; + +pub const kCGDisplayBlendNormal: f32 = 0.0; +pub const kCGDisplayBlendSolidColor: f32 = 1.0; + +pub type CGDisplayFadeReservationToken = u32; +pub const kCGDisplayFadeReservationInvalidToken: CGDisplayFadeReservationToken = 0; + +pub type Boolean = u8; +pub const FALSE: Boolean = 0; +pub const TRUE: Boolean = 1; + +pub const kCGErrorSuccess: i32 = 0; +pub const kCGErrorFailure: i32 = 1000; +pub const kCGErrorIllegalArgument: i32 = 1001; +pub const kCGErrorInvalidConnection: i32 = 1002; +pub const kCGErrorInvalidContext: i32 = 1003; +pub const kCGErrorCannotComplete: i32 = 1004; +pub const kCGErrorNotImplemented: i32 = 1006; +pub const kCGErrorRangeCheck: i32 = 1007; +pub const kCGErrorTypeCheck: i32 = 1008; +pub const kCGErrorInvalidOperation: i32 = 1010; +pub const kCGErrorNoneAvailable: i32 = 1011; + +pub const IO1BitIndexedPixels: &str = "P"; +pub const IO2BitIndexedPixels: &str = "PP"; +pub const IO4BitIndexedPixels: &str = "PPPP"; +pub const IO8BitIndexedPixels: &str = "PPPPPPPP"; +pub const IO16BitDirectPixels: &str = "-RRRRRGGGGGBBBBB"; +pub const IO32BitDirectPixels: &str = "--------RRRRRRRRGGGGGGGGBBBBBBBB"; + +pub const kIO30BitDirectPixels: &str = "--RRRRRRRRRRGGGGGGGGGGBBBBBBBBBB"; +pub const kIO64BitDirectPixels: &str = "-16R16G16B16"; + +pub const kIO16BitFloatPixels: &str = "-16FR16FG16FB16"; +pub const kIO32BitFloatPixels: &str = "-32FR32FG32FB32"; + +pub const IOYUV422Pixels: &str = "Y4U2V2"; +pub const IO8BitOverlayPixels: &str = "O8"; + +pub type CGWindowLevel = i32; +pub type CGDisplayModeRef = *mut libc::c_void; + +// `CGDisplayCreateUUIDFromDisplayID` comes from the `ColorSync` framework. +// However, that framework was only introduced "publicly" in macOS 10.13. +// +// Since we want to support older versions, we can't link to `ColorSync` +// directly. Fortunately, it has always been available as a subframework of +// `ApplicationServices`, see: +// https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/OSX_Technology_Overview/SystemFrameworks/SystemFrameworks.html#//apple_ref/doc/uid/TP40001067-CH210-BBCFFIEG +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; +} + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + pub fn CGRestorePermanentDisplayConfiguration(); + pub fn CGDisplayCapture(display: CGDirectDisplayID) -> CGError; + pub fn CGDisplayRelease(display: CGDirectDisplayID) -> CGError; + pub fn CGConfigureDisplayFadeEffect( + config: CGDisplayConfigRef, + fadeOutSeconds: CGDisplayFadeInterval, + fadeInSeconds: CGDisplayFadeInterval, + fadeRed: f32, + fadeGreen: f32, + fadeBlue: f32, + ) -> CGError; + pub fn CGAcquireDisplayFadeReservation( + seconds: CGDisplayReservationInterval, + token: *mut CGDisplayFadeReservationToken, + ) -> CGError; + pub fn CGDisplayFade( + token: CGDisplayFadeReservationToken, + duration: CGDisplayFadeInterval, + startBlend: CGDisplayBlendFraction, + endBlend: CGDisplayBlendFraction, + redBlend: f32, + greenBlend: f32, + blueBlend: f32, + synchronous: Boolean, + ) -> CGError; + pub fn CGRectContainsPoint(rect: CGRect, point: CGPoint) -> boolean_t; + pub fn CGReleaseDisplayFadeReservation(token: CGDisplayFadeReservationToken) -> CGError; + pub fn CGShieldingWindowLevel() -> CGWindowLevel; + pub fn CGDisplaySetDisplayMode( + display: CGDirectDisplayID, + mode: CGDisplayModeRef, + options: CFDictionaryRef, + ) -> CGError; + pub fn CGDisplayCopyAllDisplayModes( + display: CGDirectDisplayID, + options: CFDictionaryRef, + ) -> CFArrayRef; + pub fn CGDisplayModeGetPixelWidth(mode: CGDisplayModeRef) -> usize; + pub fn CGDisplayModeGetPixelHeight(mode: CGDisplayModeRef) -> usize; + pub fn CGDisplayModeGetRefreshRate(mode: CGDisplayModeRef) -> f64; + pub fn CGDisplayModeCopyPixelEncoding(mode: CGDisplayModeRef) -> CFStringRef; + pub fn CGDisplayModeRetain(mode: CGDisplayModeRef); + pub fn CGDisplayModeRelease(mode: CGDisplayModeRef); +} + +#[repr(transparent)] +pub struct TISInputSource(std::ffi::c_void); +pub type TISInputSourceRef = *mut TISInputSource; + +#[repr(transparent)] +pub struct UCKeyboardLayout(std::ffi::c_void); + +pub type OptionBits = u32; +pub type UniCharCount = std::os::raw::c_ulong; +pub type UniChar = u16; +pub type OSStatus = i32; + +#[allow(non_upper_case_globals)] +pub const kUCKeyActionDisplay: u16 = 3; +#[allow(non_upper_case_globals)] +pub const kUCKeyTranslateNoDeadKeysMask: OptionBits = 1; + +#[link(name = "Carbon", kind = "framework")] +extern "C" { + pub static kTISPropertyUnicodeKeyLayoutData: CFStringRef; + + #[allow(non_snake_case)] + pub fn TISGetInputSourceProperty( + inputSource: TISInputSourceRef, + propertyKey: CFStringRef, + ) -> CFDataRef; + + pub fn TISCopyCurrentKeyboardLayoutInputSource() -> TISInputSourceRef; + + pub fn LMGetKbdType() -> u8; + + #[allow(non_snake_case)] + pub fn UCKeyTranslate( + keyLayoutPtr: *const UCKeyboardLayout, + virtualKeyCode: u16, + keyAction: u16, + modifierKeyState: u32, + keyboardType: u32, + keyTranslateOptions: OptionBits, + deadKeyState: *mut u32, + maxStringLength: UniCharCount, + actualStringLength: *mut UniCharCount, + unicodeString: *mut UniChar, + ) -> OSStatus; +} + +mod core_video { + use super::*; + + #[link(name = "CoreVideo", kind = "framework")] + extern "C" {} + + // CVBase.h + + pub type CVTimeFlags = i32; // int32_t + pub const kCVTimeIsIndefinite: CVTimeFlags = 1 << 0; + + #[repr(C)] + #[derive(Debug, Clone)] + pub struct CVTime { + pub time_value: i64, // int64_t + pub time_scale: i32, // int32_t + pub flags: i32, // int32_t + } + + // CVReturn.h + + pub type CVReturn = i32; // int32_t + pub const kCVReturnSuccess: CVReturn = 0; + + // CVDisplayLink.h + + pub type CVDisplayLinkRef = *mut c_void; + + extern "C" { + pub fn CVDisplayLinkCreateWithCGDisplay( + displayID: CGDirectDisplayID, + displayLinkOut: *mut CVDisplayLinkRef, + ) -> CVReturn; + pub fn CVDisplayLinkGetNominalOutputVideoRefreshPeriod(displayLink: CVDisplayLinkRef) + -> CVTime; + pub fn CVDisplayLinkRelease(displayLink: CVDisplayLinkRef); + } +} + +pub use core_video::*; diff --git a/vendor/tao/src/platform_impl/macos/icon.rs b/vendor/tao/src/platform_impl/macos/icon.rs new file mode 100644 index 0000000000..c5c7439d06 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/icon.rs @@ -0,0 +1,14 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use crate::icon::{BadIcon, RgbaIcon}; + +#[derive(Debug, Clone)] +pub struct PlatformIcon(RgbaIcon); + +impl PlatformIcon { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + Ok(PlatformIcon(RgbaIcon::from_rgba(rgba, width, height)?)) + } +} diff --git a/vendor/tao/src/platform_impl/macos/keycode.rs b/vendor/tao/src/platform_impl/macos/keycode.rs new file mode 100644 index 0000000000..4418cfccef --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/keycode.rs @@ -0,0 +1,264 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use crate::keyboard::{KeyCode, NativeKeyCode}; + +pub fn keycode_to_scancode(code: KeyCode) -> Option { + match code { + KeyCode::KeyA => Some(0x00), + KeyCode::KeyS => Some(0x01), + KeyCode::KeyD => Some(0x02), + KeyCode::KeyF => Some(0x03), + KeyCode::KeyH => Some(0x04), + KeyCode::KeyG => Some(0x05), + KeyCode::KeyZ => Some(0x06), + KeyCode::KeyX => Some(0x07), + KeyCode::KeyC => Some(0x08), + KeyCode::KeyV => Some(0x09), + KeyCode::KeyB => Some(0x0b), + KeyCode::KeyQ => Some(0x0c), + KeyCode::KeyW => Some(0x0d), + KeyCode::KeyE => Some(0x0e), + KeyCode::KeyR => Some(0x0f), + KeyCode::KeyY => Some(0x10), + KeyCode::KeyT => Some(0x11), + KeyCode::Digit1 => Some(0x12), + KeyCode::Digit2 => Some(0x13), + KeyCode::Digit3 => Some(0x14), + KeyCode::Digit4 => Some(0x15), + KeyCode::Digit6 => Some(0x16), + KeyCode::Digit5 => Some(0x17), + KeyCode::Equal => Some(0x18), + KeyCode::Digit9 => Some(0x19), + KeyCode::Digit7 => Some(0x1a), + KeyCode::Minus => Some(0x1b), + KeyCode::Digit8 => Some(0x1c), + KeyCode::Digit0 => Some(0x1d), + KeyCode::BracketRight => Some(0x1e), + KeyCode::KeyO => Some(0x1f), + KeyCode::KeyU => Some(0x20), + KeyCode::BracketLeft => Some(0x21), + KeyCode::KeyI => Some(0x22), + KeyCode::KeyP => Some(0x23), + KeyCode::Enter => Some(0x24), + KeyCode::KeyL => Some(0x25), + KeyCode::KeyJ => Some(0x26), + KeyCode::Quote => Some(0x27), + KeyCode::KeyK => Some(0x28), + KeyCode::Semicolon => Some(0x29), + KeyCode::Backslash => Some(0x2a), + KeyCode::Comma => Some(0x2b), + KeyCode::Slash => Some(0x2c), + KeyCode::KeyN => Some(0x2d), + KeyCode::KeyM => Some(0x2e), + KeyCode::Period => Some(0x2f), + KeyCode::Tab => Some(0x30), + KeyCode::Space => Some(0x31), + KeyCode::Backquote => Some(0x32), + KeyCode::Backspace => Some(0x33), + KeyCode::Escape => Some(0x35), + KeyCode::SuperRight => Some(0x36), + KeyCode::SuperLeft => Some(0x37), + KeyCode::ShiftLeft => Some(0x38), + KeyCode::AltLeft => Some(0x3a), + KeyCode::ControlLeft => Some(0x3b), + KeyCode::ShiftRight => Some(0x3c), + KeyCode::AltRight => Some(0x3d), + KeyCode::ControlRight => Some(0x3e), + KeyCode::F17 => Some(0x40), + KeyCode::NumpadDecimal => Some(0x41), + KeyCode::NumpadMultiply => Some(0x43), + KeyCode::NumpadAdd => Some(0x45), + KeyCode::NumLock => Some(0x47), + KeyCode::AudioVolumeUp => Some(0x49), + KeyCode::AudioVolumeDown => Some(0x4a), + KeyCode::NumpadDivide => Some(0x4b), + KeyCode::NumpadEnter => Some(0x4c), + KeyCode::NumpadSubtract => Some(0x4e), + KeyCode::F18 => Some(0x4f), + KeyCode::F19 => Some(0x50), + KeyCode::NumpadEqual => Some(0x51), + KeyCode::Numpad0 => Some(0x52), + KeyCode::Numpad1 => Some(0x53), + KeyCode::Numpad2 => Some(0x54), + KeyCode::Numpad3 => Some(0x55), + KeyCode::Numpad4 => Some(0x56), + KeyCode::Numpad5 => Some(0x57), + KeyCode::Numpad6 => Some(0x58), + KeyCode::Numpad7 => Some(0x59), + KeyCode::F20 => Some(0x5a), + KeyCode::Numpad8 => Some(0x5b), + KeyCode::Numpad9 => Some(0x5c), + KeyCode::IntlYen => Some(0x5d), + KeyCode::F5 => Some(0x60), + KeyCode::F6 => Some(0x61), + KeyCode::F7 => Some(0x62), + KeyCode::F3 => Some(0x63), + KeyCode::F8 => Some(0x64), + KeyCode::F9 => Some(0x65), + KeyCode::F11 => Some(0x67), + KeyCode::F13 => Some(0x69), + KeyCode::F16 => Some(0x6a), + KeyCode::F14 => Some(0x6b), + KeyCode::F10 => Some(0x6d), + KeyCode::F12 => Some(0x6f), + KeyCode::F15 => Some(0x71), + KeyCode::Insert => Some(0x72), + KeyCode::Home => Some(0x73), + KeyCode::PageUp => Some(0x74), + KeyCode::Delete => Some(0x75), + KeyCode::F4 => Some(0x76), + KeyCode::End => Some(0x77), + KeyCode::F2 => Some(0x78), + KeyCode::PageDown => Some(0x79), + KeyCode::F1 => Some(0x7a), + KeyCode::ArrowLeft => Some(0x7b), + KeyCode::ArrowRight => Some(0x7c), + KeyCode::ArrowDown => Some(0x7d), + KeyCode::ArrowUp => Some(0x7e), + _ => None, + } +} + +pub fn keycode_from_scancode(scancode: u32) -> KeyCode { + match scancode { + 0x00 => KeyCode::KeyA, + 0x01 => KeyCode::KeyS, + 0x02 => KeyCode::KeyD, + 0x03 => KeyCode::KeyF, + 0x04 => KeyCode::KeyH, + 0x05 => KeyCode::KeyG, + 0x06 => KeyCode::KeyZ, + 0x07 => KeyCode::KeyX, + 0x08 => KeyCode::KeyC, + 0x09 => KeyCode::KeyV, + //0x0a => World 1, + 0x0b => KeyCode::KeyB, + 0x0c => KeyCode::KeyQ, + 0x0d => KeyCode::KeyW, + 0x0e => KeyCode::KeyE, + 0x0f => KeyCode::KeyR, + 0x10 => KeyCode::KeyY, + 0x11 => KeyCode::KeyT, + 0x12 => KeyCode::Digit1, + 0x13 => KeyCode::Digit2, + 0x14 => KeyCode::Digit3, + 0x15 => KeyCode::Digit4, + 0x16 => KeyCode::Digit6, + 0x17 => KeyCode::Digit5, + 0x18 => KeyCode::Equal, + 0x19 => KeyCode::Digit9, + 0x1a => KeyCode::Digit7, + 0x1b => KeyCode::Minus, + 0x1c => KeyCode::Digit8, + 0x1d => KeyCode::Digit0, + 0x1e => KeyCode::BracketRight, + 0x1f => KeyCode::KeyO, + 0x20 => KeyCode::KeyU, + 0x21 => KeyCode::BracketLeft, + 0x22 => KeyCode::KeyI, + 0x23 => KeyCode::KeyP, + 0x24 => KeyCode::Enter, + 0x25 => KeyCode::KeyL, + 0x26 => KeyCode::KeyJ, + 0x27 => KeyCode::Quote, + 0x28 => KeyCode::KeyK, + 0x29 => KeyCode::Semicolon, + 0x2a => KeyCode::Backslash, + 0x2b => KeyCode::Comma, + 0x2c => KeyCode::Slash, + 0x2d => KeyCode::KeyN, + 0x2e => KeyCode::KeyM, + 0x2f => KeyCode::Period, + 0x30 => KeyCode::Tab, + 0x31 => KeyCode::Space, + 0x32 => KeyCode::Backquote, + 0x33 => KeyCode::Backspace, + //0x34 => unknown, + 0x35 => KeyCode::Escape, + 0x36 => KeyCode::SuperRight, + 0x37 => KeyCode::SuperLeft, + 0x38 => KeyCode::ShiftLeft, + 0x39 => KeyCode::CapsLock, + 0x3a => KeyCode::AltLeft, + 0x3b => KeyCode::ControlLeft, + 0x3c => KeyCode::ShiftRight, + 0x3d => KeyCode::AltRight, + 0x3e => KeyCode::ControlRight, + 0x3f => KeyCode::Fn, + 0x40 => KeyCode::F17, + 0x41 => KeyCode::NumpadDecimal, + //0x42 -> unknown, + 0x43 => KeyCode::NumpadMultiply, + //0x44 => unknown, + 0x45 => KeyCode::NumpadAdd, + //0x46 => unknown, + 0x47 => KeyCode::NumLock, + //0x48 => KeyCode::NumpadClear, + + // TODO: (Artur) for me, kVK_VolumeUp is 0x48 + // macOS 10.11 + // /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h + //0x49 => KeyCode::AudioVolumeUp, + 0x49 => KeyCode::AudioVolumeDown, + 0x4b => KeyCode::NumpadDivide, + 0x4c => KeyCode::NumpadEnter, + //0x4d => unknown, + 0x4e => KeyCode::NumpadSubtract, + 0x4f => KeyCode::F18, + 0x50 => KeyCode::F19, + 0x51 => KeyCode::NumpadEqual, + 0x52 => KeyCode::Numpad0, + 0x53 => KeyCode::Numpad1, + 0x54 => KeyCode::Numpad2, + 0x55 => KeyCode::Numpad3, + 0x56 => KeyCode::Numpad4, + 0x57 => KeyCode::Numpad5, + 0x58 => KeyCode::Numpad6, + 0x59 => KeyCode::Numpad7, + 0x5a => KeyCode::F20, + 0x5b => KeyCode::Numpad8, + 0x5c => KeyCode::Numpad9, + 0x5d => KeyCode::IntlYen, + //0x5e => JIS Ro, + //0x5f => unknown, + 0x60 => KeyCode::F5, + 0x61 => KeyCode::F6, + 0x62 => KeyCode::F7, + 0x63 => KeyCode::F3, + 0x64 => KeyCode::F8, + 0x65 => KeyCode::F9, + //0x66 => JIS Eisuu (macOS), + 0x67 => KeyCode::F11, + //0x68 => JIS Kanna (macOS), + 0x69 => KeyCode::F13, + 0x6a => KeyCode::F16, + 0x6b => KeyCode::F14, + //0x6c => unknown, + 0x6d => KeyCode::F10, + //0x6e => unknown, + 0x6f => KeyCode::F12, + //0x70 => unknown, + 0x71 => KeyCode::F15, + 0x72 => KeyCode::Insert, + 0x73 => KeyCode::Home, + 0x74 => KeyCode::PageUp, + 0x75 => KeyCode::Delete, + 0x76 => KeyCode::F4, + 0x77 => KeyCode::End, + 0x78 => KeyCode::F2, + 0x79 => KeyCode::PageDown, + 0x7a => KeyCode::F1, + 0x7b => KeyCode::ArrowLeft, + 0x7c => KeyCode::ArrowRight, + 0x7d => KeyCode::ArrowDown, + 0x7e => KeyCode::ArrowUp, + //0x7f => unknown, + + // 0xA is the caret (^) an macOS's German QERTZ layout. This key is at the same location as + // backquote (`) on Windows' US layout. + 0xa => KeyCode::Backquote, + _ => KeyCode::Unidentified(NativeKeyCode::MacOS(scancode as u16)), + } +} diff --git a/vendor/tao/src/platform_impl/macos/mod.rs b/vendor/tao/src/platform_impl/macos/mod.rs new file mode 100644 index 0000000000..0c24a5257e --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/mod.rs @@ -0,0 +1,97 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +mod app; +mod app_delegate; +mod app_state; +mod badge; +mod dock; +mod event; +mod event_loop; +mod ffi; +mod icon; +mod keycode; +mod monitor; +mod observer; +mod progress_bar; +mod util; +mod view; +mod window; +mod window_delegate; + +use std::{fmt, ops::Deref, sync::Arc}; + +pub(crate) use self::event_loop::PlatformSpecificEventLoopAttributes; +pub use self::{ + app_delegate::get_aux_state_mut, + event::KeyEventExtra, + event_loop::{EventLoop, EventLoopWindowTarget, Proxy as EventLoopProxy}, + keycode::{keycode_from_scancode, keycode_to_scancode}, + monitor::{MonitorHandle, VideoMode}, + progress_bar::set_progress_indicator, + window::{Id as WindowId, Parent, PlatformSpecificWindowBuilderAttributes, UnownedWindow}, +}; +use crate::{ + error::OsError as RootOsError, event::DeviceId as RootDeviceId, window::WindowAttributes, +}; +pub(crate) use badge::set_badge_label; +pub(crate) use dock::set_dock_visibility; +pub(crate) use icon::PlatformIcon; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId; + +impl DeviceId { + pub unsafe fn dummy() -> Self { + DeviceId + } +} + +// Constant device ID; to be removed when if backend is updated to report real device IDs. +pub(crate) const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId); + +pub struct Window { + window: Arc, + // We keep this around so that it doesn't get dropped until the window does. + #[allow(dead_code)] + delegate: util::IdRef, +} + +#[non_exhaustive] +#[derive(Debug)] +pub enum OsError { + CGError(core_graphics::base::CGError), + CreationError(&'static str), +} + +unsafe impl Send for Window {} +unsafe impl Sync for Window {} + +impl Deref for Window { + type Target = UnownedWindow; + #[inline] + fn deref(&self) -> &Self::Target { + &self.window + } +} + +impl Window { + pub fn new( + _window_target: &EventLoopWindowTarget, + attributes: WindowAttributes, + pl_attribs: PlatformSpecificWindowBuilderAttributes, + ) -> Result { + let (window, delegate) = UnownedWindow::new(attributes, pl_attribs)?; + Ok(Window { window, delegate }) + } +} + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OsError::CGError(e) => f.pad(&format!("CGError {}", e)), + OsError::CreationError(e) => f.pad(e), + } + } +} diff --git a/vendor/tao/src/platform_impl/macos/monitor.rs b/vendor/tao/src/platform_impl/macos/monitor.rs new file mode 100644 index 0000000000..6dd78109fc --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/monitor.rs @@ -0,0 +1,338 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{collections::VecDeque, fmt}; + +use super::ffi::{self, id, nil, CGRectContainsPoint}; +use crate::{ + dpi::{PhysicalPosition, PhysicalSize}, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, +}; +use core_foundation::{ + array::{CFArrayGetCount, CFArrayGetValueAtIndex}, + base::{CFRelease, TCFType}, + string::CFString, +}; +use core_graphics::{ + display::{CGDirectDisplayID, CGDisplay, CGDisplayBounds}, + geometry::CGPoint, +}; +use objc2::{msg_send, rc::Retained}; +use objc2_app_kit::NSScreen; +use objc2_foundation::{MainThreadMarker, NSString, NSUInteger}; + +#[derive(Clone)] +pub struct VideoMode { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, + pub(crate) monitor: MonitorHandle, + pub(crate) native_mode: NativeDisplayMode, +} + +impl PartialEq for VideoMode { + fn eq(&self, other: &Self) -> bool { + self.size == other.size + && self.bit_depth == other.bit_depth + && self.refresh_rate == other.refresh_rate + && self.monitor == other.monitor + } +} + +impl Eq for VideoMode {} + +impl std::hash::Hash for VideoMode { + fn hash(&self, state: &mut H) { + self.size.hash(state); + self.bit_depth.hash(state); + self.refresh_rate.hash(state); + self.monitor.hash(state); + } +} + +impl std::fmt::Debug for VideoMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VideoMode") + .field("size", &self.size) + .field("bit_depth", &self.bit_depth) + .field("refresh_rate", &self.refresh_rate) + .field("monitor", &self.monitor) + .finish() + } +} + +pub struct NativeDisplayMode(pub ffi::CGDisplayModeRef); + +unsafe impl Send for NativeDisplayMode {} + +impl Drop for NativeDisplayMode { + fn drop(&mut self) { + unsafe { + ffi::CGDisplayModeRelease(self.0); + } + } +} + +impl Clone for NativeDisplayMode { + fn clone(&self) -> Self { + unsafe { + ffi::CGDisplayModeRetain(self.0); + } + NativeDisplayMode(self.0) + } +} + +impl VideoMode { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: self.monitor.clone(), + } + } +} + +#[derive(Clone)] +pub struct MonitorHandle(CGDirectDisplayID); + +// `CGDirectDisplayID` changes on video mode change, so we cannot rely on that +// for comparisons, but we can use `CGDisplayCreateUUIDFromDisplayID` to get an +// unique identifier that persists even across system reboots +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0) + == ffi::CGDisplayCreateUUIDFromDisplayID(other.0) + } + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0) + .cmp(&ffi::CGDisplayCreateUUIDFromDisplayID(other.0)) + } + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0).hash(state); + } + } +} + +pub fn available_monitors() -> VecDeque { + if let Ok(displays) = CGDisplay::active_displays() { + let mut monitors = VecDeque::with_capacity(displays.len()); + for display in displays { + monitors.push_back(MonitorHandle(display)); + } + monitors + } else { + VecDeque::with_capacity(0) + } +} + +pub fn primary_monitor() -> MonitorHandle { + MonitorHandle(CGDisplay::main().id) +} + +// `from_point` get a monitor handle which contains the given point. +pub fn from_point(x: f64, y: f64) -> Option { + unsafe { + for monitor in available_monitors() { + let bound = CGDisplayBounds(monitor.0); + if CGRectContainsPoint(bound, CGPoint::new(x, y)) > 0 { + return Some(monitor); + } + } + } + None +} + +impl fmt::Debug for MonitorHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // TODO: Do this using the proper fmt API + #[derive(Debug)] + #[allow(dead_code)] // none of the member reads in Debug::fmt are detected + struct MonitorHandle { + name: Option, + native_identifier: u32, + size: PhysicalSize, + position: PhysicalPosition, + scale_factor: f64, + } + + let monitor_id_proxy = MonitorHandle { + name: self.name(), + native_identifier: self.native_identifier(), + size: self.size(), + position: self.position(), + scale_factor: self.scale_factor(), + }; + + monitor_id_proxy.fmt(f) + } +} + +impl MonitorHandle { + pub fn new(id: CGDirectDisplayID) -> Self { + MonitorHandle(id) + } + + pub fn name(&self) -> Option { + let MonitorHandle(display_id) = *self; + let screen_num = CGDisplay::new(display_id).model_number(); + Some(format!("Monitor #{}", screen_num)) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + self.0 + } + + pub fn size(&self) -> PhysicalSize { + let MonitorHandle(display_id) = *self; + let display = CGDisplay::new(display_id); + let height = display.pixels_high(); + let width = display.pixels_wide(); + PhysicalSize::from_logical::<_, f64>((width as f64, height as f64), self.scale_factor()) + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + let bounds = unsafe { CGDisplayBounds(self.native_identifier()) }; + PhysicalPosition::from_logical::<_, f64>( + (bounds.origin.x as f64, bounds.origin.y as f64), + self.scale_factor(), + ) + } + + pub fn scale_factor(&self) -> f64 { + let screen = match self.ns_screen() { + Some(screen) => screen, + None => return 1.0, // default to 1.0 when we can't find the screen + }; + NSScreen::backingScaleFactor(&screen) as f64 + } + + pub fn video_modes(&self) -> impl Iterator { + let cv_refresh_rate = unsafe { + let mut display_link = std::ptr::null_mut(); + assert_eq!( + ffi::CVDisplayLinkCreateWithCGDisplay(self.0, &mut display_link), + ffi::kCVReturnSuccess + ); + let time = ffi::CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link); + ffi::CVDisplayLinkRelease(display_link); + + // This value is indefinite if an invalid display link was specified + assert!(time.flags & ffi::kCVTimeIsIndefinite == 0); + + time.time_scale as i64 / time.time_value + }; + + let monitor = self.clone(); + + unsafe { + let modes = { + let array = ffi::CGDisplayCopyAllDisplayModes(self.0, std::ptr::null()); + assert!(!array.is_null(), "failed to get list of display modes"); + let array_count = CFArrayGetCount(array); + let modes: Vec<_> = (0..array_count) + .map(move |i| { + let mode = CFArrayGetValueAtIndex(array, i) as *mut _; + ffi::CGDisplayModeRetain(mode); + mode + }) + .collect(); + CFRelease(array as *const _); + modes + }; + + modes.into_iter().map(move |mode| { + let cg_refresh_rate = ffi::CGDisplayModeGetRefreshRate(mode).round() as i64; + + // CGDisplayModeGetRefreshRate returns 0.0 for any display that + // isn't a CRT + let refresh_rate = if cg_refresh_rate > 0 { + cg_refresh_rate + } else { + cv_refresh_rate + }; + + let pixel_encoding = + CFString::wrap_under_create_rule(ffi::CGDisplayModeCopyPixelEncoding(mode)).to_string(); + let bit_depth = if pixel_encoding.eq_ignore_ascii_case(ffi::IO32BitDirectPixels) { + 32 + } else if pixel_encoding.eq_ignore_ascii_case(ffi::IO16BitDirectPixels) { + 16 + } else if pixel_encoding.eq_ignore_ascii_case(ffi::kIO30BitDirectPixels) { + 30 + } else { + unimplemented!() + }; + + let video_mode = VideoMode { + size: ( + ffi::CGDisplayModeGetPixelWidth(mode) as u32, + ffi::CGDisplayModeGetPixelHeight(mode) as u32, + ), + refresh_rate: refresh_rate as u16, + bit_depth, + monitor: monitor.clone(), + native_mode: NativeDisplayMode(mode), + }; + + RootVideoMode { video_mode } + }) + } + } + + pub(crate) fn ns_screen(&self) -> Option> { + // SAFETY: TODO. + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + unsafe { + let uuid = ffi::CGDisplayCreateUUIDFromDisplayID(self.0); + let screens = NSScreen::screens(mtm); + let count: NSUInteger = msg_send![&screens, count]; + let key = NSString::from_str("NSScreenNumber"); + for i in 0..count { + let screen: Retained = msg_send![&screens, objectAtIndex: i as NSUInteger]; + let device_description = NSScreen::deviceDescription(&screen); + let value: id = msg_send![&device_description, objectForKey: &*key]; + if value != nil { + let other_native_id: NSUInteger = msg_send![value, unsignedIntegerValue]; + let other_uuid = + ffi::CGDisplayCreateUUIDFromDisplayID(other_native_id as CGDirectDisplayID); + if uuid == other_uuid { + return Some(screen); + } + } + } + None + } + } +} diff --git a/vendor/tao/src/platform_impl/macos/observer.rs b/vendor/tao/src/platform_impl/macos/observer.rs new file mode 100644 index 0000000000..02524ea7d4 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/observer.rs @@ -0,0 +1,305 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + self, + os::raw::*, + panic::{AssertUnwindSafe, UnwindSafe}, + ptr, + rc::Weak, + time::Instant, +}; + +use crate::platform_impl::platform::{ + app_state::AppState, + event_loop::{stop_app_on_panic, PanicInfo}, + ffi, +}; + +#[link(name = "CoreFoundation", kind = "framework")] +extern "C" { + pub static kCFRunLoopCommonModes: CFRunLoopMode; + + pub fn CFRunLoopGetMain() -> CFRunLoopRef; + pub fn CFRunLoopWakeUp(rl: CFRunLoopRef); + + pub fn CFRunLoopObserverCreate( + allocator: CFAllocatorRef, + activities: CFOptionFlags, + repeats: ffi::Boolean, + order: CFIndex, + callout: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, + ) -> CFRunLoopObserverRef; + pub fn CFRunLoopAddObserver( + rl: CFRunLoopRef, + observer: CFRunLoopObserverRef, + mode: CFRunLoopMode, + ); + + pub fn CFRunLoopTimerCreate( + allocator: CFAllocatorRef, + fireDate: CFAbsoluteTime, + interval: CFTimeInterval, + flags: CFOptionFlags, + order: CFIndex, + callout: CFRunLoopTimerCallBack, + context: *mut CFRunLoopTimerContext, + ) -> CFRunLoopTimerRef; + pub fn CFRunLoopAddTimer(rl: CFRunLoopRef, timer: CFRunLoopTimerRef, mode: CFRunLoopMode); + pub fn CFRunLoopTimerSetNextFireDate(timer: CFRunLoopTimerRef, fireDate: CFAbsoluteTime); + pub fn CFRunLoopTimerInvalidate(time: CFRunLoopTimerRef); + + pub fn CFRunLoopSourceCreate( + allocator: CFAllocatorRef, + order: CFIndex, + context: *mut CFRunLoopSourceContext, + ) -> CFRunLoopSourceRef; + pub fn CFRunLoopAddSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFRunLoopMode); + #[allow(dead_code)] + pub fn CFRunLoopSourceInvalidate(source: CFRunLoopSourceRef); + pub fn CFRunLoopSourceSignal(source: CFRunLoopSourceRef); + + pub fn CFAbsoluteTimeGetCurrent() -> CFAbsoluteTime; + pub fn CFRelease(cftype: *const c_void); +} + +pub enum CFAllocator {} +pub type CFAllocatorRef = *mut CFAllocator; +pub enum CFRunLoop {} +pub type CFRunLoopRef = *mut CFRunLoop; +pub type CFRunLoopMode = CFStringRef; +pub enum CFRunLoopObserver {} +pub type CFRunLoopObserverRef = *mut CFRunLoopObserver; +pub enum CFRunLoopTimer {} +pub type CFRunLoopTimerRef = *mut CFRunLoopTimer; +pub enum CFRunLoopSource {} +pub type CFRunLoopSourceRef = *mut CFRunLoopSource; +pub enum CFString {} +pub type CFStringRef = *const CFString; + +pub type CFHashCode = c_ulong; +pub type CFIndex = c_long; +pub type CFOptionFlags = c_ulong; +pub type CFRunLoopActivity = CFOptionFlags; + +pub type CFAbsoluteTime = CFTimeInterval; +pub type CFTimeInterval = f64; + +#[allow(non_upper_case_globals)] +pub const kCFRunLoopEntry: CFRunLoopActivity = 0; +#[allow(non_upper_case_globals)] +pub const kCFRunLoopBeforeWaiting: CFRunLoopActivity = 1 << 5; +#[allow(non_upper_case_globals)] +pub const kCFRunLoopAfterWaiting: CFRunLoopActivity = 1 << 6; +#[allow(non_upper_case_globals)] +pub const kCFRunLoopExit: CFRunLoopActivity = 1 << 7; + +pub type CFRunLoopObserverCallBack = + extern "C" fn(observer: CFRunLoopObserverRef, activity: CFRunLoopActivity, info: *mut c_void); +pub type CFRunLoopTimerCallBack = extern "C" fn(timer: CFRunLoopTimerRef, info: *mut c_void); + +pub enum CFRunLoopTimerContext {} + +/// This mirrors the struct with the same name from Core Foundation. +/// https://developer.apple.com/documentation/corefoundation/cfrunloopobservercontext?language=objc +#[allow(non_snake_case)] +#[repr(C)] +pub struct CFRunLoopObserverContext { + pub version: CFIndex, + pub info: *mut c_void, + pub retain: Option *const c_void>, + pub release: Option, + pub copyDescription: Option CFStringRef>, +} + +#[allow(non_snake_case)] +#[repr(C)] +pub struct CFRunLoopSourceContext { + pub version: CFIndex, + pub info: *mut c_void, + pub retain: Option *const c_void>, + pub release: Option, + pub copyDescription: Option CFStringRef>, + pub equal: Option ffi::Boolean>, + pub hash: Option CFHashCode>, + pub schedule: Option, + pub cancel: Option, + pub perform: Option, +} + +unsafe fn control_flow_handler(panic_info: *mut c_void, f: F) +where + F: FnOnce(Weak) + UnwindSafe, +{ + let info_from_raw = Weak::from_raw(panic_info as *mut PanicInfo); + // Asserting unwind safety on this type should be fine because `PanicInfo` is + // `RefUnwindSafe` and `Rc` is `UnwindSafe` if `T` is `RefUnwindSafe`. + let panic_info = AssertUnwindSafe(Weak::clone(&info_from_raw)); + // `from_raw` takes ownership of the data behind the pointer. + // But if this scope takes ownership of the weak pointer, then + // the weak pointer will get free'd at the end of the scope. + // However we want to keep that weak reference around after the function. + std::mem::forget(info_from_raw); + + stop_app_on_panic(Weak::clone(&panic_info), move || { + let _ = &panic_info; + f(panic_info.0) + }); +} + +// begin is queued with the highest priority to ensure it is processed before other observers +extern "C" fn control_flow_begin_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + panic_info: *mut c_void, +) { + unsafe { + control_flow_handler(panic_info, |panic_info| { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopAfterWaiting => { + //trace!("Triggered `CFRunLoopAfterWaiting`"); + AppState::wakeup(panic_info); + //trace!("Completed `CFRunLoopAfterWaiting`"); + } + kCFRunLoopEntry => unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } + }); + } +} + +// end is queued with the lowest priority to ensure it is processed after other observers +// without that, LoopDestroyed would get sent after MainEventsCleared +extern "C" fn control_flow_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + panic_info: *mut c_void, +) { + unsafe { + control_flow_handler(panic_info, |panic_info| { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => { + //trace!("Triggered `CFRunLoopBeforeWaiting`"); + AppState::cleared(panic_info); + //trace!("Completed `CFRunLoopBeforeWaiting`"); + } + kCFRunLoopExit => (), //unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } + }); + } +} + +struct RunLoop(CFRunLoopRef); + +impl RunLoop { + unsafe fn get() -> Self { + RunLoop(CFRunLoopGetMain()) + } + + unsafe fn add_observer( + &self, + flags: CFOptionFlags, + priority: CFIndex, + handler: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, + ) { + let observer = CFRunLoopObserverCreate( + ptr::null_mut(), + flags, + ffi::TRUE, // Indicates we want this to run repeatedly + priority, // The lower the value, the sooner this will run + handler, + context, + ); + CFRunLoopAddObserver(self.0, observer, kCFRunLoopCommonModes); + } +} + +pub fn setup_control_flow_observers(panic_info: Weak) { + unsafe { + let mut context = CFRunLoopObserverContext { + info: Weak::into_raw(panic_info) as *mut _, + version: 0, + retain: None, + release: None, + copyDescription: None, + }; + let run_loop = RunLoop::get(); + run_loop.add_observer( + kCFRunLoopEntry | kCFRunLoopAfterWaiting, + CFIndex::MIN, + control_flow_begin_handler, + &mut context as *mut _, + ); + run_loop.add_observer( + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + CFIndex::MAX, + control_flow_end_handler, + &mut context as *mut _, + ); + } +} + +pub struct EventLoopWaker { + timer: CFRunLoopTimerRef, +} + +impl Drop for EventLoopWaker { + fn drop(&mut self) { + unsafe { + CFRunLoopTimerInvalidate(self.timer); + CFRelease(self.timer as _); + } + } +} + +impl Default for EventLoopWaker { + fn default() -> EventLoopWaker { + extern "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {} + unsafe { + // Create a timer with a 0.1µs interval (1ns does not work) to mimic polling. + // It is initially setup with a first fire time really far into the + // future, but that gets changed to fire immediately in did_finish_launching + let timer = CFRunLoopTimerCreate( + ptr::null_mut(), + f64::MAX, + 0.000_000_1, + 0, + 0, + wakeup_main_loop, + ptr::null_mut(), + ); + CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes); + EventLoopWaker { timer } + } + } +} + +impl EventLoopWaker { + pub fn stop(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MAX) } + } + + pub fn start(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MIN) } + } + + pub fn start_at(&mut self, instant: Instant) { + let now = Instant::now(); + if now >= instant { + self.start(); + } else { + unsafe { + let current = CFAbsoluteTimeGetCurrent(); + let duration = instant - now; + let fsecs = duration.subsec_nanos() as f64 / 1_000_000_000.0 + duration.as_secs() as f64; + CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs) + } + } + } +} diff --git a/vendor/tao/src/platform_impl/macos/progress_bar.rs b/vendor/tao/src/platform_impl/macos/progress_bar.rs new file mode 100644 index 0000000000..d500153615 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/progress_bar.rs @@ -0,0 +1,161 @@ +use std::{ffi::CStr, sync::Once}; + +use objc2::{ + msg_send, + rc::Retained, + runtime::{AnyClass as Class, AnyObject as Object, ClassBuilder as ClassDecl, Sel}, +}; +use objc2_foundation::{NSArray, NSInsetRect, NSPoint, NSRect, NSSize}; + +use super::ffi::{id, nil, NO}; +use crate::window::{ProgressBarState, ProgressState}; + +/// Set progress indicator in the Dock. +pub fn set_progress_indicator(progress_state: ProgressBarState) { + unsafe { + let ns_app: id = msg_send![class!(NSApplication), sharedApplication]; + let dock_tile: id = msg_send![ns_app, dockTile]; + if dock_tile == nil { + return; + } + + // check progress indicator is already set or create new one + let progress_indicator: id = get_exist_progress_indicator(dock_tile) + .unwrap_or_else(|| create_progress_indicator(ns_app, dock_tile)); + + // set progress indicator state + if let Some(progress) = progress_state.progress { + let progress = progress.clamp(0, 100) as f64; + let _: () = msg_send![progress_indicator, setDoubleValue: progress]; + let _: () = msg_send![progress_indicator, setHidden: NO]; + } + #[allow(deprecated)] // TODO: Use define_class! + if let Some(state) = progress_state.state { + *(*progress_indicator).get_mut_ivar("state") = state as u8; + let _: () = msg_send![ + progress_indicator, + setHidden: matches!(state, ProgressState::None) + ]; + } + + let _: () = msg_send![dock_tile, display]; + } +} + +fn create_progress_indicator(ns_app: id, dock_tile: id) -> id { + unsafe { + let mut image_view: id = msg_send![dock_tile, contentView]; + if image_view == nil { + // create new dock tile view with current app icon + let app_icon_image: id = msg_send![ns_app, applicationIconImage]; + image_view = msg_send![class!(NSImageView), imageViewWithImage: app_icon_image]; + let _: () = msg_send![dock_tile, setContentView: image_view]; + } + + // create custom progress indicator + let dock_tile_size: NSSize = msg_send![dock_tile, size]; + let frame = NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(dock_tile_size.width, 15.0), + ); + let progress_class = create_progress_indicator_class(); + let progress_indicator: id = msg_send![progress_class, alloc]; + let progress_indicator: id = msg_send![progress_indicator, initWithFrame: frame]; + let _: id = msg_send![progress_indicator, autorelease]; + + // set progress indicator to the dock tile + let _: () = msg_send![image_view, addSubview: progress_indicator]; + + progress_indicator + } +} + +fn get_exist_progress_indicator(dock_tile: id) -> Option { + unsafe { + let content_view: id = msg_send![dock_tile, contentView]; + if content_view == nil { + return None; + } + let subviews: Option> = msg_send![content_view, subviews]; + let subviews = subviews?; + + for idx in 0..subviews.count() { + let subview: id = msg_send![&subviews, objectAtIndex: idx]; + + let is_progress_indicator: bool = + msg_send![subview, isKindOfClass: class!(NSProgressIndicator)]; + if is_progress_indicator { + return Some(subview); + } + } + } + None +} + +fn create_progress_indicator_class() -> *const Class { + static mut APP_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + INIT.call_once(|| unsafe { + let superclass = class!(NSProgressIndicator); + let mut decl = ClassDecl::new( + CStr::from_bytes_with_nul(b"TaoProgressIndicator\0").unwrap(), + superclass, + ) + .unwrap(); + + decl.add_method(sel!(drawRect:), draw_progress_bar as extern "C" fn(_, _, _)); + + // progress bar states, follows ProgressState + decl.add_ivar::(CStr::from_bytes_with_nul(b"state\0").unwrap()); + + APP_CLASS = decl.register(); + }); + + unsafe { APP_CLASS } +} + +extern "C" fn draw_progress_bar(this: &Object, _: Sel, rect: NSRect) { + #[allow(deprecated)] // TODO: Use define_class! + unsafe { + let bar = NSRect::new( + NSPoint { x: 0.0, y: 4.0 }, + NSSize { + width: rect.size.width, + height: 8.0, + }, + ); + let bar_inner = NSInsetRect(bar, 0.5, 0.5); + let mut bar_progress = NSInsetRect(bar, 1.0, 1.0); + + // set progress width + let current_progress: f64 = msg_send![this, doubleValue]; + let normalized_progress: f64 = (current_progress / 100.0).clamp(0.0, 1.0); + bar_progress.size.width *= normalized_progress; + + // draw outer bar + let bg_color: id = msg_send![class!(NSColor), colorWithWhite:1.0 alpha:0.05]; + let _: () = msg_send![bg_color, set]; + draw_rounded_rect(bar); + // draw inner bar + draw_rounded_rect(bar_inner); + + // draw progress + let state: u8 = *(this.get_ivar("state")); + let progress_color: id = match state { + x if x == ProgressState::Paused as u8 => msg_send![class!(NSColor), systemYellowColor], + x if x == ProgressState::Error as u8 => msg_send![class!(NSColor), systemRedColor], + _ => msg_send![class!(NSColor), systemBlueColor], + }; + let _: () = msg_send![progress_color, set]; + draw_rounded_rect(bar_progress); + } +} + +fn draw_rounded_rect(rect: NSRect) { + unsafe { + let raduis = rect.size.height / 2.0; + let bezier_path: id = msg_send![class!(NSBezierPath), bezierPathWithRoundedRect:rect, xRadius:raduis, yRadius:raduis]; + let _: () = msg_send![bezier_path, fill]; + } +} diff --git a/vendor/tao/src/platform_impl/macos/util/async.rs b/vendor/tao/src/platform_impl/macos/util/async.rs new file mode 100644 index 0000000000..b7f6b4fa94 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/util/async.rs @@ -0,0 +1,257 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + ops::Deref, + sync::{Mutex, Weak}, +}; + +use core_graphics::base::CGFloat; +use dispatch::Queue; +use objc2::{rc::autoreleasepool, Message}; +use objc2_app_kit::{NSScreen, NSView, NSWindow, NSWindowStyleMask}; +use objc2_foundation::{MainThreadMarker, NSPoint, NSSize, NSString}; + +use crate::{ + dpi::LogicalSize, + platform_impl::platform::{ + ffi::{self, id, NO, YES}, + window::SharedState, + }, +}; + +pub fn is_main_thread() -> bool { + unsafe { msg_send!(class!(NSThread), isMainThread) } +} + +// Unsafe wrapper type that allows us to dispatch things that aren't Send. +// This should *only* be used to dispatch to the main queue. +// While it is indeed not guaranteed that these types can safely be sent to +// other threads, we know that they're safe to use on the main thread. +struct MainThreadSafe(T); + +unsafe impl Send for MainThreadSafe {} + +impl Deref for MainThreadSafe { + type Target = T; + fn deref(&self) -> &T { + &self.0 + } +} + +fn run_on_main(f: impl FnOnce() -> R + Send) -> R { + if is_main_thread() { + f() + } else { + Queue::main().exec_sync(f) + } +} + +unsafe fn set_style_mask(ns_window: &NSWindow, ns_view: &NSView, mask: NSWindowStyleMask) { + ns_window.setStyleMask(mask); + // If we don't do this, key handling will break + // (at least until the window is clicked again/etc.) + ns_window.makeFirstResponder(Some(ns_view)); +} + +// Always use this function instead of trying to modify `styleMask` directly! +// `setStyleMask:` isn't thread-safe, so we have to use Grand Central Dispatch. +// Otherwise, this would vomit out errors about not being on the main thread +// and fail to do anything. +pub unsafe fn set_style_mask_async( + ns_window: &NSWindow, + ns_view: &NSView, + mask: NSWindowStyleMask, +) { + let ns_window = MainThreadSafe(ns_window.retain()); + let ns_view = MainThreadSafe(ns_view.retain()); + Queue::main().exec_async(move || { + set_style_mask(&ns_window, &ns_view, mask); + }); +} +pub unsafe fn set_style_mask_sync(ns_window: &NSWindow, ns_view: &NSView, mask: NSWindowStyleMask) { + if is_main_thread() { + set_style_mask(ns_window, ns_view, mask); + } else { + let ns_window = MainThreadSafe(ns_window.retain()); + let ns_view = MainThreadSafe(ns_view.retain()); + Queue::main().exec_sync(move || { + set_style_mask(&ns_window, &ns_view, mask); + }) + } +} + +// `setContentSize:` isn't thread-safe either, though it doesn't log any errors +// and just fails silently. Anyway, GCD to the rescue! +pub unsafe fn set_content_size_async(ns_window: &NSWindow, size: LogicalSize) { + let ns_window = MainThreadSafe(ns_window.retain()); + Queue::main().exec_async(move || { + ns_window.setContentSize(NSSize::new(size.width as CGFloat, size.height as CGFloat)); + }); +} + +// `setFrameTopLeftPoint:` isn't thread-safe, but fortunately has the courtesy +// to log errors. +pub unsafe fn set_frame_top_left_point_async(ns_window: &NSWindow, point: NSPoint) { + let ns_window = MainThreadSafe(ns_window.retain()); + Queue::main().exec_async(move || { + ns_window.setFrameTopLeftPoint(point); + }); +} + +// `setFrameTopLeftPoint:` isn't thread-safe, and fails silently. +pub unsafe fn set_level_async(ns_window: &NSWindow, level: ffi::NSWindowLevel) { + let ns_window = MainThreadSafe(ns_window.retain()); + Queue::main().exec_async(move || { + ns_window.setLevel(level as _); + }); +} + +// `toggleFullScreen` is thread-safe, but our additional logic to account for +// window styles isn't. +pub unsafe fn toggle_full_screen_async( + ns_window: &NSWindow, + ns_view: &NSView, + not_fullscreen: bool, + shared_state: Weak>, +) { + let ns_window = MainThreadSafe(ns_window.retain()); + let ns_view = MainThreadSafe(ns_view.retain()); + let shared_state = MainThreadSafe(shared_state); + Queue::main().exec_async(move || { + // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we + // set a normal style temporarily. The previous state will be + // restored in `WindowDelegate::window_did_exit_fullscreen`. + if not_fullscreen { + let curr_mask = ns_window.styleMask(); + let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable; + if !curr_mask.contains(required) { + set_style_mask(&ns_window, &ns_view, required); + if let Some(shared_state) = shared_state.upgrade() { + trace!("Locked shared state in `toggle_full_screen_callback`"); + let mut shared_state_lock = shared_state.lock().unwrap(); + shared_state_lock.saved_style = Some(curr_mask); + trace!("Unlocked shared state in `toggle_full_screen_callback`"); + } + } + } + // Window level must be restored from `CGShieldingWindowLevel() + // + 1` back to normal in order for `toggleFullScreen` to do + // anything + ns_window.setLevel(0); + ns_window.toggleFullScreen(None); + }); +} + +pub unsafe fn restore_display_mode_async(ns_screen: u32) { + Queue::main().exec_async(move || { + ffi::CGRestorePermanentDisplayConfiguration(); + assert_eq!(ffi::CGDisplayRelease(ns_screen), ffi::kCGErrorSuccess); + }); +} + +// `setMaximized` is not thread-safe +pub unsafe fn set_maximized_async( + ns_window: &NSWindow, + is_zoomed: bool, + maximized: bool, + shared_state: Weak>, +) { + let ns_window = MainThreadSafe(ns_window.retain()); + let shared_state = MainThreadSafe(shared_state); + Queue::main().exec_async(move || { + if let Some(shared_state) = shared_state.upgrade() { + trace!("Locked shared state in `set_maximized`"); + let mut shared_state_lock = shared_state.lock().unwrap(); + + // Save the standard frame sized if it is not zoomed + if !is_zoomed { + shared_state_lock.standard_frame = Some(NSWindow::frame(&ns_window)); + } + + shared_state_lock.maximized = maximized; + + let curr_mask = ns_window.styleMask(); + if shared_state_lock.fullscreen.is_some() { + // Handle it in window_did_exit_fullscreen + return; + } else if curr_mask.contains(NSWindowStyleMask::Resizable) + && curr_mask.contains(NSWindowStyleMask::Titled) + { + // Just use the native zoom if resizable + ns_window.zoom(None); + } else { + // if it's not resizable, we set the frame directly + let new_rect = if maximized { + let mtm = MainThreadMarker::new_unchecked(); + let screen = NSScreen::mainScreen(mtm).unwrap(); + NSScreen::visibleFrame(&screen) + } else { + shared_state_lock.saved_standard_frame() + }; + let _: () = msg_send![&*ns_window, setFrame:new_rect, display:NO, animate: YES]; + } + + trace!("Unlocked shared state in `set_maximized`"); + } + }); +} + +// `orderOut:` isn't thread-safe. Calling it from another thread actually works, +// but with an odd delay. +pub unsafe fn order_out_sync(ns_window: &NSWindow) { + let ns_window = MainThreadSafe(ns_window.retain()); + run_on_main(move || { + ns_window.orderOut(None); + }); +} + +// `makeKeyAndOrderFront:` isn't thread-safe. Calling it from another thread +// actually works, but with an odd delay. +pub unsafe fn make_key_and_order_front_sync(ns_window: &NSWindow) { + let ns_window = MainThreadSafe(ns_window.retain()); + run_on_main(move || { + ns_window.makeKeyAndOrderFront(None); + }); +} + +// `setTitle:` isn't thread-safe. Calling it from another thread invalidates the +// window drag regions, which throws an exception when not done in the main +// thread +pub unsafe fn set_title_async(ns_window: &NSWindow, title: String) { + let ns_window = MainThreadSafe(ns_window.retain()); + Queue::main().exec_async(move || { + let title = NSString::from_str(&title); + ns_window.setTitle(&title); + }); +} + +// `setFocus:` isn't thread-safe. +pub unsafe fn set_focus(ns_window: &NSWindow) { + let ns_window = MainThreadSafe(ns_window.retain()); + run_on_main(move || { + ns_window.makeKeyAndOrderFront(None); + let app: id = msg_send![class!(NSApplication), sharedApplication]; + let () = msg_send![app, activateIgnoringOtherApps: YES]; + }); +} + +// `close:` is thread-safe, but we want the event to be triggered from the main +// thread. Though, it's a good idea to look into that more... +pub unsafe fn close_async(ns_window: &NSWindow) { + let ns_window = MainThreadSafe(ns_window.retain()); + run_on_main(move || { + autoreleasepool(move |_| { + ns_window.close(); + }); + }); +} + +// `setIgnoresMouseEvents_:` isn't thread-safe, and fails silently. +pub unsafe fn set_ignore_mouse_events(ns_window: &NSWindow, ignore: bool) { + let ns_window = MainThreadSafe(ns_window.retain()); + Queue::main().exec_async(move || { + ns_window.setIgnoresMouseEvents(ignore); + }); +} diff --git a/vendor/tao/src/platform_impl/macos/util/cursor.rs b/vendor/tao/src/platform_impl/macos/util/cursor.rs new file mode 100644 index 0000000000..fcc38becca --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/util/cursor.rs @@ -0,0 +1,177 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use crate::platform_impl::platform::ffi::{id, nil, NO}; +use objc2::{ + msg_send, + rc::Retained, + runtime::{AnyObject, Sel}, + AllocAnyThread, +}; +use objc2_app_kit::NSImage; +use objc2_foundation::{NSDictionary, NSPoint, NSString}; +use std::{ + cell::RefCell, + ffi::{c_void, CString}, + ptr::null_mut, +}; + +use crate::window::CursorIcon; + +#[derive(Default)] +pub enum Cursor { + #[default] + Default, + Native(&'static str), + Undocumented(&'static str), + WebKit(&'static str), +} + +impl From for Cursor { + fn from(cursor: CursorIcon) -> Self { + // See native cursors at https://developer.apple.com/documentation/appkit/nscursor?language=objc. + match cursor { + CursorIcon::Default => Cursor::Default, + CursorIcon::Arrow => Cursor::Native("arrowCursor"), + CursorIcon::Hand => Cursor::Native("pointingHandCursor"), + CursorIcon::Grab => Cursor::Native("openHandCursor"), + CursorIcon::Grabbing => Cursor::Native("closedHandCursor"), + CursorIcon::Text => Cursor::Native("IBeamCursor"), + CursorIcon::VerticalText => Cursor::Native("IBeamCursorForVerticalLayout"), + CursorIcon::Copy => Cursor::Native("dragCopyCursor"), + CursorIcon::Alias => Cursor::Native("dragLinkCursor"), + CursorIcon::NotAllowed | CursorIcon::NoDrop => Cursor::Native("operationNotAllowedCursor"), + CursorIcon::ContextMenu => Cursor::Native("contextualMenuCursor"), + CursorIcon::Crosshair => Cursor::Native("crosshairCursor"), + CursorIcon::EResize => Cursor::Native("resizeRightCursor"), + CursorIcon::NResize => Cursor::Native("resizeUpCursor"), + CursorIcon::WResize => Cursor::Native("resizeLeftCursor"), + CursorIcon::SResize => Cursor::Native("resizeDownCursor"), + CursorIcon::EwResize | CursorIcon::ColResize => Cursor::Native("resizeLeftRightCursor"), + CursorIcon::NsResize | CursorIcon::RowResize => Cursor::Native("resizeUpDownCursor"), + + // Undocumented cursors: https://stackoverflow.com/a/46635398/5435443 + CursorIcon::Help => Cursor::Undocumented("_helpCursor"), + CursorIcon::ZoomIn => Cursor::Undocumented("_zoomInCursor"), + CursorIcon::ZoomOut => Cursor::Undocumented("_zoomOutCursor"), + CursorIcon::NeResize => Cursor::Undocumented("_windowResizeNorthEastCursor"), + CursorIcon::NwResize => Cursor::Undocumented("_windowResizeNorthWestCursor"), + CursorIcon::SeResize => Cursor::Undocumented("_windowResizeSouthEastCursor"), + CursorIcon::SwResize => Cursor::Undocumented("_windowResizeSouthWestCursor"), + CursorIcon::NeswResize => Cursor::Undocumented("_windowResizeNorthEastSouthWestCursor"), + CursorIcon::NwseResize => Cursor::Undocumented("_windowResizeNorthWestSouthEastCursor"), + + // While these are available, the former just loads a white arrow, + // and the latter loads an ugly deflated beachball! + // CursorIcon::Move => Cursor::Undocumented("_moveCursor"), + // CursorIcon::Wait => Cursor::Undocumented("_waitCursor"), + + // An even more undocumented cursor... + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=522349 + // This is the wrong semantics for `Wait`, but it's the same as + // what's used in Safari and Chrome. + CursorIcon::Wait | CursorIcon::Progress => Cursor::Undocumented("busyButClickableCursor"), + + // For the rest, we can just snatch the cursors from WebKit... + // They fit the style of the native cursors, and will seem + // completely standard to macOS users. + // https://stackoverflow.com/a/21786835/5435443 + CursorIcon::Move | CursorIcon::AllScroll => Cursor::WebKit("move"), + CursorIcon::Cell => Cursor::WebKit("cell"), + } + } +} + +impl Cursor { + pub unsafe fn load(&self) -> id { + match self { + Cursor::Default => null_mut(), + Cursor::Native(cursor_name) => { + let sel = Sel::register(&CString::new(*cursor_name).unwrap()); + msg_send![class!(NSCursor), performSelector: sel] + } + Cursor::Undocumented(cursor_name) => { + let class = class!(NSCursor); + let sel = Sel::register(&CString::new(*cursor_name).unwrap()); + let sel = if msg_send![class, respondsToSelector: sel] { + sel + } else { + warn!("Cursor `{}` appears to be invalid", cursor_name); + sel!(arrowCursor) + }; + msg_send![class, performSelector: sel] + } + Cursor::WebKit(cursor_name) => load_webkit_cursor(cursor_name), + } + } +} + +// Note that loading `busybutclickable` with this code won't animate the frames; +// instead you'll just get them all in a column. +pub unsafe fn load_webkit_cursor(cursor_name: &str) -> id { + const CURSOR_ROOT: &str = "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors"; + let cursor_root = NSString::from_str(CURSOR_ROOT); + let cursor_name = NSString::from_str(cursor_name); + let cursor_pdf = NSString::from_str("cursor.pdf"); + let cursor_plist = NSString::from_str("info.plist"); + let key_x = NSString::from_str("hotx"); + let key_y = NSString::from_str("hoty"); + + let cursor_path: Retained = + msg_send![&cursor_root, stringByAppendingPathComponent: &*cursor_name]; + let pdf_path: Retained = + msg_send![&cursor_path, stringByAppendingPathComponent: &*cursor_pdf]; + let info_path: Retained = + msg_send![&cursor_path, stringByAppendingPathComponent: &*cursor_plist]; + + let image = NSImage::initByReferencingFile(NSImage::alloc(), &pdf_path).unwrap(); + #[allow(deprecated)] + let info = + NSDictionary::::dictionaryWithContentsOfFile(&info_path).unwrap(); + let x = info.objectForKey(&key_x).unwrap(); + let y = info.objectForKey(&key_y).unwrap(); + let point = NSPoint::new(msg_send![&x, doubleValue], msg_send![&y, doubleValue]); + let cursor: id = msg_send![class!(NSCursor), alloc]; + msg_send![ + cursor, + initWithImage:&*image, + hotSpot:point, + ] +} + +pub unsafe fn invisible_cursor() -> id { + // 16x16 GIF data for invisible cursor + // You can reproduce this via ImageMagick. + // $ convert -size 16x16 xc:none cursor.gif + static CURSOR_BYTES: &[u8] = &[ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x10, 0x00, 0x10, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x10, 0x00, 0x00, 0x02, 0x0E, 0x84, 0x8F, 0xA9, 0xCB, 0xED, 0x0F, 0xA3, 0x9C, 0xB4, + 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B, + ]; + + thread_local! { + // We can't initialize this at startup. + static CURSOR_OBJECT: RefCell = const { RefCell::new(nil) }; + } + + CURSOR_OBJECT.with(|cursor_obj| { + if *cursor_obj.borrow() == nil { + // Create a cursor from `CURSOR_BYTES` + let cursor_data: id = msg_send![ + class!(NSData), + dataWithBytesNoCopy:CURSOR_BYTES.as_ptr().cast::(), + length:CURSOR_BYTES.len(), + freeWhenDone:NO, + ]; + + let ns_image: id = msg_send![class!(NSImage), alloc]; + let _: id = msg_send![ns_image, initWithData: cursor_data]; + let cursor: id = msg_send![class!(NSCursor), alloc]; + *cursor_obj.borrow_mut() = + msg_send![cursor, initWithImage:ns_image, hotSpot: NSPoint::new(0.0, 0.0)]; + } + *cursor_obj.borrow() + }) +} diff --git a/vendor/tao/src/platform_impl/macos/util/mod.rs b/vendor/tao/src/platform_impl/macos/util/mod.rs new file mode 100644 index 0000000000..b9a39147e6 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/util/mod.rs @@ -0,0 +1,145 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +mod r#async; +mod cursor; + +pub use self::{cursor::*, r#async::*}; + +use std::ops::{BitAnd, Deref}; + +use core_graphics::display::CGDisplay; +use objc2::{ + class, + runtime::{AnyClass as Class, AnyObject as Object, Sel}, +}; +use objc2_app_kit::{NSApp, NSView, NSWindow, NSWindowStyleMask}; +use objc2_foundation::{MainThreadMarker, NSAutoreleasePool, NSPoint, NSRange, NSRect, NSUInteger}; + +use crate::{ + dpi::{LogicalPosition, PhysicalPosition}, + error::ExternalError, + platform_impl::platform::ffi::{self, id, nil, BOOL, YES}, +}; + +// Replace with `!` once stable +#[derive(Debug)] +pub enum Never {} + +pub fn has_flag(bitset: T, flag: T) -> bool +where + T: Copy + PartialEq + BitAnd, +{ + bitset & flag == flag +} + +pub const EMPTY_RANGE: NSRange = NSRange { + location: ffi::NSNotFound as NSUInteger, + length: 0, +}; + +#[derive(Debug, PartialEq)] +pub struct IdRef(id); + +impl IdRef { + pub fn new(inner: id) -> IdRef { + IdRef(inner) + } + + #[allow(dead_code)] + pub fn retain(inner: id) -> IdRef { + if inner != nil { + let _: id = unsafe { msg_send![inner, retain] }; + } + IdRef(inner) + } +} + +impl Drop for IdRef { + fn drop(&mut self) { + if self.0 != nil { + unsafe { + let _pool = NSAutoreleasePool::new(); + let () = msg_send![self.0, release]; + }; + } + } +} + +impl Deref for IdRef { + type Target = id; + #[allow(clippy::needless_lifetimes)] + fn deref<'a>(&'a self) -> &'a id { + &self.0 + } +} + +impl Clone for IdRef { + fn clone(&self) -> IdRef { + IdRef::retain(self.0) + } +} + +// For consistency with other platforms, this will... +// 1. translate the bottom-left window corner into the top-left window corner +// 2. translate the coordinate from a bottom-left origin coordinate system to a top-left one +pub fn bottom_left_to_top_left(rect: NSRect) -> f64 { + CGDisplay::main().pixels_high() as f64 - (rect.origin.y + rect.size.height) +} + +/// Converts from tao screen-coordinates to macOS screen-coordinates. +/// Tao: top-left is (0, 0) and y increasing downwards +/// macOS: bottom-left is (0, 0) and y increasing upwards +pub fn window_position(position: LogicalPosition) -> NSPoint { + NSPoint::new( + position.x, + CGDisplay::main().pixels_high() as f64 - position.y, + ) +} + +pub fn cursor_position() -> Result, ExternalError> { + let point: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] }; + let y = CGDisplay::main().pixels_high() as f64 - point.y; + let point = LogicalPosition::new(point.x, y); + Ok(point.to_physical(super::monitor::primary_monitor().scale_factor())) +} + +pub unsafe fn superclass<'a>(this: &'a Object) -> &'a Class { + let superclass: *const Class = msg_send![this, superclass]; + &*superclass +} + +pub unsafe fn create_input_context(view: &NSView) -> IdRef { + let input_context: id = msg_send![class!(NSTextInputContext), alloc]; + let input_context: id = msg_send![input_context, initWithClient: view]; + IdRef::new(input_context) +} + +#[allow(dead_code)] +pub unsafe fn open_emoji_picker() { + // SAFETY: TODO + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + let () = msg_send![&NSApp(mtm), orderFrontCharacterPalette: nil]; +} + +pub extern "C" fn yes(_: &Object, _: Sel) -> BOOL { + YES +} + +pub unsafe fn toggle_style_mask( + window: &NSWindow, + view: &NSView, + mask: NSWindowStyleMask, + on: bool, +) { + let current_style_mask = window.styleMask(); + if on { + window.setStyleMask(current_style_mask | mask); + } else { + window.setStyleMask(current_style_mask & (!mask)); + } + + // If we don't do this, key handling will break. Therefore, never call `setStyleMask` directly! + window.makeFirstResponder(Some(view)); +} diff --git a/vendor/tao/src/platform_impl/macos/view.rs b/vendor/tao/src/platform_impl/macos/view.rs new file mode 100644 index 0000000000..981d6fd807 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/view.rs @@ -0,0 +1,1184 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +#![allow(unused_unsafe)] +#![allow(deprecated)] // TODO: Use define_class! + +use std::{ + boxed::Box, + collections::{HashSet, VecDeque}, + ffi::CStr, + os::raw::*, + ptr, + sync::{Arc, Mutex, Weak}, +}; + +use objc2::{ + msg_send, + rc::Retained, + runtime::{ + AnyClass as Class, AnyObject as Object, AnyProtocol as Protocol, ClassBuilder as ClassDecl, Sel, + }, + AllocAnyThread, +}; +use objc2_app_kit::{ + NSApp, NSEvent, NSEventModifierFlags, NSEventPhase, NSView, NSWindow, NSWindowButton, +}; +use objc2_foundation::{ + MainThreadMarker, NSAttributedString, NSInteger, NSMutableAttributedString, NSPoint, NSRange, + NSRect, NSSize, NSString, NSUInteger, +}; + +use crate::{ + dpi::LogicalPosition, + event::{ + DeviceEvent, ElementState, Event, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent, + }, + keyboard::{KeyCode, ModifiersState}, + platform_impl::platform::{ + app_state::AppState, + event::{code_to_key, create_key_event, event_mods, get_scancode, EventWrapper}, + ffi::*, + util::{self}, + window::get_window_id, + DEVICE_ID, + }, + window::WindowId, +}; + +pub struct CursorState { + pub visible: bool, + pub cursor: util::Cursor, +} + +impl Default for CursorState { + fn default() -> Self { + Self { + visible: true, + cursor: Default::default(), + } + } +} + +pub(super) struct ViewState { + ns_window: objc2::rc::Weak, + pub cursor_state: Arc>, + ime_spot: Option<(f64, f64)>, + + /// This is true when we are currently modifying a marked text + /// using ime. When the text gets commited, this is set to false. + in_ime_preedit: bool, + + /// This is used to detect if a key-press causes an ime event. + /// If a key-press does not cause an ime event, that means + /// that the key-press cancelled the ime session. (Except arrow keys) + key_triggered_ime: bool, + // Not Needed Anymore + //raw_characters: Option, + is_key_down: bool, + pub(super) modifiers: ModifiersState, + phys_modifiers: HashSet, + tracking_rect: Option, + pub(super) traffic_light_inset: Option>, +} + +impl ViewState { + fn get_scale_factor(&self) -> f64 { + (unsafe { NSWindow::backingScaleFactor(&self.ns_window.load().unwrap()) }) as f64 + } +} + +pub fn new_view(ns_window: &NSWindow) -> (Option>, Weak>) { + let cursor_state = Default::default(); + let cursor_access = Arc::downgrade(&cursor_state); + let state = ViewState { + ns_window: objc2::rc::Weak::from(ns_window), + cursor_state, + ime_spot: None, + in_ime_preedit: false, + key_triggered_ime: false, + is_key_down: false, + modifiers: Default::default(), + phys_modifiers: Default::default(), + tracking_rect: None, + traffic_light_inset: None, + }; + unsafe { + // This is free'd in `dealloc` + let state_ptr = Box::into_raw(Box::new(state)) as *mut c_void; + let ns_view = msg_send![VIEW_CLASS.0, alloc]; + (msg_send![ns_view, initWithTao: state_ptr], cursor_access) + } +} + +pub unsafe fn set_ime_position(ns_view: &NSView, input_context: id, x: f64, y: f64) { + let state_ptr: *mut c_void = *ns_view.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + let content_rect = NSWindow::contentRectForFrameRect( + &state.ns_window.load().unwrap(), + NSWindow::frame(&state.ns_window.load().unwrap()), + ); + let base_x = content_rect.origin.x as f64; + let base_y = (content_rect.origin.y + content_rect.size.height) as f64; + state.ime_spot = Some((base_x + x, base_y - y)); + let _: () = msg_send![input_context, invalidateCharacterCoordinates]; +} + +fn is_arrow_key(keycode: KeyCode) -> bool { + matches!( + keycode, + KeyCode::ArrowUp | KeyCode::ArrowDown | KeyCode::ArrowLeft | KeyCode::ArrowRight + ) +} + +struct ViewClass(&'static Class); +unsafe impl Send for ViewClass {} +unsafe impl Sync for ViewClass {} + +lazy_static! { + static ref VIEW_CLASS: ViewClass = unsafe { + let superclass = class!(NSView); + let mut decl = + ClassDecl::new(CStr::from_bytes_with_nul(b"TaoView\0").unwrap(), superclass).unwrap(); + decl.add_method(sel!(dealloc), dealloc as extern "C" fn(_, _)); + decl.add_method( + sel!(initWithTao:), + init_with_tao as extern "C" fn(_, _, _) -> _, + ); + decl.add_method( + sel!(viewDidMoveToWindow), + view_did_move_to_window as extern "C" fn(_, _), + ); + decl.add_method(sel!(drawRect:), draw_rect as extern "C" fn(_, _, _)); + decl.add_method( + sel!(acceptsFirstResponder), + accepts_first_responder as extern "C" fn(_, _) -> _, + ); + decl.add_method(sel!(touchBar), touch_bar as extern "C" fn(_, _) -> _); + decl.add_method( + sel!(resetCursorRects), + reset_cursor_rects as extern "C" fn(_, _), + ); + decl.add_method( + sel!(hasMarkedText), + has_marked_text as extern "C" fn(_, _) -> _, + ); + decl.add_method(sel!(markedRange), marked_range as extern "C" fn(_, _) -> _); + decl.add_method( + sel!(selectedRange), + selected_range as extern "C" fn(_, _) -> _, + ); + decl.add_method( + sel!(setMarkedText:selectedRange:replacementRange:), + set_marked_text as extern "C" fn(_, _, _, _, _), + ); + decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(_, _)); + decl.add_method( + sel!(validAttributesForMarkedText), + valid_attributes_for_marked_text as extern "C" fn(_, _) -> _, + ); + decl.add_method( + sel!(attributedSubstringForProposedRange:actualRange:), + attributed_substring_for_proposed_range as extern "C" fn(_, _, _, _) -> _, + ); + decl.add_method( + sel!(insertText:replacementRange:), + insert_text as extern "C" fn(_, _, _, _), + ); + decl.add_method( + sel!(characterIndexForPoint:), + character_index_for_point as extern "C" fn(_, _, _) -> _, + ); + decl.add_method( + sel!(firstRectForCharacterRange:actualRange:), + first_rect_for_character_range as extern "C" fn(_, _, _, _) -> _, + ); + decl.add_method( + sel!(doCommandBySelector:), + do_command_by_selector as extern "C" fn(_, _, _), + ); + decl.add_method(sel!(keyDown:), key_down as extern "C" fn(_, _, _)); + decl.add_method(sel!(keyUp:), key_up as extern "C" fn(_, _, _)); + decl.add_method(sel!(flagsChanged:), flags_changed as extern "C" fn(_, _, _)); + decl.add_method(sel!(insertTab:), insert_tab as extern "C" fn(_, _, _)); + decl.add_method( + sel!(insertBackTab:), + insert_back_tab as extern "C" fn(_, _, _), + ); + decl.add_method(sel!(mouseDown:), mouse_down as extern "C" fn(_, _, _)); + decl.add_method(sel!(mouseUp:), mouse_up as extern "C" fn(_, _, _)); + decl.add_method( + sel!(rightMouseDown:), + right_mouse_down as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(rightMouseUp:), + right_mouse_up as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(otherMouseDown:), + other_mouse_down as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(otherMouseUp:), + other_mouse_up as extern "C" fn(_, _, _), + ); + decl.add_method(sel!(mouseMoved:), mouse_moved as extern "C" fn(_, _, _)); + decl.add_method(sel!(mouseDragged:), mouse_dragged as extern "C" fn(_, _, _)); + decl.add_method( + sel!(rightMouseDragged:), + right_mouse_dragged as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(otherMouseDragged:), + other_mouse_dragged as extern "C" fn(_, _, _), + ); + decl.add_method(sel!(mouseEntered:), mouse_entered as extern "C" fn(_, _, _)); + decl.add_method(sel!(mouseExited:), mouse_exited as extern "C" fn(_, _, _)); + decl.add_method(sel!(scrollWheel:), scroll_wheel as extern "C" fn(_, _, _)); + decl.add_method( + sel!(pressureChangeWithEvent:), + pressure_change_with_event as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(_wantsKeyDownForEvent:), + wants_key_down_for_event as extern "C" fn(_, _, _) -> _, + ); + decl.add_method( + sel!(cancelOperation:), + cancel_operation as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(frameDidChange:), + frame_did_change as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(acceptsFirstMouse:), + accepts_first_mouse as extern "C" fn(_, _, _) -> _, + ); + decl.add_ivar::<*mut c_void>(CStr::from_bytes_with_nul(b"taoState\0").unwrap()); + decl.add_ivar::(CStr::from_bytes_with_nul(b"markedText\0").unwrap()); + let protocol = + Protocol::get(CStr::from_bytes_with_nul(b"NSTextInputClient\0").unwrap()).unwrap(); + decl.add_protocol(protocol); + ViewClass(decl.register()) + }; +} + +extern "C" fn dealloc(this: &Object, _sel: Sel) { + unsafe { + let state: *mut c_void = *this.get_ivar("taoState"); + let marked_text: *mut NSMutableAttributedString = *this.get_ivar("markedText"); + let _: () = msg_send![marked_text, release]; + drop(Box::from_raw(state as *mut ViewState)); + } +} + +extern "C" fn init_with_tao(this: &Object, _sel: Sel, state: *mut c_void) -> id { + unsafe { + let this: id = msg_send![this, init]; + if this != nil { + *(*this).get_mut_ivar("taoState") = state; + let marked_text = Retained::into_raw(NSMutableAttributedString::new()); + *(*this).get_mut_ivar("markedText") = marked_text; + let _: () = msg_send![this, setPostsFrameChangedNotifications: YES]; + + let notification_center: &Object = msg_send![class!(NSNotificationCenter), defaultCenter]; + let notification_name = NSString::from_str("NSViewFrameDidChangeNotification"); + let _: () = msg_send![ + notification_center, + addObserver: this + selector: sel!(frameDidChange:) + name: &*notification_name + object: this + ]; + } + this + } +} + +extern "C" fn view_did_move_to_window(this: &Object, _sel: Sel) { + trace!("Triggered `viewDidMoveToWindow`"); + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + if let Some(tracking_rect) = state.tracking_rect.take() { + let _: () = msg_send![this, removeTrackingRect: tracking_rect]; + } + + let rect: NSRect = msg_send![this, visibleRect]; + let tracking_rect: NSInteger = msg_send![this, + addTrackingRect:rect + owner:this + userData:ptr::null_mut::() + assumeInside:NO + ]; + state.tracking_rect = Some(tracking_rect); + } + trace!("Completed `viewDidMoveToWindow`"); +} + +extern "C" fn frame_did_change(this: &Object, _sel: Sel, _event: id) { + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + if let Some(tracking_rect) = state.tracking_rect.take() { + let _: () = msg_send![this, removeTrackingRect: tracking_rect]; + } + + let rect: NSRect = msg_send![this, visibleRect]; + let tracking_rect: NSInteger = msg_send![this, + addTrackingRect:rect + owner:this + userData:ptr::null_mut::() + assumeInside:NO + ]; + + state.tracking_rect = Some(tracking_rect); + } +} + +extern "C" fn draw_rect(this: &Object, _sel: Sel, rect: NSRect) { + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + if let Some(position) = state.traffic_light_inset { + let window = state.ns_window.load().unwrap(); + inset_traffic_lights(&window, position); + } + + AppState::handle_redraw(WindowId(get_window_id(&state.ns_window.load().unwrap()))); + + let superclass = util::superclass(this); + let () = msg_send![super(this, superclass), drawRect: rect]; + } +} + +extern "C" fn accepts_first_responder(_this: &Object, _sel: Sel) -> BOOL { + YES +} + +// This is necessary to prevent a beefy terminal error on MacBook Pros: +// IMKInputSession [0x7fc573576ff0 presentFunctionRowItemTextInputViewWithEndpoint:completionHandler:] : [self textInputContext]=0x7fc573558e10 *NO* NSRemoteViewController to client, NSError=Error Domain=NSCocoaErrorDomain Code=4099 "The connection from pid 0 was invalidated from this process." UserInfo={NSDebugDescription=The connection from pid 0 was invalidated from this process.}, com.apple.inputmethod.EmojiFunctionRowItem +// TODO: Add an API extension for using `NSTouchBar` +extern "C" fn touch_bar(_this: &Object, _sel: Sel) -> id { + nil +} + +extern "C" fn reset_cursor_rects(this: &Object, _sel: Sel) { + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + let bounds: NSRect = msg_send![this, bounds]; + let cursor_state = state.cursor_state.lock().unwrap(); + let cursor = if cursor_state.visible { + cursor_state.cursor.load() + } else { + util::invisible_cursor() + }; + + if !cursor.is_null() { + let _: () = msg_send![this, + addCursorRect:bounds + cursor:cursor + ]; + } + } +} + +extern "C" fn has_marked_text(this: &Object, _sel: Sel) -> BOOL { + unsafe { + trace!("Triggered `hasMarkedText`"); + let marked_text: &NSMutableAttributedString = *this.get_ivar("markedText"); + trace!("Completed `hasMarkedText`"); + (marked_text.length() > 0).into() + } +} + +extern "C" fn marked_range(this: &Object, _sel: Sel) -> NSRange { + unsafe { + trace!("Triggered `markedRange`"); + let marked_text: &NSMutableAttributedString = *this.get_ivar("markedText"); + let length = marked_text.length(); + trace!("Completed `markedRange`"); + if length > 0 { + NSRange::new(0, length - 1) + } else { + util::EMPTY_RANGE + } + } +} + +extern "C" fn selected_range(_this: &Object, _sel: Sel) -> NSRange { + trace!("Triggered `selectedRange`"); + trace!("Completed `selectedRange`"); + util::EMPTY_RANGE +} + +/// An IME pre-edit operation happened, changing the text that's +/// currently being pre-edited. This more or less corresponds to +/// the `compositionupdate` event on the web. +extern "C" fn set_marked_text( + this: &mut Object, + _sel: Sel, + string: id, + _selected_range: NSRange, + _replacement_range: NSRange, +) { + trace!("Triggered `setMarkedText`"); + unsafe { + let has_attr: bool = msg_send![string, isKindOfClass: class!(NSAttributedString)]; + let marked_text = if has_attr { + NSMutableAttributedString::initWithAttributedString( + NSMutableAttributedString::alloc(), + &*(string as *const NSAttributedString), + ) + } else { + NSMutableAttributedString::initWithString( + NSMutableAttributedString::alloc(), + &*(string as *const NSString), + ) + }; + let marked_text_ref: &mut *mut NSMutableAttributedString = this.get_mut_ivar("markedText"); + let () = msg_send![(*marked_text_ref), release]; + *marked_text_ref = Retained::into_raw(marked_text); + + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + state.in_ime_preedit = true; + state.key_triggered_ime = true; + } + trace!("Completed `setMarkedText`"); +} + +extern "C" fn unmark_text(this: &mut Object, _sel: Sel) { + trace!("Triggered `unmarkText`"); + unsafe { + let marked_text_ref: &mut *mut NSMutableAttributedString = this.get_mut_ivar("markedText"); + let () = msg_send![(*marked_text_ref), release]; + *marked_text_ref = Retained::into_raw(NSMutableAttributedString::new()); + let input_context: id = msg_send![this, inputContext]; + let _: () = msg_send![input_context, discardMarkedText]; + } + trace!("Completed `unmarkText`"); +} + +extern "C" fn valid_attributes_for_marked_text(_this: &Object, _sel: Sel) -> id { + trace!("Triggered `validAttributesForMarkedText`"); + trace!("Completed `validAttributesForMarkedText`"); + unsafe { msg_send![class!(NSArray), array] } +} + +extern "C" fn attributed_substring_for_proposed_range( + _this: &Object, + _sel: Sel, + _range: NSRange, + _actual_range: *mut c_void, // *mut NSRange +) -> id { + trace!("Triggered `attributedSubstringForProposedRange`"); + trace!("Completed `attributedSubstringForProposedRange`"); + nil +} + +extern "C" fn character_index_for_point(_this: &Object, _sel: Sel, _point: NSPoint) -> NSUInteger { + trace!("Triggered `characterIndexForPoint`"); + trace!("Completed `characterIndexForPoint`"); + 0 +} + +extern "C" fn first_rect_for_character_range( + this: &Object, + _sel: Sel, + _range: NSRange, + _actual_range: *mut c_void, // *mut NSRange +) -> NSRect { + unsafe { + trace!("Triggered `firstRectForCharacterRange`"); + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + let (x, y) = state.ime_spot.unwrap_or_else(|| { + let content_rect = NSWindow::contentRectForFrameRect( + &state.ns_window.load().unwrap(), + NSWindow::frame(&state.ns_window.load().unwrap()), + ); + let x = content_rect.origin.x; + let y = util::bottom_left_to_top_left(content_rect); + (x, y) + }); + trace!("Completed `firstRectForCharacterRange`"); + NSRect::new(NSPoint::new(x as _, y as _), NSSize::new(0.0, 0.0)) + } +} + +extern "C" fn insert_text( + this: &Object, + _sel: Sel, + string: &NSString, + _replacement_range: NSRange, +) { + trace!("Triggered `insertText`"); + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + let has_attr: bool = msg_send![string, isKindOfClass: class!(NSAttributedString)]; + let characters = if has_attr { + // This is a *mut NSAttributedString + msg_send![string, string] + } else { + // This is already a *mut NSString + string + }; + + let string: String = characters + .to_string() + .chars() + .filter(|c| !is_corporate_character(*c)) + .collect(); + state.is_key_down = true; + + // We don't need this now, but it's here if that changes. + //let event: id = msg_send![NSApp(), currentEvent]; + + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + event: WindowEvent::ReceivedImeText(string), + })); + if state.in_ime_preedit { + state.in_ime_preedit = false; + state.key_triggered_ime = true; + } + } + trace!("Completed `insertText`"); +} + +extern "C" fn do_command_by_selector(_this: &Object, _sel: Sel, _command: Sel) { + trace!("Triggered `doCommandBySelector`"); + // TODO: (Artur) all these inputs seem to trigger a key event with the correct text + // content so this is not needed anymore, it seems. + + // Basically, we're sent this message whenever a keyboard event that doesn't generate a "human readable" character + // happens, i.e. newlines, tabs, and Ctrl+C. + + // unsafe { + // let state_ptr: *mut c_void = *this.get_ivar("taoState"); + // let state = &mut *(state_ptr as *mut ViewState); + + // let mut events = VecDeque::with_capacity(1); + // if command == sel!(insertNewline:) { + // // The `else` condition would emit the same character, but I'm keeping this here both... + // // 1) as a reminder for how `doCommandBySelector` works + // // 2) to make our use of carriage return explicit + // events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { + // window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + // event: WindowEvent::ReceivedCharacter('\r'), + // })); + // } else { + // let raw_characters = state.raw_characters.take(); + // if let Some(raw_characters) = raw_characters { + // for character in raw_characters + // .chars() + // .filter(|c| !is_corporate_character(*c)) + // { + // events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { + // window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + // event: WindowEvent::ReceivedCharacter(character), + // })); + // } + // } + // }; + // AppState::queue_events(events); + // } + + trace!("Completed `doCommandBySelector`"); +} + +// Get characters +// fn get_characters(event: id, ignore_modifiers: bool) -> String { +// unsafe { +// let characters: id = if ignore_modifiers { +// msg_send![event, charactersIgnoringModifiers] +// } else { +// msg_send![event, characters] +// }; + +// assert_ne!(characters, nil); +// let slice = slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len()); + +// let string = str::from_utf8_unchecked(slice); +// string.to_owned() +// } +// } + +// As defined in: https://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/CORPCHAR.TXT +fn is_corporate_character(c: char) -> bool { + #[allow(clippy::match_like_matches_macro)] + match c { + '\u{F700}'..='\u{F747}' + | '\u{F802}'..='\u{F84F}' + | '\u{F850}' + | '\u{F85C}' + | '\u{F85D}' + | '\u{F85F}' + | '\u{F860}'..='\u{F86B}' + | '\u{F870}'..='\u{F8FF}' => true, + _ => false, + } +} + +// // Retrieves a layout-independent keycode given an event. +// fn retrieve_keycode(event: id) -> Option { +// #[inline] +// fn get_code(ev: id, raw: bool) -> Option { +// let characters = get_characters(ev, raw); +// characters.chars().next().and_then(|c| char_to_keycode(c)) +// } + +// // Cmd switches Roman letters for Dvorak-QWERTY layout, so we try modified characters first. +// // If we don't get a match, then we fall back to unmodified characters. +// let code = get_code(event, false).or_else(|| get_code(event, true)); + +// // We've checked all layout related keys, so fall through to scancode. +// // Reaching this code means that the key is layout-independent (e.g. Backspace, Return). +// // +// // We're additionally checking here for F21-F24 keys, since their keycode +// // can vary, but we know that they are encoded +// // in characters property. +// code.or_else(|| { +// let scancode = get_scancode(event); +// scancode_to_keycode(scancode).or_else(|| check_function_keys(&get_characters(event, true))) +// }) +// } + +// Update `state.modifiers` if `event` has something different +fn update_potentially_stale_modifiers(state: &mut ViewState, event: &NSEvent) { + let event_modifiers = event_mods(event); + if state.modifiers != event_modifiers { + state.modifiers = event_modifiers; + + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + event: WindowEvent::ModifiersChanged(state.modifiers), + })); + } +} + +extern "C" fn key_down(this: &mut Object, _sel: Sel, event: &NSEvent) { + trace!("Triggered `keyDown`"); + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + let window_id = WindowId(get_window_id(&state.ns_window.load().unwrap())); + + // keyboard refactor: don't seems to be needed anymore + // let characters = get_characters(event, false); + //state.raw_characters = Some(characters); + + let is_repeat: bool = msg_send![event, isARepeat]; + + update_potentially_stale_modifiers(state, event); + + let pass_along = !is_repeat || !state.is_key_down; + if pass_along { + // See below for why we do this. + let marked_text_ref: &mut *mut NSMutableAttributedString = this.get_mut_ivar("markedText"); + let () = msg_send![(*marked_text_ref), release]; + *marked_text_ref = Retained::into_raw(NSMutableAttributedString::new()); + state.key_triggered_ime = false; + + // Some keys (and only *some*, with no known reason) don't trigger `insertText`, while others do... + // So, we don't give repeats the opportunity to trigger that, since otherwise our hack will cause some + // keys to generate twice as many characters. + let array: id = msg_send![class!(NSArray), arrayWithObject: event]; + let () = msg_send![&*this, interpretKeyEvents: array]; + } + // The `interpretKeyEvents` above, may invoke `set_marked_text` or `insert_text`, + // if the event corresponds to an IME event. + let in_ime = state.key_triggered_ime; + let key_event = create_key_event(event, true, is_repeat, in_ime, None); + let is_arrow_key = is_arrow_key(key_event.physical_key); + if pass_along { + // The `interpretKeyEvents` above, may invoke `set_marked_text` or `insert_text`, + // if the event corresponds to an IME event. + // If `set_marked_text` or `insert_text` were not invoked, then the IME was deactivated, + // and we should cancel the IME session. + // When using arrow keys in an IME window, the input context won't invoke the + // IME related methods, so in that case we shouldn't cancel the IME session. + let is_preediting: bool = state.in_ime_preedit; + if is_preediting && !state.key_triggered_ime && !is_arrow_key { + // In this case we should cancel the IME session. + let () = msg_send![this, unmarkText]; + state.in_ime_preedit = false; + } + } + let window_event = Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: key_event, + is_synthetic: false, + }, + }; + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } + trace!("Completed `keyDown`"); +} + +extern "C" fn key_up(this: &Object, _sel: Sel, event: &NSEvent) { + trace!("Triggered `keyUp`"); + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + state.is_key_down = false; + + update_potentially_stale_modifiers(state, event); + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: create_key_event(event, false, false, false, None), + is_synthetic: false, + }, + }; + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } + trace!("Completed `keyUp`"); +} + +extern "C" fn flags_changed(this: &Object, _sel: Sel, ns_event: &NSEvent) { + use KeyCode::{ + AltLeft, AltRight, ControlLeft, ControlRight, ShiftLeft, ShiftRight, SuperLeft, SuperRight, + }; + + trace!("Triggered `flagsChanged`"); + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + let scancode = get_scancode(ns_event); + + // We'll correct the `is_press` and the `key_override` below. + let event = create_key_event(ns_event, false, false, false, Some(KeyCode::SuperLeft)); + let mut events = VecDeque::with_capacity(4); + + macro_rules! process_event { + ($tao_flag:expr, $ns_flag:expr, $target_key:expr) => { + let ns_event_contains_keymask = NSEvent::modifierFlags(ns_event).contains($ns_flag); + let mut actual_key = KeyCode::from_scancode(scancode as u32); + let was_pressed = state.phys_modifiers.contains(&actual_key); + let mut is_pressed = ns_event_contains_keymask; + let matching_keys; + if actual_key == KeyCode::KeyA { + // When switching keyboard layout using Ctrl+Space, the Ctrl release event + // has `KeyA` as its keycode which would produce an incorrect key event. + // To avoid this, we detect this scenario and override the key with one + // that should be reasonable + actual_key = $target_key; + matching_keys = true; + } else { + matching_keys = actual_key == $target_key; + if matching_keys && is_pressed && was_pressed { + // If we received a modifier flags changed event AND the key that triggered + // it was already pressed then this must mean that this event indicates the + // release of that key. + is_pressed = false; + } + } + if matching_keys { + if is_pressed != was_pressed { + let mut event = event.clone(); + event.state = if is_pressed { + ElementState::Pressed + } else { + ElementState::Released + }; + event.physical_key = actual_key; + event.logical_key = code_to_key(event.physical_key, scancode); + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + if is_pressed { + state.phys_modifiers.insert($target_key); + } else { + state.phys_modifiers.remove(&$target_key); + } + if ns_event_contains_keymask { + state.modifiers.insert($tao_flag); + } else { + state.modifiers.remove($tao_flag); + } + } + } + }; + } + process_event!( + ModifiersState::SHIFT, + NSEventModifierFlags::Shift, + ShiftLeft + ); + process_event!( + ModifiersState::SHIFT, + NSEventModifierFlags::Shift, + ShiftRight + ); + process_event!( + ModifiersState::CONTROL, + NSEventModifierFlags::Control, + ControlLeft + ); + process_event!( + ModifiersState::CONTROL, + NSEventModifierFlags::Control, + ControlRight + ); + process_event!(ModifiersState::ALT, NSEventModifierFlags::Option, AltLeft); + process_event!(ModifiersState::ALT, NSEventModifierFlags::Option, AltRight); + process_event!( + ModifiersState::SUPER, + NSEventModifierFlags::Command, + SuperLeft + ); + process_event!( + ModifiersState::SUPER, + NSEventModifierFlags::Command, + SuperRight + ); + + let window_id = WindowId(get_window_id(&state.ns_window.load().unwrap())); + + for event in events { + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id, + event, + })); + } + + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { + window_id, + event: WindowEvent::ModifiersChanged(state.modifiers), + })); + } + trace!("Completed `flagsChanged`"); +} + +extern "C" fn insert_tab(this: &Object, _sel: Sel, _sender: id) { + unsafe { + let window: id = msg_send![this, window]; + let first_responder: id = msg_send![window, firstResponder]; + let this_ptr = this as *const _ as *mut _; + if first_responder == this_ptr { + let (): _ = msg_send![window, selectNextKeyView: this]; + } + } +} + +extern "C" fn insert_back_tab(this: &Object, _sel: Sel, _sender: id) { + unsafe { + let window: id = msg_send![this, window]; + let first_responder: id = msg_send![window, firstResponder]; + let this_ptr = this as *const _ as *mut _; + if first_responder == this_ptr { + let (): _ = msg_send![window, selectPreviousKeyView: this]; + } + } +} + +// Allows us to receive Cmd-. (the shortcut for closing a dialog) +// https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6 +extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { + trace!("Triggered `cancelOperation`"); + unsafe { + let mtm = MainThreadMarker::new_unchecked(); + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + let event: Retained = msg_send![&NSApp(mtm), currentEvent]; + update_potentially_stale_modifiers(state, &event); + + let scancode = 0x2f; + let key = KeyCode::from_scancode(scancode); + debug_assert_eq!(key, KeyCode::Period); + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: create_key_event(&event, true, false, false, Some(key)), + is_synthetic: false, + }, + }; + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } + trace!("Completed `cancelOperation`"); +} + +fn mouse_click(this: &Object, event: &NSEvent, button: MouseButton, button_state: ElementState) { + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + update_potentially_stale_modifiers(state, event); + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + event: WindowEvent::MouseInput { + device_id: DEVICE_ID, + state: button_state, + button, + modifiers: event_mods(event), + }, + }; + + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } +} + +extern "C" fn mouse_down(this: &NSView, _sel: Sel, event: &NSEvent) { + mouse_motion(this, event); + mouse_click(this, event, MouseButton::Left, ElementState::Pressed); +} + +extern "C" fn mouse_up(this: &NSView, _sel: Sel, event: &NSEvent) { + mouse_motion(this, event); + mouse_click(this, event, MouseButton::Left, ElementState::Released); +} + +extern "C" fn right_mouse_down(this: &NSView, _sel: Sel, event: &NSEvent) { + mouse_motion(this, event); + mouse_click(this, event, MouseButton::Right, ElementState::Pressed); +} + +extern "C" fn right_mouse_up(this: &NSView, _sel: Sel, event: &NSEvent) { + mouse_motion(this, event); + mouse_click(this, event, MouseButton::Right, ElementState::Released); +} + +extern "C" fn other_mouse_down(this: &NSView, _sel: Sel, event: &NSEvent) { + mouse_motion(this, event); + mouse_click(this, event, MouseButton::Middle, ElementState::Pressed); +} + +extern "C" fn other_mouse_up(this: &NSView, _sel: Sel, event: &NSEvent) { + mouse_motion(this, event); + mouse_click(this, event, MouseButton::Middle, ElementState::Released); +} + +fn mouse_motion(this: &NSView, event: &NSEvent) { + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + let window_point = event.locationInWindow(); + let view_point = this.convertPoint_fromView(window_point, None); + let view_rect = NSView::frame(this); + + if view_point.x.is_sign_negative() + || view_point.y.is_sign_negative() + || view_point.x > view_rect.size.width + || view_point.y > view_rect.size.height + { + let mouse_buttons_down: NSUInteger = msg_send![class!(NSEvent), pressedMouseButtons]; + if mouse_buttons_down == 0 { + // Point is outside of the client area (view) and no buttons are pressed + return; + } + } + + let x = view_point.x as f64; + let y = view_rect.size.height as f64 - view_point.y as f64; + let logical_position = LogicalPosition::new(x, y); + + update_potentially_stale_modifiers(state, event); + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + event: WindowEvent::CursorMoved { + device_id: DEVICE_ID, + position: logical_position.to_physical(state.get_scale_factor()), + modifiers: event_mods(event), + }, + }; + + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } +} + +extern "C" fn mouse_moved(this: &NSView, _sel: Sel, event: &NSEvent) { + mouse_motion(this, event); +} + +extern "C" fn mouse_dragged(this: &NSView, _sel: Sel, event: &NSEvent) { + mouse_motion(this, event); +} + +extern "C" fn right_mouse_dragged(this: &NSView, _sel: Sel, event: &NSEvent) { + mouse_motion(this, event); +} + +extern "C" fn other_mouse_dragged(this: &NSView, _sel: Sel, event: &NSEvent) { + mouse_motion(this, event); +} + +extern "C" fn mouse_entered(this: &Object, _sel: Sel, _event: id) { + trace!("Triggered `mouseEntered`"); + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + let enter_event = Event::WindowEvent { + window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + event: WindowEvent::CursorEntered { + device_id: DEVICE_ID, + }, + }; + + AppState::queue_event(EventWrapper::StaticEvent(enter_event)); + } + trace!("Completed `mouseEntered`"); +} + +extern "C" fn mouse_exited(this: &Object, _sel: Sel, _event: id) { + trace!("Triggered `mouseExited`"); + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + event: WindowEvent::CursorLeft { + device_id: DEVICE_ID, + }, + }; + + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } + trace!("Completed `mouseExited`"); +} + +extern "C" fn scroll_wheel(this: &NSView, _sel: Sel, event: &NSEvent) { + trace!("Triggered `scrollWheel`"); + + mouse_motion(this, event); + + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + let delta = { + // macOS horizontal sign convention is the inverse of tao. + let (x, y) = (event.scrollingDeltaX() * -1.0, event.scrollingDeltaY()); + if event.hasPreciseScrollingDeltas() { + let delta = LogicalPosition::new(x, y).to_physical(state.get_scale_factor()); + MouseScrollDelta::PixelDelta(delta) + } else { + MouseScrollDelta::LineDelta(x as f32, y as f32) + } + }; + let phase = match event.phase() { + NSEventPhase::MayBegin | NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Ended => TouchPhase::Ended, + _ => TouchPhase::Moved, + }; + + let device_event = Event::DeviceEvent { + device_id: DEVICE_ID, + event: DeviceEvent::MouseWheel { delta }, + }; + + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + update_potentially_stale_modifiers(state, event); + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + event: WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta, + phase, + modifiers: event_mods(event), + }, + }; + + AppState::queue_event(EventWrapper::StaticEvent(device_event)); + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } + trace!("Completed `scrollWheel`"); +} + +extern "C" fn pressure_change_with_event(this: &NSView, _sel: Sel, event: &NSEvent) { + trace!("Triggered `pressureChangeWithEvent`"); + + mouse_motion(this, event); + + unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + + let pressure = event.pressure(); + let stage = event.stage(); + + let window_event = Event::WindowEvent { + window_id: WindowId(get_window_id(&state.ns_window.load().unwrap())), + event: WindowEvent::TouchpadPressure { + device_id: DEVICE_ID, + pressure, + stage: stage as i64, + }, + }; + + AppState::queue_event(EventWrapper::StaticEvent(window_event)); + } + trace!("Completed `pressureChangeWithEvent`"); +} + +// Allows us to receive Ctrl-Tab and Ctrl-Esc. +// Note that this *doesn't* help with any missing Cmd inputs. +// https://github.com/chromium/chromium/blob/a86a8a6bcfa438fa3ac2eba6f02b3ad1f8e0756f/ui/views/cocoa/bridged_content_view.mm#L816 +extern "C" fn wants_key_down_for_event(_this: &Object, _sel: Sel, _event: id) -> BOOL { + YES +} + +extern "C" fn accepts_first_mouse(_this: &Object, _sel: Sel, _event: id) -> BOOL { + YES +} + +pub unsafe fn inset_traffic_lights(window: &NSWindow, position: LogicalPosition) { + let (x, y) = (position.x, position.y); + + let close = window + .standardWindowButton(NSWindowButton::CloseButton) + .unwrap(); + let miniaturize = window + .standardWindowButton(NSWindowButton::MiniaturizeButton) + .unwrap(); + let zoom = window + .standardWindowButton(NSWindowButton::ZoomButton) + .unwrap(); + + let title_bar_container_view = close.superview().unwrap().superview().unwrap(); + + let close_rect = NSView::frame(&close); + let title_bar_frame_height = close_rect.size.height + y; + let mut title_bar_rect = NSView::frame(&title_bar_container_view); + title_bar_rect.size.height = title_bar_frame_height; + title_bar_rect.origin.y = window.frame().size.height - title_bar_frame_height; + let _: () = msg_send![&title_bar_container_view, setFrame: title_bar_rect]; + + let window_buttons = vec![close, miniaturize.clone(), zoom]; + let space_between = NSView::frame(&miniaturize).origin.x - close_rect.origin.x; + + for (i, button) in window_buttons.into_iter().enumerate() { + let mut rect = NSView::frame(&button); + rect.origin.x = x + (i as f64 * space_between); + button.setFrameOrigin(rect.origin); + } +} diff --git a/vendor/tao/src/platform_impl/macos/window.rs b/vendor/tao/src/platform_impl/macos/window.rs new file mode 100644 index 0000000000..861f01857c --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/window.rs @@ -0,0 +1,1788 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +#![allow(unused_unsafe)] + +use std::{ + collections::VecDeque, + convert::TryInto, + f64, + ffi::CStr, + os::raw::c_void, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, Weak, + }, +}; + +use crate::{ + dpi::{ + LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, Size::Logical, + }, + error::{ExternalError, NotSupportedError, OsError as RootOsError}, + icon::Icon, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform::macos::WindowExtMacOS, + platform_impl::{ + platform::{ + app_state::AppState, + ffi, + monitor::{self, MonitorHandle, VideoMode}, + util::{self, IdRef}, + view::{self, new_view, CursorState}, + window_delegate::new_delegate, + OsError, + }, + set_badge_label, set_progress_indicator, + }, + window::{ + CursorIcon, Fullscreen, ProgressBarState, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowId as RootWindowId, WindowSizeConstraints, + }, +}; +use core_graphics::{ + base::CGFloat, + display::{CGDisplay, CGDisplayMode}, + geometry::CGPoint, +}; +use objc2::{ + msg_send, + rc::Retained, + runtime::{AnyClass as Class, AnyObject as Object, Bool, ClassBuilder as ClassDecl, Sel}, +}; +use objc2_app_kit::{ + self as appkit, NSApp, NSApplicationPresentationOptions, NSBackingStoreType, NSColor, NSEvent, + NSEventModifierFlags, NSEventSubtype, NSEventType, NSRequestUserAttentionType, NSScreen, NSView, + NSWindow, NSWindowButton, NSWindowCollectionBehavior, NSWindowFullScreenButton, + NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, +}; +use objc2_foundation::{ + MainThreadMarker, NSArray, NSAutoreleasePool, NSDictionary, NSInteger, NSPoint, NSRect, NSSize, + NSString, NSTimeInterval, NSUInteger, +}; + +use super::{ + ffi::{id, nil, NO}, + view::ViewState, +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(pub usize); + +impl Id { + pub unsafe fn dummy() -> Self { + Id(0) + } +} + +// Convert the `NSWindow` associated with a window to a usize to use as a unique identifier +// for the window. +pub fn get_window_id(window_cocoa_id: &NSWindow) -> Id { + Id(window_cocoa_id as *const NSWindow as _) +} + +#[non_exhaustive] +#[derive(Clone)] +pub enum Parent { + None, + ChildOf(*mut c_void), +} + +#[derive(Clone)] +pub struct PlatformSpecificWindowBuilderAttributes { + pub parent: Parent, + pub movable_by_window_background: bool, + pub titlebar_transparent: bool, + pub title_hidden: bool, + pub titlebar_hidden: bool, + pub titlebar_buttons_hidden: bool, + pub fullsize_content_view: bool, + pub resize_increments: Option>, + pub disallow_hidpi: bool, + pub has_shadow: bool, + pub traffic_light_inset: Option, + pub automatic_tabbing: bool, + pub tabbing_identifier: Option, +} + +impl Default for PlatformSpecificWindowBuilderAttributes { + #[inline] + fn default() -> Self { + Self { + parent: Parent::None, + movable_by_window_background: false, + titlebar_transparent: false, + title_hidden: false, + titlebar_hidden: false, + titlebar_buttons_hidden: false, + fullsize_content_view: false, + resize_increments: None, + disallow_hidpi: false, + has_shadow: true, + traffic_light_inset: None, + automatic_tabbing: true, + tabbing_identifier: None, + } + } +} + +unsafe fn create_view( + ns_window: &NSWindow, + pl_attribs: &PlatformSpecificWindowBuilderAttributes, +) -> Option<(Retained, Weak>)> { + let (ns_view, cursor_state) = new_view(ns_window); + ns_view.map(|ns_view| { + if !pl_attribs.disallow_hidpi { + #[allow(deprecated)] + ns_view.setWantsBestResolutionOpenGLSurface(true); + } + + #[allow(deprecated)] // TODO: Use define_class! + if let Some(position) = pl_attribs.traffic_light_inset { + let state_ptr: *mut c_void = *(**ns_view).get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + let scale_factor = NSWindow::backingScaleFactor(ns_window); + let position = position.to_logical(scale_factor); + state.traffic_light_inset = Some(position); + } + + // On Mojave, views automatically become layer-backed shortly after being added to + // a window. Changing the layer-backedness of a view breaks the association between + // the view and its associated OpenGL context. To work around this, on Mojave we + // explicitly make the view layer-backed up front so that AppKit doesn't do it + // itself and break the association with its context. + if f64::floor(appkit::NSAppKitVersionNumber) > appkit::NSAppKitVersionNumber10_12 { + ns_view.setWantsLayer(true); + } + + (ns_view, cursor_state) + }) +} + +fn create_window( + attrs: &WindowAttributes, + pl_attrs: &PlatformSpecificWindowBuilderAttributes, +) -> Option> { + unsafe { + let mtm = MainThreadMarker::new_unchecked(); + let _pool = NSAutoreleasePool::new(); + let screen = match attrs.fullscreen { + Some(Fullscreen::Borderless(Some(RootMonitorHandle { inner: ref monitor }))) + | Some(Fullscreen::Exclusive(RootVideoMode { + video_mode: VideoMode { ref monitor, .. }, + })) => { + let monitor_screen = monitor.ns_screen(); + Some(monitor_screen.unwrap_or_else(|| appkit::NSScreen::mainScreen(mtm).unwrap())) + } + Some(Fullscreen::Borderless(None)) => Some( + attrs + .position + .and_then(screen_from_position) + .unwrap_or_else(|| appkit::NSScreen::mainScreen(mtm).unwrap()), + ), + None => None, + }; + let frame = match &screen { + Some(screen) => NSScreen::frame(screen), + None => { + let screen = attrs + .position + .and_then(screen_from_position) + .unwrap_or_else(|| appkit::NSScreen::mainScreen(mtm).unwrap()); + let scale_factor = NSScreen::backingScaleFactor(&screen) as f64; + let desired_size = attrs + .inner_size + .unwrap_or_else(|| PhysicalSize::new(800, 600).into()); + let (width, height): (f64, f64) = attrs + .inner_size_constraints + .clamp(desired_size, scale_factor) + .to_logical::(scale_factor) + .into(); + let (left, bottom) = match attrs.position { + Some(position) => { + let logical = util::window_position(position.to_logical(scale_factor)); + // macOS wants the position of the bottom left corner, + // but caller is setting the position of top left corner + (logical.x, logical.y - height) + } + // This value is ignored by calling win.center() below + None => (0.0, 0.0), + }; + NSRect::new(NSPoint::new(left, bottom), NSSize::new(width, height)) + } + }; + + let mut masks = if !attrs.decorations && screen.is_none() || pl_attrs.titlebar_hidden { + // Resizable UnownedWindow without a titlebar or borders + // if decorations is set to false, ignore pl_attrs + NSWindowStyleMask::Borderless + | NSWindowStyleMask::Resizable + | NSWindowStyleMask::Miniaturizable + } else { + // default case, resizable window with titlebar and titlebar buttons + NSWindowStyleMask::Closable + | NSWindowStyleMask::Miniaturizable + | NSWindowStyleMask::Resizable + | NSWindowStyleMask::Titled + }; + + if !attrs.resizable { + masks &= !NSWindowStyleMask::Resizable; + } + + if !attrs.minimizable { + masks &= !NSWindowStyleMask::Miniaturizable; + } + + if !attrs.closable { + masks &= !NSWindowStyleMask::Closable; + } + + if pl_attrs.fullsize_content_view { + masks |= NSWindowStyleMask::FullSizeContentView; + } + + let ns_window: id = msg_send![WINDOW_CLASS.0, alloc]; + let ns_window_ptr: id = msg_send![ + ns_window, + initWithContentRect: frame, + styleMask: masks, + backing: NSBackingStoreType::Buffered, + defer: NO, + ]; + + Retained::retain(ns_window_ptr).and_then(|r| r.downcast::().ok()).map(|ns_window| { + #[allow(deprecated)] + { + *((*ns_window_ptr).get_mut_ivar::("focusable")) = attrs.focusable.into(); + } + + let title = NSString::from_str(&attrs.title); + ns_window.setReleasedWhenClosed(false); + ns_window.setTitle(&title); + ns_window.setAcceptsMouseMovedEvents(true); + + if pl_attrs.titlebar_transparent { + ns_window.setTitlebarAppearsTransparent(true); + } + if pl_attrs.title_hidden { + ns_window.setTitleVisibility(appkit::NSWindowTitleVisibility::Hidden); + } + if pl_attrs.titlebar_buttons_hidden { + for titlebar_button in &[ + NSWindowFullScreenButton, + NSWindowButton::MiniaturizeButton, + NSWindowButton::CloseButton, + NSWindowButton::ZoomButton, + ] { + if let Some(button) = ns_window.standardWindowButton(*titlebar_button) { + button.setHidden(true); + } + } + } + if pl_attrs.movable_by_window_background { + ns_window.setMovableByWindowBackground(true); + } + + if attrs.always_on_top { + ns_window.setLevel(ffi::NSWindowLevel::NSFloatingWindowLevel as isize); + } + + if attrs.always_on_bottom { + ns_window.setLevel(ffi::NSWindowLevel::BelowNormalWindowLevel as isize); + } + + if attrs.content_protection { + ns_window.setSharingType(NSWindowSharingType::None); + } + + if !attrs.maximizable { + if let Some(button) = ns_window + .standardWindowButton(NSWindowButton::ZoomButton) { + button.setEnabled(false); + } + } + + if let Some(increments) = pl_attrs.resize_increments { + let (x, y) = (increments.width, increments.height); + if x >= 1.0 && y >= 1.0 { + let size = NSSize::new(x as CGFloat, y as CGFloat); + ns_window.setResizeIncrements(size); + } + } + + if let Parent::ChildOf(parent) = pl_attrs.parent { + let _: () = + msg_send![parent as id, addChildWindow: &*ns_window, ordered: NSWindowOrderingMode::Above]; + } + + if !pl_attrs.automatic_tabbing { + NSWindow::setAllowsAutomaticWindowTabbing(false, mtm); + } + + if let Some(tabbing_identifier) = &pl_attrs.tabbing_identifier { + ns_window.setTabbingIdentifier( &NSString::from_str(tabbing_identifier)); + } + + if !pl_attrs.has_shadow { + ns_window.setHasShadow(false); + } + if attrs.position.is_none() { + ns_window.center(); + } + + ns_window + }) + } +} + +fn screen_from_position(position: Position) -> Option> { + for m in monitor::available_monitors() { + let monitor_pos = m.position(); + let monitor_size = m.size(); + + // type annotations required for 32bit targets. + let window_position = position.to_physical::(m.scale_factor()); + + let is_in_monitor = monitor_pos.x <= window_position.x + && window_position.x < monitor_pos.x + monitor_size.width as i32 + && monitor_pos.y <= window_position.y + && window_position.y < monitor_pos.y + monitor_size.height as i32; + if is_in_monitor { + return m.ns_screen(); + } + } + None +} + +pub(super) fn get_ns_theme() -> Theme { + unsafe { + let appearances: Vec> = vec![ + NSString::from_str("NSAppearanceNameAqua"), + NSString::from_str("NSAppearanceNameDarkAqua"), + ]; + let app_class = class!(NSApplication); + let app: id = msg_send![app_class, sharedApplication]; + let has_theme: bool = msg_send![app, respondsToSelector: sel!(effectiveAppearance)]; + if !has_theme { + return Theme::Light; + } + let appearance: id = msg_send![app, effectiveAppearance]; + let name: Retained = msg_send![ + appearance, + bestMatchFromAppearancesWithNames: &*NSArray::from_retained_slice(&appearances) + ]; + let name = name.to_string(); + match &*name { + "NSAppearanceNameDarkAqua" => Theme::Dark, + _ => Theme::Light, + } + } +} + +pub(super) fn set_ns_theme(theme: Option) { + unsafe { + let app_class = class!(NSApplication); + let app: id = msg_send![app_class, sharedApplication]; + let has_theme: bool = msg_send![app, respondsToSelector: sel!(effectiveAppearance)]; + if has_theme { + let appearance = if let Some(theme) = theme { + let name = NSString::from_str(match theme { + Theme::Dark => "NSAppearanceNameDarkAqua", + Theme::Light => "NSAppearanceNameAqua", + }); + msg_send![class!(NSAppearance), appearanceNamed: &*name] + } else { + nil + }; + let _: () = msg_send![app, setAppearance: appearance]; + } + } +} + +struct WindowClass(&'static Class); +unsafe impl Send for WindowClass {} +unsafe impl Sync for WindowClass {} + +lazy_static! { + static ref WINDOW_CLASS: WindowClass = unsafe { + let window_superclass = class!(NSWindow); + let mut decl = ClassDecl::new( + CStr::from_bytes_with_nul(b"TaoWindow\0").unwrap(), + window_superclass, + ) + .unwrap(); + decl.add_method( + sel!(canBecomeMainWindow), + is_focusable as extern "C" fn(_, _) -> _, + ); + decl.add_method( + sel!(canBecomeKeyWindow), + is_focusable as extern "C" fn(_, _) -> _, + ); + decl.add_method(sel!(sendEvent:), send_event as extern "C" fn(_, _, _)); + // progress bar states, follows ProgressState + decl.add_ivar::(CStr::from_bytes_with_nul(b"focusable\0").unwrap()); + WindowClass(decl.register()) + }; +} + +extern "C" fn is_focusable(this: &Object, _: Sel) -> Bool { + #[allow(deprecated)] // TODO: Use define_class! + unsafe { + *(this.get_ivar("focusable")) + } +} + +extern "C" fn send_event(this: &Object, _sel: Sel, event: &NSEvent) { + unsafe { + let event_type = event.r#type(); + if event_type == NSEventType::LeftMouseDown { + // When wkwebview is set on NSWindow, `WindowBuilder::with_movable_by_window_background` is not working. + // Because of this, we need to invoke `[NSWindow performWindowDragWithEvent]` in NSLeftMouseDown event. + let is_movable_window: bool = msg_send![this, isMovableByWindowBackground]; + if is_movable_window { + let _: () = msg_send![this, performWindowDragWithEvent: event]; + } + } + let superclass = util::superclass(this); + let _: () = msg_send![super(this, superclass), sendEvent: event]; + } +} + +#[derive(Default)] +pub struct SharedState { + pub resizable: bool, + pub fullscreen: Option, + // This is true between windowWillEnterFullScreen and windowDidEnterFullScreen + // or windowWillExitFullScreen and windowDidExitFullScreen. + // We must not toggle fullscreen when this is true. + pub in_fullscreen_transition: bool, + // If it is attempted to toggle fullscreen when in_fullscreen_transition is true, + // Set target_fullscreen and do after fullscreen transition is end. + pub target_fullscreen: Option>, + pub maximized: bool, + pub standard_frame: Option, + is_simple_fullscreen: bool, + pub saved_style: Option, + /// Presentation options saved before entering `set_simple_fullscreen`, and + /// restored upon exiting it. Also used when transitioning from Borderless to + /// Exclusive fullscreen in `set_fullscreen` because we need to disable the menu + /// bar in exclusive fullscreen but want to restore the original options when + /// transitioning back to borderless fullscreen. + save_presentation_opts: Option, + pub saved_desktop_display_mode: Option<(CGDisplay, CGDisplayMode)>, + pub current_theme: Theme, +} + +impl SharedState { + pub fn saved_standard_frame(&self) -> NSRect { + self + .standard_frame + .unwrap_or_else(|| NSRect::new(NSPoint::new(50.0, 50.0), NSSize::new(800.0, 600.0))) + } +} + +impl From for SharedState { + fn from(attribs: WindowAttributes) -> Self { + SharedState { + resizable: attribs.resizable, + // This fullscreen field tracks the current state of the window + // (as seen by `WindowDelegate`), and since the window hasn't + // actually been fullscreened yet, we can't set it yet. This is + // necessary for state transitions to work right, since otherwise + // the initial value and the first `set_fullscreen` call would be + // identical, resulting in a no-op. + fullscreen: None, + maximized: attribs.maximized, + ..Default::default() + } + } +} + +pub struct UnownedWindow { + pub ns_window: Retained, // never changes + pub ns_view: Retained, // never changes + input_context: IdRef, // never changes + pub shared_state: Arc>, + decorations: AtomicBool, + cursor_state: Weak>, + transparent: bool, + pub inner_rect: Option>, +} + +unsafe impl Send for UnownedWindow {} +unsafe impl Sync for UnownedWindow {} + +impl UnownedWindow { + pub fn new( + mut win_attribs: WindowAttributes, + pl_attribs: PlatformSpecificWindowBuilderAttributes, + ) -> Result<(Arc, IdRef), RootOsError> { + if !util::is_main_thread() { + panic!("Windows can only be created on the main thread on macOS"); + } + trace!("Creating new window"); + + let _pool = unsafe { NSAutoreleasePool::new() }; + let ns_window = create_window(&win_attribs, &pl_attribs) + .ok_or_else(|| os_error!(OsError::CreationError("Couldn't create `NSWindow`")))?; + + let (ns_view, cursor_state) = unsafe { create_view(&ns_window, &pl_attribs) } + .ok_or_else(|| os_error!(OsError::CreationError("Couldn't create `NSView`")))?; + + // Configure the new view as the "key view" for the window + unsafe { + ns_window.setContentView(Some(&ns_view)); + ns_window.setInitialFirstResponder(Some(&ns_view)); + } + + let input_context = unsafe { util::create_input_context(&ns_view) }; + + let scale_factor = unsafe { NSWindow::backingScaleFactor(&ns_window) as f64 }; + + unsafe { + if win_attribs.transparent { + ns_window.setOpaque(false); + } + + if win_attribs.transparent || win_attribs.background_color.is_some() { + let color = win_attribs + .background_color + .map(|(r, g, b, a)| { + NSColor::colorWithRed_green_blue_alpha( + r as f64 / 255.0, + g as f64 / 255.0, + b as f64 / 255.0, + a as f64 / 255.0, + ) + }) + .unwrap_or_else(|| NSColor::clearColor()); + ns_window.setBackgroundColor(Some(&color)); + } + + if win_attribs.inner_size_constraints.has_min() { + let min_size = win_attribs + .inner_size_constraints + .min_size_logical(scale_factor); + set_min_inner_size(&ns_window, min_size); + } + if win_attribs.inner_size_constraints.has_max() { + let max_size = win_attribs + .inner_size_constraints + .max_size_logical(scale_factor); + set_max_inner_size(&ns_window, max_size); + } + + // register for drag and drop operations. + ns_window.registerForDraggedTypes(&NSArray::arrayWithObject(appkit::NSFilenamesPboardType)); + } + + // Since `win_attribs` is put into a mutex below, we'll just copy these + // attributes now instead of bothering to lock it later. + // Also, `SharedState` doesn't carry `fullscreen` over; it's set + // indirectly by us calling `set_fullscreen` below, causing handlers in + // `WindowDelegate` to update the state. + let fullscreen = win_attribs.fullscreen.take(); + let maximized = win_attribs.maximized; + let transparent = win_attribs.transparent; + let visible = win_attribs.visible; + let focused = win_attribs.focused; + let decorations = win_attribs.decorations; + let visible_on_all_workspaces = win_attribs.visible_on_all_workspaces; + let inner_rect = win_attribs + .inner_size + .map(|size| size.to_physical(scale_factor)); + + let cloned_preferred_theme = win_attribs.preferred_theme; + + let window = Arc::new(UnownedWindow { + ns_view, + ns_window, + input_context, + shared_state: Arc::new(Mutex::new(win_attribs.into())), + decorations: AtomicBool::new(decorations), + cursor_state, + inner_rect, + transparent, + }); + + match cloned_preferred_theme { + Some(theme) => { + set_ns_theme(Some(theme)); + let mut state = window.shared_state.lock().unwrap(); + state.current_theme = theme.clone(); + } + None => { + let mut state = window.shared_state.lock().unwrap(); + state.current_theme = get_ns_theme(); + } + } + + let delegate = new_delegate(&window, fullscreen.is_some()); + + // Set fullscreen mode after we setup everything + window.set_fullscreen(fullscreen); + window.set_visible_on_all_workspaces(visible_on_all_workspaces); + + // Setting the window as key has to happen *after* we set the fullscreen + // state, since otherwise we'll briefly see the window at normal size + // before it transitions. + if visible { + if focused { + // Tightly linked with `app_state::window_activation_hack` + unsafe { window.ns_window.makeKeyAndOrderFront(None) }; + } else { + unsafe { window.ns_window.orderFront(None) }; + } + } + + if maximized { + window.set_maximized(maximized); + } + + Ok((window, delegate)) + } + + fn set_style_mask_async(&self, mask: NSWindowStyleMask) { + unsafe { util::set_style_mask_async(&self.ns_window, &self.ns_view, mask) }; + } + + fn set_style_mask_sync(&self, mask: NSWindowStyleMask) { + unsafe { util::set_style_mask_sync(&self.ns_window, &self.ns_view, mask) }; + } + + pub fn id(&self) -> Id { + get_window_id(&self.ns_window) + } + + pub fn set_title(&self, title: &str) { + unsafe { + util::set_title_async(&self.ns_window, title.to_string()); + } + } + + pub fn title(&self) -> String { + self.ns_window.title().to_string() + } + + pub fn set_visible(&self, visible: bool) { + match visible { + true => unsafe { util::make_key_and_order_front_sync(&self.ns_window) }, + false => unsafe { util::order_out_sync(&self.ns_window) }, + } + } + + #[inline] + // Shortener for set_visible(true) + pub fn set_focus(&self) { + unsafe { + let is_minimized = self.ns_window.isMiniaturized(); + let is_visible = self.ns_window.isVisible(); + if !is_minimized && is_visible { + util::set_focus(&self.ns_window); + } + } + } + + #[inline] + pub fn set_focusable(&self, focusable: bool) { + #[allow(deprecated)] // TODO: Use define_class! + unsafe { + let ns_window = + Retained::into_raw(Retained::cast_unchecked::(self.ns_window.clone())); + *((*ns_window).get_mut_ivar::("focusable")) = focusable.into(); + } + } + + #[inline] + pub fn is_focused(&self) -> bool { + unsafe { self.ns_window.isKeyWindow() } + } + + pub fn request_redraw(&self) { + AppState::queue_redraw(RootWindowId(self.id())); + } + + pub fn outer_position(&self) -> Result, NotSupportedError> { + let frame_rect = unsafe { NSWindow::frame(&self.ns_window) }; + let position = LogicalPosition::new( + frame_rect.origin.x as f64, + util::bottom_left_to_top_left(frame_rect), + ); + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) + } + + pub fn inner_position(&self) -> Result, NotSupportedError> { + let content_rect = unsafe { + NSWindow::contentRectForFrameRect(&self.ns_window, NSWindow::frame(&self.ns_window)) + }; + let position = LogicalPosition::new( + content_rect.origin.x as f64, + util::bottom_left_to_top_left(content_rect), + ); + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) + } + + pub fn set_outer_position(&self, position: Position) { + let scale_factor = self.scale_factor(); + let position = position.to_logical(scale_factor); + unsafe { + util::set_frame_top_left_point_async(&self.ns_window, util::window_position(position)); + } + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let view_frame = unsafe { NSView::frame(&self.ns_view) }; + let logical: LogicalSize = + (view_frame.size.width as f64, view_frame.size.height as f64).into(); + let scale_factor = self.scale_factor(); + logical.to_physical(scale_factor) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let view_frame = unsafe { NSWindow::frame(&self.ns_window) }; + let logical: LogicalSize = + (view_frame.size.width as f64, view_frame.size.height as f64).into(); + let scale_factor = self.scale_factor(); + logical.to_physical(scale_factor) + } + + #[inline] + pub fn set_inner_size(&self, size: Size) { + unsafe { + let scale_factor = self.scale_factor(); + util::set_content_size_async(&self.ns_window, size.to_logical(scale_factor)); + } + } + + pub fn set_min_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.unwrap_or(Logical(LogicalSize { + width: 0.0, + height: 0.0, + })); + let scale_factor = self.scale_factor(); + unsafe { + set_min_inner_size(&self.ns_window, dimensions.to_logical(scale_factor)); + } + } + + pub fn set_max_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.unwrap_or(Logical(LogicalSize { + width: f32::MAX as f64, + height: f32::MAX as f64, + })); + let scale_factor = self.scale_factor(); + unsafe { + set_max_inner_size(&self.ns_window, dimensions.to_logical(scale_factor)); + } + } + + pub fn set_inner_size_constraints(&self, constraints: WindowSizeConstraints) { + let scale_factor = self.scale_factor(); + unsafe { + let min_size = constraints.min_size_logical(scale_factor); + set_min_inner_size(&self.ns_window, min_size); + let max_size = constraints.max_size_logical(scale_factor); + set_max_inner_size(&self.ns_window, max_size); + } + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + let fullscreen = { + trace!("Locked shared state in `set_resizable`"); + let mut shared_state_lock = self.shared_state.lock().unwrap(); + shared_state_lock.resizable = resizable; + trace!("Unlocked shared state in `set_resizable`"); + shared_state_lock.fullscreen.is_some() + }; + if !fullscreen { + let mut mask = unsafe { self.ns_window.styleMask() }; + if resizable { + mask |= NSWindowStyleMask::Resizable; + } else { + mask &= !NSWindowStyleMask::Resizable; + } + self.set_style_mask_sync(mask); + } // Otherwise, we don't change the mask until we exit fullscreen. + } + + #[inline] + pub fn set_minimizable(&self, minimizable: bool) { + let mut mask = unsafe { self.ns_window.styleMask() }; + if minimizable { + mask |= NSWindowStyleMask::Miniaturizable; + } else { + mask &= !NSWindowStyleMask::Miniaturizable; + } + self.set_style_mask_sync(mask); + } + + #[inline] + pub fn set_maximizable(&self, maximizable: bool) { + unsafe { + if let Some(button) = self + .ns_window + .standardWindowButton(NSWindowButton::ZoomButton) + { + button.setEnabled(maximizable); + } + } + } + + #[inline] + pub fn set_closable(&self, closable: bool) { + let mut mask = unsafe { self.ns_window.styleMask() }; + if closable { + mask |= NSWindowStyleMask::Closable; + } else { + mask &= !NSWindowStyleMask::Closable; + } + self.set_style_mask_sync(mask); + } + + pub fn set_cursor_icon(&self, cursor: CursorIcon) { + let cursor = util::Cursor::from(cursor); + if let Some(cursor_access) = self.cursor_state.upgrade() { + cursor_access.lock().unwrap().cursor = cursor; + } + unsafe { + self.ns_window.invalidateCursorRectsForView(&self.ns_view); + } + } + + #[inline] + pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { + // TODO: Do this for real https://stackoverflow.com/a/40922095/5435443 + CGDisplay::associate_mouse_and_mouse_cursor_position(!grab) + .map_err(|status| ExternalError::Os(os_error!(OsError::CGError(status)))) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + if let Some(cursor_access) = self.cursor_state.upgrade() { + let mut cursor_state = cursor_access.lock().unwrap(); + if visible != cursor_state.visible { + cursor_state.visible = visible; + drop(cursor_state); + unsafe { + self.ns_window.invalidateCursorRectsForView(&self.ns_view); + } + } + } + } + + #[inline] + pub fn cursor_position(&self) -> Result, ExternalError> { + util::cursor_position() + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + NSWindow::backingScaleFactor(&self.ns_window) as _ + } + + #[inline] + pub fn set_cursor_position(&self, cursor_position: Position) -> Result<(), ExternalError> { + let physical_window_position = self.inner_position().unwrap(); + let scale_factor = self.scale_factor(); + let window_position = physical_window_position.to_logical::(scale_factor); + let logical_cursor_position = cursor_position.to_logical::(scale_factor); + let point = CGPoint { + x: logical_cursor_position.x + window_position.x, + y: logical_cursor_position.y + window_position.y, + }; + CGDisplay::warp_mouse_cursor_position(point) + .map_err(|e| ExternalError::Os(os_error!(OsError::CGError(e))))?; + CGDisplay::associate_mouse_and_mouse_cursor_position(true) + .map_err(|e| ExternalError::Os(os_error!(OsError::CGError(e))))?; + + Ok(()) + } + + #[inline] + pub fn set_background_color(&self, color: Option) { + unsafe { + let color = color + .map(|(r, g, b, a)| { + Some(NSColor::colorWithRed_green_blue_alpha( + r as f64 / 255.0, + g as f64 / 255.0, + b as f64 / 255.0, + a as f64 / 255.0, + )) + }) + .unwrap_or_else(|| { + if self.transparent { + Some(NSColor::clearColor()) + } else { + None + } + }); + self.ns_window.setBackgroundColor(color.as_deref()); + } + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + unsafe { + let mtm = MainThreadMarker::new_unchecked(); + let mut event: id = msg_send![&NSApp(mtm), currentEvent]; + + let event_type: NSUInteger = msg_send![event, type]; + if event_type == 0x15 { + let mouse_location: NSPoint = msg_send![class!(NSEvent), mouseLocation]; + let event_modifier_flags: NSEventModifierFlags = msg_send![event, modifierFlags]; + let event_timestamp: NSTimeInterval = msg_send![event, timestamp]; + let event_window_number: NSInteger = msg_send![event, windowNumber]; + + event = msg_send![ + class!(NSEvent), + mouseEventWithType: NSEventType::LeftMouseDown, + location: mouse_location, + modifierFlags: event_modifier_flags, + timestamp: event_timestamp, + windowNumber: event_window_number, + context: nil, + eventNumber: NSEventSubtype::WindowExposed, + clickCount: 1, + pressure: 1.0, + ]; + } + + let _: () = msg_send![&self.ns_window, performWindowDragWithEvent: event]; + } + + Ok(()) + } + + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn set_ignore_cursor_events(&self, ignore: bool) -> Result<(), ExternalError> { + unsafe { + util::set_ignore_mouse_events(&self.ns_window, ignore); + } + + Ok(()) + } + + pub(crate) fn is_zoomed(&self) -> bool { + // because `isZoomed` doesn't work if the window's borderless, + // we make it resizable temporalily. + let curr_mask = unsafe { self.ns_window.styleMask() }; + + let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable; + let needs_temp_mask = !curr_mask.contains(required); + if needs_temp_mask { + self.set_style_mask_sync(required); + } + + let is_zoomed: bool = unsafe { self.ns_window.isZoomed() }; + + // Roll back temp styles + if needs_temp_mask { + self.set_style_mask_sync(curr_mask); + } + + is_zoomed + } + + fn saved_style(&self, shared_state: &mut SharedState) -> NSWindowStyleMask { + let base_mask = shared_state + .saved_style + .take() + .unwrap_or_else(|| unsafe { self.ns_window.styleMask() }); + if shared_state.resizable { + base_mask | NSWindowStyleMask::Resizable + } else { + base_mask & !NSWindowStyleMask::Resizable + } + } + + /// This is called when the window is exiting fullscreen, whether by the + /// user clicking on the green fullscreen button or programmatically by + /// `toggleFullScreen:` + pub(crate) fn restore_state_from_fullscreen(&self) { + trace!("Locked shared state in `restore_state_from_fullscreen`"); + let mut shared_state_lock = self.shared_state.lock().unwrap(); + + shared_state_lock.fullscreen = None; + + let maximized = shared_state_lock.maximized; + let mask = self.saved_style(&mut *shared_state_lock); + + drop(shared_state_lock); + trace!("Unlocked shared state in `restore_state_from_fullscreen`"); + + self.set_style_mask_async(mask); + self.set_maximized(maximized); + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let is_minimized: bool = unsafe { self.ns_window.isMiniaturized() }; + if is_minimized == minimized { + return; + } + + if minimized { + unsafe { + NSWindow::miniaturize(&self.ns_window, Some(&self.ns_window)); + } + } else { + unsafe { + NSWindow::deminiaturize(&self.ns_window, Some(&self.ns_window)); + } + } + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let is_zoomed = self.is_zoomed(); + if is_zoomed == maximized { + return; + }; + unsafe { + util::set_maximized_async( + &self.ns_window, + is_zoomed, + maximized, + Arc::downgrade(&self.shared_state), + ); + } + } + + #[inline] + pub fn fullscreen(&self) -> Option { + let shared_state_lock = self.shared_state.lock().unwrap(); + shared_state_lock.fullscreen.clone() + } + + #[inline] + pub fn is_visible(&self) -> bool { + unsafe { self.ns_window.isVisible() } + } + + #[inline] + pub fn is_always_on_top(&self) -> bool { + unsafe { self.ns_window.level() == ffi::kCGFloatingWindowLevelKey } + } + + #[inline] + pub fn is_maximized(&self) -> bool { + self.is_zoomed() + } + + #[inline] + pub fn is_minimized(&self) -> bool { + unsafe { self.ns_window.isMiniaturized() } + } + + #[inline] + pub fn is_resizable(&self) -> bool { + unsafe { msg_send![&self.ns_window, isResizable] } + } + + #[inline] + pub fn is_minimizable(&self) -> bool { + unsafe { msg_send![&self.ns_window, isMiniaturizable] } + } + + #[inline] + pub fn is_maximizable(&self) -> bool { + unsafe { + if let Some(button) = self + .ns_window + .standardWindowButton(NSWindowButton::ZoomButton) + { + button.isEnabled() + } else { + false + } + } + } + + #[inline] + pub fn is_closable(&self) -> bool { + let is_closable: bool = unsafe { msg_send![&self.ns_window, hasCloseBox] }; + is_closable + } + + #[inline] + pub fn is_decorated(&self) -> bool { + self.decorations.load(Ordering::Acquire) + } + + #[inline] + pub fn set_fullscreen(&self, fullscreen: Option) { + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + + trace!("Locked shared state in `set_fullscreen`"); + let mut shared_state_lock = self.shared_state.lock().unwrap(); + if shared_state_lock.is_simple_fullscreen { + trace!("Unlocked shared state in `set_fullscreen`"); + return; + } + if shared_state_lock.in_fullscreen_transition { + // We can't set fullscreen here. + // Set fullscreen after transition. + shared_state_lock.target_fullscreen = Some(fullscreen); + trace!("Unlocked shared state in `set_fullscreen`"); + return; + } + let old_fullscreen = shared_state_lock.fullscreen.clone(); + if fullscreen == old_fullscreen { + trace!("Unlocked shared state in `set_fullscreen`"); + return; + } + trace!("Unlocked shared state in `set_fullscreen`"); + drop(shared_state_lock); + + // If the fullscreen is on a different monitor, we must move the window + // to that monitor before we toggle fullscreen (as `toggleFullScreen` + // does not take a screen parameter, but uses the current screen) + if let Some(ref fullscreen) = fullscreen { + let new_screen = match fullscreen { + Fullscreen::Borderless(Some(borderless)) => { + let RootMonitorHandle { inner: monitor } = borderless.clone(); + monitor + } + Fullscreen::Borderless(None) => { + if let Some(current) = self.current_monitor_inner() { + let RootMonitorHandle { inner: monitor } = current; + monitor + } else { + return; + } + } + Fullscreen::Exclusive(RootVideoMode { + video_mode: VideoMode { ref monitor, .. }, + }) => monitor.clone(), + } + .ns_screen() + .unwrap(); + + unsafe { + let old_screen = NSWindow::screen(&self.ns_window); + if old_screen.as_ref() != Some(&new_screen) { + let mut screen_frame: NSRect = msg_send![&new_screen, frame]; + // The coordinate system here has its origin at bottom-left + // and Y goes up + screen_frame.origin.y += screen_frame.size.height; + util::set_frame_top_left_point_async(&self.ns_window, screen_frame.origin); + } + } + } + + if let Some(Fullscreen::Exclusive(ref video_mode)) = fullscreen { + // Note: `enterFullScreenMode:withOptions:` seems to do the exact + // same thing as we're doing here (captures the display, sets the + // video mode, and hides the menu bar and dock), with the exception + // of that I couldn't figure out how to set the display mode with + // it. I think `enterFullScreenMode:withOptions:` is still using the + // older display mode API where display modes were of the type + // `CFDictionary`, but this has changed, so we can't obtain the + // correct parameter for this any longer. Apple's code samples for + // this function seem to just pass in "YES" for the display mode + // parameter, which is not consistent with the docs saying that it + // takes a `NSDictionary`.. + + let display_id = video_mode.monitor().inner.native_identifier(); + + let mut fade_token = ffi::kCGDisplayFadeReservationInvalidToken; + + if matches!(old_fullscreen, Some(Fullscreen::Borderless(_))) { + unsafe { + let app = NSApp(mtm); + trace!("Locked shared state in `set_fullscreen`"); + let mut shared_state_lock = self.shared_state.lock().unwrap(); + shared_state_lock.save_presentation_opts = Some(app.presentationOptions()); + } + } + + unsafe { + // Fade to black (and wait for the fade to complete) to hide the + // flicker from capturing the display and switching display mode + if ffi::CGAcquireDisplayFadeReservation(5.0, &mut fade_token) == ffi::kCGErrorSuccess { + ffi::CGDisplayFade( + fade_token, + 0.3, + ffi::kCGDisplayBlendNormal, + ffi::kCGDisplayBlendSolidColor, + 0.0, + 0.0, + 0.0, + ffi::TRUE, + ); + } + + assert_eq!(ffi::CGDisplayCapture(display_id), ffi::kCGErrorSuccess); + } + + unsafe { + let result = ffi::CGDisplaySetDisplayMode( + display_id, + video_mode.video_mode.native_mode.0, + std::ptr::null(), + ); + assert!(result == ffi::kCGErrorSuccess, "failed to set video mode"); + + // After the display has been configured, fade back in + // asynchronously + if fade_token != ffi::kCGDisplayFadeReservationInvalidToken { + ffi::CGDisplayFade( + fade_token, + 0.6, + ffi::kCGDisplayBlendSolidColor, + ffi::kCGDisplayBlendNormal, + 0.0, + 0.0, + 0.0, + ffi::FALSE, + ); + ffi::CGReleaseDisplayFadeReservation(fade_token); + } + } + } + + trace!("Locked shared state in `set_fullscreen`"); + let mut shared_state_lock = self.shared_state.lock().unwrap(); + shared_state_lock.fullscreen = fullscreen.clone(); + trace!("Unlocked shared state in `set_fullscreen`"); + + match (&old_fullscreen, &fullscreen) { + (&None, &Some(_)) => unsafe { + util::toggle_full_screen_async( + &self.ns_window, + &self.ns_view, + old_fullscreen.is_none(), + Arc::downgrade(&self.shared_state), + ); + }, + (&Some(Fullscreen::Borderless(_)), &None) => unsafe { + // State is restored by `window_did_exit_fullscreen` + util::toggle_full_screen_async( + &self.ns_window, + &self.ns_view, + old_fullscreen.is_none(), + Arc::downgrade(&self.shared_state), + ); + }, + (&Some(Fullscreen::Exclusive(RootVideoMode { ref video_mode })), &None) => unsafe { + util::restore_display_mode_async(video_mode.monitor().inner.native_identifier()); + // Rest of the state is restored by `window_did_exit_fullscreen` + util::toggle_full_screen_async( + &self.ns_window, + &self.ns_view, + old_fullscreen.is_none(), + Arc::downgrade(&self.shared_state), + ); + }, + (&Some(Fullscreen::Borderless(_)), &Some(Fullscreen::Exclusive(_))) => unsafe { + // If we're already in fullscreen mode, calling + // `CGDisplayCapture` will place the shielding window on top of + // our window, which results in a black display and is not what + // we want. So, we must place our window on top of the shielding + // window. Unfortunately, this also makes our window be on top + // of the menu bar, and this looks broken, so we must make sure + // that the menu bar is disabled. This is done in the window + // delegate in `window:willUseFullScreenPresentationOptions:`. + let app = NSApp(mtm); + trace!("Locked shared state in `set_fullscreen`"); + shared_state_lock.save_presentation_opts = Some(app.presentationOptions()); + + let presentation_options = NSApplicationPresentationOptions::FullScreen + | NSApplicationPresentationOptions::HideDock + | NSApplicationPresentationOptions::HideMenuBar; + app.setPresentationOptions(presentation_options); + + self + .ns_window + .setLevel(ffi::CGShieldingWindowLevel() as isize + 1); + }, + ( + &Some(Fullscreen::Exclusive(RootVideoMode { ref video_mode })), + &Some(Fullscreen::Borderless(_)), + ) => unsafe { + let presentation_options = shared_state_lock.save_presentation_opts.unwrap_or_else(|| { + NSApplicationPresentationOptions::FullScreen + | NSApplicationPresentationOptions::AutoHideDock + | NSApplicationPresentationOptions::AutoHideMenuBar + }); + NSApp(mtm).setPresentationOptions(presentation_options); + + util::restore_display_mode_async(video_mode.monitor().inner.native_identifier()); + + // Restore the normal window level following the Borderless fullscreen + // `CGShieldingWindowLevel() + 1` hack. + let () = msg_send![ + &self.ns_window, + setLevel: ffi::NSWindowLevel::NSNormalWindowLevel + ]; + }, + _ => {} + } + trace!("Unlocked shared state in `set_fullscreen`"); + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + if decorations != self.decorations.load(Ordering::Acquire) { + self.decorations.store(decorations, Ordering::Release); + + let (fullscreen, resizable) = { + trace!("Locked shared state in `set_decorations`"); + let shared_state_lock = self.shared_state.lock().unwrap(); + trace!("Unlocked shared state in `set_decorations`"); + ( + shared_state_lock.fullscreen.is_some(), + shared_state_lock.resizable, + ) + }; + + // If we're in fullscreen mode, we wait to apply decoration changes + // until we're in `window_did_exit_fullscreen`. + if fullscreen { + return; + } + + let new_mask = { + let mut new_mask = if decorations { + NSWindowStyleMask::Closable + | NSWindowStyleMask::Miniaturizable + | NSWindowStyleMask::Resizable + | NSWindowStyleMask::Titled + } else { + NSWindowStyleMask::Borderless | NSWindowStyleMask::Resizable + }; + if !resizable { + new_mask &= !NSWindowStyleMask::Resizable; + } + new_mask + }; + self.set_style_mask_async(new_mask); + } + } + + #[inline] + pub fn set_always_on_bottom(&self, always_on_bottom: bool) { + let level = if always_on_bottom { + ffi::NSWindowLevel::BelowNormalWindowLevel + } else { + ffi::NSWindowLevel::NSNormalWindowLevel + }; + unsafe { util::set_level_async(&self.ns_window, level) }; + } + + #[inline] + pub fn set_always_on_top(&self, always_on_top: bool) { + let level = if always_on_top { + ffi::NSWindowLevel::NSFloatingWindowLevel + } else { + ffi::NSWindowLevel::NSNormalWindowLevel + }; + unsafe { util::set_level_async(&self.ns_window, level) }; + } + + #[inline] + pub fn set_window_icon(&self, _icon: Option) { + // macOS doesn't have window icons. Though, there is + // `setRepresentedFilename`, but that's semantically distinct and should + // only be used when the window is in some way representing a specific + // file/directory. For instance, Terminal.app uses this for the CWD. + // Anyway, that should eventually be implemented as + // `WindowBuilderExt::with_represented_file` or something, and doesn't + // have anything to do with `set_window_icon`. + // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/WinPanel/Tasks/SettingWindowTitle.html + } + + #[inline] + pub fn set_ime_position(&self, spot: Position) { + let scale_factor = self.scale_factor(); + let logical_spot = spot.to_logical(scale_factor); + unsafe { + view::set_ime_position( + &self.ns_view, + *self.input_context, + logical_spot.x, + logical_spot.y, + ); + } + } + + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let ns_request_type = request_type.map(|ty| match ty { + UserAttentionType::Critical => NSRequestUserAttentionType::CriticalRequest, + UserAttentionType::Informational => NSRequestUserAttentionType::InformationalRequest, + }); + unsafe { + let mtm = MainThreadMarker::new_unchecked(); + if let Some(ty) = ns_request_type { + NSApp(mtm).requestUserAttention(ty); + } + } + } + + #[inline] + // Allow directly accessing the current monitor internally without unwrapping. + pub(crate) fn current_monitor_inner(&self) -> Option { + unsafe { + let screen: Retained = self.ns_window.screen()?; + let desc = NSScreen::deviceDescription(&screen); + let key = NSString::from_str("NSScreenNumber"); + let value = NSDictionary::objectForKey(&desc, &key).unwrap(); + let display_id: NSUInteger = msg_send![&value, unsignedIntegerValue]; + Some(RootMonitorHandle { + inner: MonitorHandle::new(display_id.try_into().unwrap()), + }) + } + } + + #[inline] + pub fn current_monitor(&self) -> Option { + self.current_monitor_inner() + } + #[inline] + pub fn monitor_from_point(&self, x: f64, y: f64) -> Option { + monitor::from_point(x, y).map(|inner| RootMonitorHandle { inner }) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(RootMonitorHandle { inner: monitor }) + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::AppKitHandle::empty(); + window_handle.ns_window = self.ns_window(); + window_handle.ns_view = self.ns_view(); + rwh_04::RawWindowHandle::AppKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::AppKitWindowHandle::empty(); + window_handle.ns_window = self.ns_window(); + window_handle.ns_view = self.ns_view(); + rwh_05::RawWindowHandle::AppKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let window_handle = rwh_06::AppKitWindowHandle::new({ + let ptr = self.ns_view(); + std::ptr::NonNull::new(ptr).expect("Id should never be null") + }); + Ok(rwh_06::RawWindowHandle::AppKit(window_handle)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06(&self) -> Result { + Ok(rwh_06::RawDisplayHandle::AppKit( + rwh_06::AppKitDisplayHandle::new(), + )) + } + + #[inline] + pub fn theme(&self) -> Theme { + let state = self.shared_state.lock().unwrap(); + state.current_theme + } + + pub fn set_theme(&self, theme: Option) { + set_ns_theme(theme); + let mut state = self.shared_state.lock().unwrap(); + state.current_theme = theme.unwrap_or_else(get_ns_theme); + } + + pub fn set_content_protection(&self, enabled: bool) { + unsafe { + self.ns_window.setSharingType(if enabled { + NSWindowSharingType::None + } else { + NSWindowSharingType::ReadOnly + }); + } + } + + pub fn set_visible_on_all_workspaces(&self, visible: bool) { + unsafe { + let mut collection_behavior = self.ns_window.collectionBehavior(); + if visible { + collection_behavior |= NSWindowCollectionBehavior::CanJoinAllSpaces; + } else { + collection_behavior &= !NSWindowCollectionBehavior::CanJoinAllSpaces; + }; + self.ns_window.setCollectionBehavior(collection_behavior) + } + } + + pub fn set_progress_bar(&self, progress: ProgressBarState) { + set_progress_indicator(progress); + } + + pub fn set_badge_label(&self, label: Option) { + set_badge_label(label); + } +} + +impl WindowExtMacOS for UnownedWindow { + #[inline] + fn ns_window(&self) -> *mut c_void { + &*self.ns_window as *const NSWindow as *mut _ + } + + #[inline] + fn ns_view(&self) -> *mut c_void { + &*self.ns_window.contentView().unwrap() as *const NSView as *mut _ + } + + #[inline] + fn simple_fullscreen(&self) -> bool { + let shared_state_lock = self.shared_state.lock().unwrap(); + shared_state_lock.is_simple_fullscreen + } + + #[inline] + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { + let mut shared_state_lock = self.shared_state.lock().unwrap(); + + unsafe { + let mtm = MainThreadMarker::new_unchecked(); + let app = NSApp(mtm); + let is_native_fullscreen = shared_state_lock.fullscreen.is_some(); + let is_simple_fullscreen = shared_state_lock.is_simple_fullscreen; + + // Do nothing if native fullscreen is active. + if is_native_fullscreen + || (fullscreen && is_simple_fullscreen) + || (!fullscreen && !is_simple_fullscreen) + { + return false; + } + + if fullscreen { + // Remember the original window's settings + // Exclude title bar + shared_state_lock.standard_frame = Some(NSWindow::contentRectForFrameRect( + &self.ns_window, + NSWindow::frame(&self.ns_window), + )); + shared_state_lock.saved_style = Some(self.ns_window.styleMask()); + shared_state_lock.save_presentation_opts = Some(app.presentationOptions()); + + // Tell our window's state that we're in fullscreen + shared_state_lock.is_simple_fullscreen = true; + + // Simulate pre-Lion fullscreen by hiding the dock and menu bar + let presentation_options = NSApplicationPresentationOptions::AutoHideDock + | NSApplicationPresentationOptions::AutoHideMenuBar; + app.setPresentationOptions(presentation_options); + + // Hide the titlebar + util::toggle_style_mask( + &self.ns_window, + &self.ns_view, + NSWindowStyleMask::Titled, + false, + ); + + // Set the window frame to the screen frame size + let screen = self.ns_window.screen().unwrap(); + let screen_frame = NSScreen::frame(&screen); + NSWindow::setFrame_display(&self.ns_window, screen_frame, true); + + // Fullscreen windows can't be resized, minimized, or moved + util::toggle_style_mask( + &self.ns_window, + &self.ns_view, + NSWindowStyleMask::Miniaturizable, + false, + ); + util::toggle_style_mask( + &self.ns_window, + &self.ns_view, + NSWindowStyleMask::Resizable, + false, + ); + NSWindow::setMovable(&self.ns_window, false); + + true + } else { + let new_mask = self.saved_style(&mut *shared_state_lock); + self.set_style_mask_async(new_mask); + shared_state_lock.is_simple_fullscreen = false; + + if let Some(presentation_opts) = shared_state_lock.save_presentation_opts { + app.setPresentationOptions(presentation_opts); + } + + let frame = shared_state_lock.saved_standard_frame(); + NSWindow::setFrame_display(&self.ns_window, frame, true); + NSWindow::setMovable(&self.ns_window, true); + + true + } + } + } + + #[inline] + fn has_shadow(&self) -> bool { + self.ns_window.hasShadow() + } + + #[inline] + fn set_has_shadow(&self, has_shadow: bool) { + self.ns_window.setHasShadow(has_shadow) + } + + #[inline] + fn set_traffic_light_inset>(&self, position: P) { + let position: Position = position.into(); + #[allow(deprecated)] // TODO: Use define_class! + unsafe { + let state_ptr: *mut c_void = *(self.ns_view).get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + state.traffic_light_inset = Some(position.to_logical(self.scale_factor())); + } + } + + #[inline] + fn set_is_document_edited(&self, edited: bool) { + self.ns_window.setDocumentEdited(edited) + } + + #[inline] + fn is_document_edited(&self) -> bool { + unsafe { self.ns_window.isDocumentEdited() } + } + + #[inline] + fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + let mtm = MainThreadMarker::new().unwrap(); + NSWindow::setAllowsAutomaticWindowTabbing(enabled, mtm) + } + + #[inline] + fn allows_automatic_window_tabbing(&self) -> bool { + let mtm = MainThreadMarker::new().unwrap(); + NSWindow::allowsAutomaticWindowTabbing(mtm) + } + + #[inline] + fn set_tabbing_identifier(&self, identifier: &str) { + unsafe { + self + .ns_window + .setTabbingIdentifier(&NSString::from_str(identifier)); + } + } + + #[inline] + fn tabbing_identifier(&self) -> String { + let tabbing_identifier = NSWindow::tabbingIdentifier(&self.ns_window); + tabbing_identifier.to_string() + } + + #[inline] + fn set_fullsize_content_view(&self, fullsize: bool) { + let mut mask = self.ns_window.styleMask(); + if fullsize { + mask |= NSWindowStyleMask::FullSizeContentView; + } else { + mask &= !NSWindowStyleMask::FullSizeContentView; + } + self.set_style_mask_sync(mask); + } + + #[inline] + fn set_titlebar_transparent(&self, transparent: bool) { + self.ns_window.setTitlebarAppearsTransparent(transparent); + } + + fn set_badge_label(&self, label: Option) { + set_badge_label(label); + } +} + +impl Drop for UnownedWindow { + fn drop(&mut self) { + trace!("Dropping `UnownedWindow` ({:?})", self as *mut _); + // Close the window if it has not yet been closed. + unsafe { util::close_async(&self.ns_window) }; + } +} + +unsafe fn set_min_inner_size(window: &NSWindow, mut min_size: LogicalSize) { + let mut current_rect = NSWindow::frame(window); + let content_rect = NSWindow::contentRectForFrameRect(window, NSWindow::frame(window)); + // Convert from client area size to window size + min_size.width += (current_rect.size.width - content_rect.size.width) as f64; // this tends to be 0 + min_size.height += (current_rect.size.height - content_rect.size.height) as f64; + window.setMinSize(NSSize { + width: min_size.width as CGFloat, + height: min_size.height as CGFloat, + }); + // If necessary, resize the window to match constraint + if current_rect.size.width < min_size.width { + current_rect.size.width = min_size.width; + window.setFrame_display(current_rect, false) + } + if current_rect.size.height < min_size.height { + // The origin point of a rectangle is at its bottom left in Cocoa. + // To ensure the window's top-left point remains the same: + current_rect.origin.y += current_rect.size.height - min_size.height; + current_rect.size.height = min_size.height; + window.setFrame_display(current_rect, false) + } +} + +unsafe fn set_max_inner_size(window: &NSWindow, mut max_size: LogicalSize) { + let mut current_rect = NSWindow::frame(window); + let content_rect = NSWindow::contentRectForFrameRect(window, NSWindow::frame(window)); + // Convert from client area size to window size + max_size.width += (current_rect.size.width - content_rect.size.width) as f64; // this tends to be 0 + max_size.height += (current_rect.size.height - content_rect.size.height) as f64; + window.setMaxSize(NSSize { + width: max_size.width as CGFloat, + height: max_size.height as CGFloat, + }); + // If necessary, resize the window to match constraint + if current_rect.size.width > max_size.width { + current_rect.size.width = max_size.width; + window.setFrame_display(current_rect, false) + } + if current_rect.size.height > max_size.height { + // The origin point of a rectangle is at its bottom left in Cocoa. + // To ensure the window's top-left point remains the same: + current_rect.origin.y += current_rect.size.height - max_size.height; + current_rect.size.height = max_size.height; + window.setFrame_display(current_rect, false) + } +} diff --git a/vendor/tao/src/platform_impl/macos/window_delegate.rs b/vendor/tao/src/platform_impl/macos/window_delegate.rs new file mode 100644 index 0000000000..b84edc98c4 --- /dev/null +++ b/vendor/tao/src/platform_impl/macos/window_delegate.rs @@ -0,0 +1,679 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + f64, + ffi::CStr, + os::raw::c_void, + sync::{Arc, Weak}, +}; + +use objc2::{ + msg_send, + rc::Retained, + runtime::{AnyClass as Class, AnyObject as Object, ClassBuilder as ClassDecl, Sel}, +}; +use objc2_app_kit::{ + self as appkit, NSApplicationPresentationOptions, NSPasteboard, NSView, NSWindow, +}; +use objc2_foundation::{NSArray, NSAutoreleasePool, NSString, NSUInteger}; + +use crate::{ + dpi::{LogicalPosition, LogicalSize}, + event::{Event, WindowEvent}, + keyboard::ModifiersState, + platform_impl::platform::{ + app_state::AppState, + event::{EventProxy, EventWrapper}, + ffi::{id, nil, BOOL, NO, YES}, + util::{self, IdRef}, + view::ViewState, + window::{get_ns_theme, get_window_id, UnownedWindow}, + }, + window::{Fullscreen, WindowId}, +}; + +pub struct WindowDelegateState { + ns_window: Retained, // never changes + // We keep this ns_view because we still need its view state for some extern function + // like didResignKey + ns_view: Retained, // never changes + + window: Weak, + + // TODO: It's possible for delegate methods to be called asynchronously, + // causing data races / `RefCell` panics. + + // This is set when WindowBuilder::with_fullscreen was set, + // see comments of `window_did_fail_to_enter_fullscreen` + initial_fullscreen: bool, + + // During `windowDidResize`, we use this to only send Moved if the position changed. + previous_position: Option<(f64, f64)>, + + // Used to prevent redundant events. + previous_scale_factor: f64, + + // Used to prevent resized events from being fired + // when we are using our workaround in the `is_zoomed` function. + is_checking_zoomed_in: bool, +} + +impl WindowDelegateState { + pub fn new(window: &Arc, initial_fullscreen: bool) -> Self { + let scale_factor = window.scale_factor(); + let mut delegate_state = WindowDelegateState { + ns_window: window.ns_window.clone(), + ns_view: window.ns_view.clone(), + window: Arc::downgrade(window), + initial_fullscreen, + previous_position: None, + previous_scale_factor: scale_factor, + is_checking_zoomed_in: false, + }; + if (scale_factor - 1.0).abs() > f64::EPSILON { + delegate_state.emit_static_scale_factor_changed_event(); + } + + delegate_state + } + + fn ns_view(&self) -> Retained { + self.ns_window.contentView().unwrap() + } + + fn with_window(&mut self, callback: F) -> Option + where + F: FnOnce(&UnownedWindow) -> T, + { + self.window.upgrade().map(|ref window| callback(window)) + } + + pub fn emit_event(&mut self, event: WindowEvent<'static>) { + let event = Event::WindowEvent { + window_id: WindowId(get_window_id(&self.ns_window)), + event, + }; + AppState::queue_event(EventWrapper::StaticEvent(event)); + } + + pub fn emit_static_scale_factor_changed_event(&mut self) { + let scale_factor = self.get_scale_factor(); + if (scale_factor - self.previous_scale_factor).abs() < f64::EPSILON { + return; + }; + + self.previous_scale_factor = scale_factor; + let wrapper = EventWrapper::EventProxy(EventProxy::DpiChangedProxy { + ns_window: self.ns_window.clone(), + suggested_size: self.view_size(), + scale_factor, + }); + AppState::queue_event(wrapper); + } + + pub fn emit_resize_event(&mut self) { + let rect = NSView::frame(&self.ns_view()); + let scale_factor = self.get_scale_factor(); + let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); + let size = logical_size.to_physical(scale_factor); + self.emit_event(WindowEvent::Resized(size)); + } + + fn emit_move_event(&mut self) { + let rect = NSWindow::frame(&self.ns_window); + let x = rect.origin.x as f64; + let y = util::bottom_left_to_top_left(rect); + let moved = self.previous_position != Some((x, y)); + if moved { + self.previous_position = Some((x, y)); + let scale_factor = self.get_scale_factor(); + let physical_pos = LogicalPosition::::from((x, y)).to_physical(scale_factor); + self.emit_event(WindowEvent::Moved(physical_pos)); + } + } + + fn get_scale_factor(&self) -> f64 { + NSWindow::backingScaleFactor(&self.ns_window) as f64 + } + + fn view_size(&self) -> LogicalSize { + let ns_size = NSView::frame(&self.ns_view()).size; + LogicalSize::new(ns_size.width as f64, ns_size.height as f64) + } +} + +pub fn new_delegate(window: &Arc, initial_fullscreen: bool) -> IdRef { + let state = WindowDelegateState::new(window, initial_fullscreen); + unsafe { + // This is free'd in `dealloc` + let state_ptr = Box::into_raw(Box::new(state)) as *mut c_void; + let delegate: id = msg_send![WINDOW_DELEGATE_CLASS.0, alloc]; + IdRef::new(msg_send![delegate, initWithTao: state_ptr]) + } +} + +struct WindowDelegateClass(*const Class); +unsafe impl Send for WindowDelegateClass {} +unsafe impl Sync for WindowDelegateClass {} + +lazy_static! { + static ref WINDOW_DELEGATE_CLASS: WindowDelegateClass = unsafe { + let superclass = class!(NSResponder); + let mut decl = ClassDecl::new( + CStr::from_bytes_with_nul(b"TaoWindowDelegate\0").unwrap(), + superclass, + ) + .unwrap(); + + decl.add_method(sel!(dealloc), dealloc as extern "C" fn(_, _)); + decl.add_method( + sel!(initWithTao:), + init_with_tao as extern "C" fn(_, _, _) -> _, + ); + decl.add_method( + sel!(markIsCheckingZoomedIn), + mark_is_checking_zoomed_in as extern "C" fn(_, _), + ); + decl.add_method( + sel!(clearIsCheckingZoomedIn), + clear_is_checking_zoomed_in as extern "C" fn(_, _), + ); + + decl.add_method( + sel!(windowShouldClose:), + window_should_close as extern "C" fn(_, _, _) -> _, + ); + decl.add_method( + sel!(windowWillClose:), + window_will_close as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(windowDidResize:), + window_did_resize as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(windowDidMove:), + window_did_move as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(windowDidChangeBackingProperties:), + window_did_change_backing_properties as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(windowDidBecomeKey:), + window_did_become_key as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(windowDidResignKey:), + window_did_resign_key as extern "C" fn(_, _, _), + ); + + decl.add_method( + sel!(draggingEntered:), + dragging_entered as extern "C" fn(_, _, _) -> _, + ); + decl.add_method( + sel!(prepareForDragOperation:), + prepare_for_drag_operation as extern "C" fn(_, _, _) -> _, + ); + decl.add_method( + sel!(performDragOperation:), + perform_drag_operation as extern "C" fn(_, _, _) -> _, + ); + decl.add_method( + sel!(concludeDragOperation:), + conclude_drag_operation as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(draggingExited:), + dragging_exited as extern "C" fn(_, _, _), + ); + + decl.add_method( + sel!(window:willUseFullScreenPresentationOptions:), + window_will_use_fullscreen_presentation_options as extern "C" fn(_, _, _, _) -> _, + ); + decl.add_method( + sel!(windowDidEnterFullScreen:), + window_did_enter_fullscreen as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(windowWillEnterFullScreen:), + window_will_enter_fullscreen as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(windowDidExitFullScreen:), + window_did_exit_fullscreen as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(windowWillExitFullScreen:), + window_will_exit_fullscreen as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(windowDidFailToEnterFullScreen:), + window_did_fail_to_enter_fullscreen as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(effectiveAppearanceDidChange:), + effective_appearance_did_change as extern "C" fn(_, _, _), + ); + decl.add_method( + sel!(effectiveAppearanceDidChangedOnMainThread:), + effective_appearance_did_changed_on_main_thread as extern "C" fn(_, _, _), + ); + + decl.add_ivar::<*mut c_void>(CStr::from_bytes_with_nul(b"taoState\0").unwrap()); + WindowDelegateClass(decl.register()) + }; +} + +// This function is definitely unsafe, but labeling that would increase +// boilerplate and wouldn't really clarify anything... +fn with_state T, T>(this: &Object, callback: F) { + #[allow(deprecated)] // TODO: Use define_class! + let state_ptr = unsafe { + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + &mut *(state_ptr as *mut WindowDelegateState) + }; + callback(state_ptr); +} + +extern "C" fn dealloc(this: &Object, _sel: Sel) { + with_state(this, |state| unsafe { + drop(Box::from_raw(state as *mut WindowDelegateState)); + }); +} + +extern "C" fn init_with_tao(this: &Object, _sel: Sel, state: *mut c_void) -> id { + #[allow(deprecated)] // TODO: Use define_class! + unsafe { + let this: id = msg_send![this, init]; + if this != nil { + *(*this).get_mut_ivar("taoState") = state; + with_state(&*this, |state| { + let () = msg_send![&state.ns_window, setDelegate: this]; + }); + } + + let notification_center: &Object = + msg_send![class!(NSDistributedNotificationCenter), defaultCenter]; + let notification_name = NSString::from_str("AppleInterfaceThemeChangedNotification"); + let _: () = msg_send![ + notification_center, + addObserver: this + selector: sel!(effectiveAppearanceDidChange:) + name: &*notification_name + object: nil + ]; + + this + } +} + +extern "C" fn mark_is_checking_zoomed_in(this: &Object, _sel: Sel) { + with_state(&*this, |state| { + state.is_checking_zoomed_in = true; + }); +} + +extern "C" fn clear_is_checking_zoomed_in(this: &Object, _sel: Sel) { + with_state(&*this, |state| { + state.is_checking_zoomed_in = false; + }); +} + +extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL { + trace!("Triggered `windowShouldClose:`"); + with_state(this, |state| state.emit_event(WindowEvent::CloseRequested)); + trace!("Completed `windowShouldClose:`"); + NO +} + +extern "C" fn window_will_close(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowWillClose:`"); + with_state(this, |state| unsafe { + // `setDelegate:` retains the previous value and then autoreleases it + let _pool = NSAutoreleasePool::new(); + // Since El Capitan, we need to be careful that delegate methods can't + // be called after the window closes. + let () = msg_send![&state.ns_window, setDelegate: nil]; + state.emit_event(WindowEvent::Destroyed); + }); + trace!("Completed `windowWillClose:`"); +} + +extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidResize:`"); + with_state(this, |state| { + if !state.is_checking_zoomed_in { + state.emit_resize_event(); + state.emit_move_event(); + } + }); + trace!("Completed `windowDidResize:`"); +} + +// This won't be triggered if the move was part of a resize. +extern "C" fn window_did_move(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidMove:`"); + with_state(this, |state| { + state.emit_move_event(); + }); + trace!("Completed `windowDidMove:`"); +} + +extern "C" fn window_did_change_backing_properties(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidChangeBackingProperties:`"); + with_state(this, |state| { + state.emit_static_scale_factor_changed_event(); + }); + trace!("Completed `windowDidChangeBackingProperties:`"); +} + +extern "C" fn window_did_become_key(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidBecomeKey:`"); + with_state(this, |state| { + // TODO: center the cursor if the window had mouse grab when it + // lost focus + state.emit_event(WindowEvent::Focused(true)); + }); + trace!("Completed `windowDidBecomeKey:`"); +} + +extern "C" fn window_did_resign_key(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidResignKey:`"); + with_state(this, |state| { + // It happens rather often, e.g. when the user is Cmd+Tabbing, that the + // NSWindowDelegate will receive a didResignKey event despite no event + // being received when the modifiers are released. This is because + // flagsChanged events are received by the NSView instead of the + // NSWindowDelegate, and as a result a tracked modifiers state can quite + // easily fall out of synchrony with reality. This requires us to emit + // a synthetic ModifiersChanged event when we lose focus. + // + // Here we (very unsafely) acquire the taoState (a ViewState) from the + // Object referenced by state.ns_view (an IdRef, which is dereferenced + // to an id) + #[allow(deprecated)] // TODO: Use define_class! + let view_state: &mut ViewState = unsafe { + let ns_view: &Object = &state.ns_view; + let state_ptr: *mut c_void = *ns_view.get_ivar("taoState"); + &mut *(state_ptr as *mut ViewState) + }; + + // Both update the state and emit a ModifiersChanged event. + if !view_state.modifiers.is_empty() { + view_state.modifiers = ModifiersState::empty(); + state.emit_event(WindowEvent::ModifiersChanged(view_state.modifiers)); + } + + state.emit_event(WindowEvent::Focused(false)); + }); + trace!("Completed `windowDidResignKey:`"); +} + +/// Invoked when the dragged image enters destination bounds or frame +extern "C" fn dragging_entered(this: &Object, _: Sel, sender: id) -> BOOL { + trace!("Triggered `draggingEntered:`"); + + use std::path::PathBuf; + + let pb: Retained = unsafe { msg_send![sender, draggingPasteboard] }; + let filenames = + unsafe { NSPasteboard::propertyListForType(&pb, appkit::NSFilenamesPboardType) }.unwrap(); + + for file in unsafe { Retained::cast_unchecked::(filenames) } { + let file = unsafe { Retained::cast_unchecked::(file) }; + + unsafe { + let f = NSString::UTF8String(&file); + let path = CStr::from_ptr(f).to_string_lossy().into_owned(); + + with_state(this, |state| { + state.emit_event(WindowEvent::HoveredFile(PathBuf::from(path))); + }); + } + } + + trace!("Completed `draggingEntered:`"); + YES +} + +/// Invoked when the image is released +extern "C" fn prepare_for_drag_operation(_: &Object, _: Sel, _: id) -> BOOL { + trace!("Triggered `prepareForDragOperation:`"); + trace!("Completed `prepareForDragOperation:`"); + YES +} + +/// Invoked after the released image has been removed from the screen +extern "C" fn perform_drag_operation(this: &Object, _: Sel, sender: id) -> BOOL { + trace!("Triggered `performDragOperation:`"); + + use std::path::PathBuf; + + let pb: Retained = unsafe { msg_send![sender, draggingPasteboard] }; + let filenames = + unsafe { NSPasteboard::propertyListForType(&pb, appkit::NSFilenamesPboardType) }.unwrap(); + + for file in unsafe { Retained::cast_unchecked::(filenames) } { + let file = unsafe { Retained::cast_unchecked::(file) }; + + unsafe { + let f = NSString::UTF8String(&file); + let path = CStr::from_ptr(f).to_string_lossy().into_owned(); + + with_state(this, |state| { + state.emit_event(WindowEvent::DroppedFile(PathBuf::from(path))); + }); + } + } + + trace!("Completed `performDragOperation:`"); + YES +} + +/// Invoked when the dragging operation is complete +extern "C" fn conclude_drag_operation(_: &Object, _: Sel, _: id) { + trace!("Triggered `concludeDragOperation:`"); + trace!("Completed `concludeDragOperation:`"); +} + +/// Invoked when the dragging operation is cancelled +extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) { + trace!("Triggered `draggingExited:`"); + with_state(this, |state| { + state.emit_event(WindowEvent::HoveredFileCancelled) + }); + trace!("Completed `draggingExited:`"); +} + +/// Invoked when before enter fullscreen +extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowWillEnterFullscreen:`"); + + with_state(this, |state| { + state.with_window(|window| { + trace!("Locked shared state in `window_will_enter_fullscreen`"); + let mut shared_state = window.shared_state.lock().unwrap(); + shared_state.maximized = window.is_zoomed(); + match shared_state.fullscreen { + // Exclusive mode sets the state in `set_fullscreen` as the user + // can't enter exclusive mode by other means (like the + // fullscreen button on the window decorations) + Some(Fullscreen::Exclusive(_)) => (), + // `window_will_enter_fullscreen` was triggered and we're already + // in fullscreen, so we must've reached here by `set_fullscreen` + // as it updates the state + Some(Fullscreen::Borderless(_)) => (), + // Otherwise, we must've reached fullscreen by the user clicking + // on the green fullscreen button. Update state! + None => { + let current_monitor = window.current_monitor_inner(); + shared_state.fullscreen = Some(Fullscreen::Borderless(current_monitor)) + } + } + shared_state.in_fullscreen_transition = true; + trace!("Unlocked shared state in `window_will_enter_fullscreen`"); + }) + }); + trace!("Completed `windowWillEnterFullscreen:`"); +} + +/// Invoked when before exit fullscreen +extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowWillExitFullScreen:`"); + + with_state(this, |state| { + state.with_window(|window| { + trace!("Locked shared state in `window_will_exit_fullscreen`"); + let mut shared_state = window.shared_state.lock().unwrap(); + shared_state.in_fullscreen_transition = true; + trace!("Unlocked shared state in `window_will_exit_fullscreen`"); + }); + }); + trace!("Completed `windowWillExitFullScreen:`"); +} + +extern "C" fn window_will_use_fullscreen_presentation_options( + this: &Object, + _: Sel, + _: id, + proposed_options: NSUInteger, +) -> NSUInteger { + // Generally, games will want to disable the menu bar and the dock. Ideally, + // this would be configurable by the user. Unfortunately because of our + // `CGShieldingWindowLevel() + 1` hack (see `set_fullscreen`), our window is + // placed on top of the menu bar in exclusive fullscreen mode. This looks + // broken so we always disable the menu bar in exclusive fullscreen. We may + // still want to make this configurable for borderless fullscreen. Right now + // we don't, for consistency. If we do, it should be documented that the + // user-provided options are ignored in exclusive fullscreen. + let mut options: NSUInteger = proposed_options; + with_state(this, |state| { + state.with_window(|window| { + trace!("Locked shared state in `window_will_use_fullscreen_presentation_options`"); + let shared_state = window.shared_state.lock().unwrap(); + if let Some(Fullscreen::Exclusive(_)) = shared_state.fullscreen { + options = (NSApplicationPresentationOptions::FullScreen + | NSApplicationPresentationOptions::HideDock + | NSApplicationPresentationOptions::HideMenuBar) + .bits(); + } + trace!("Unlocked shared state in `window_will_use_fullscreen_presentation_options`"); + }) + }); + + options +} + +/// Invoked when entered fullscreen +extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidEnterFullscreen:`"); + with_state(this, |state| { + state.initial_fullscreen = false; + state.with_window(|window| { + trace!("Locked shared state in `window_did_enter_fullscreen`"); + let mut shared_state = window.shared_state.lock().unwrap(); + shared_state.in_fullscreen_transition = false; + let target_fullscreen = shared_state.target_fullscreen.take(); + trace!("Unlocked shared state in `window_did_enter_fullscreen`"); + drop(shared_state); + if let Some(target_fullscreen) = target_fullscreen { + window.set_fullscreen(target_fullscreen); + } + }); + state.emit_resize_event(); + state.emit_move_event(); + }); + trace!("Completed `windowDidEnterFullscreen:`"); +} + +/// Invoked when exited fullscreen +extern "C" fn window_did_exit_fullscreen(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidExitFullscreen:`"); + with_state(this, |state| { + state.with_window(|window| { + window.restore_state_from_fullscreen(); + trace!("Locked shared state in `window_did_exit_fullscreen`"); + let mut shared_state = window.shared_state.lock().unwrap(); + shared_state.in_fullscreen_transition = false; + let target_fullscreen = shared_state.target_fullscreen.take(); + trace!("Unlocked shared state in `window_did_exit_fullscreen`"); + drop(shared_state); + if let Some(target_fullscreen) = target_fullscreen { + window.set_fullscreen(target_fullscreen); + } + }); + state.emit_resize_event(); + state.emit_move_event(); + }); + trace!("Completed `windowDidExitFullscreen:`"); +} + +/// Invoked when fail to enter fullscreen +/// +/// When this window launch from a fullscreen app (e.g. launch from VS Code +/// terminal), it creates a new virtual destkop and a transition animation. +/// This animation takes one second and cannot be disable without +/// elevated privileges. In this animation time, all toggleFullscreen events +/// will be failed. In this implementation, we will try again by using +/// performSelector:withObject:afterDelay: until window_did_enter_fullscreen. +/// It should be fine as we only do this at initialzation (i.e with_fullscreen +/// was set). +/// +/// From Apple doc: +/// In some cases, the transition to enter full-screen mode can fail, +/// due to being in the midst of handling some other animation or user gesture. +/// This method indicates that there was an error, and you should clean up any +/// work you may have done to prepare to enter full-screen mode. +extern "C" fn window_did_fail_to_enter_fullscreen(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidFailToEnterFullscreen:`"); + with_state(this, |state| { + state.with_window(|window| { + trace!("Locked shared state in `window_did_fail_to_enter_fullscreen`"); + let mut shared_state = window.shared_state.lock().unwrap(); + shared_state.in_fullscreen_transition = false; + shared_state.target_fullscreen = None; + trace!("Unlocked shared state in `window_did_fail_to_enter_fullscreen`"); + }); + if state.initial_fullscreen { + let _: () = unsafe { + msg_send![ + &state.ns_window, + performSelector:sel!(toggleFullScreen:), + withObject:nil, + afterDelay: 0.5, + ] + }; + } else { + state.with_window(|window| window.restore_state_from_fullscreen()); + } + }); + trace!("Completed `windowDidFailToEnterFullscreen:`"); +} + +// Observe theme change +extern "C" fn effective_appearance_did_change(this: &Object, _: Sel, _: id) { + trace!("Triggered `effectiveAppearDidChange:`"); + unsafe { + let _: () = msg_send![this, performSelectorOnMainThread: sel!(effectiveAppearanceDidChangedOnMainThread:), withObject:nil, waitUntilDone:false]; + } +} +extern "C" fn effective_appearance_did_changed_on_main_thread(this: &Object, _: Sel, _: id) { + with_state(this, |state| { + let theme = get_ns_theme(); + let current_theme = state.window.upgrade().map(|w| { + let mut state = w.shared_state.lock().unwrap(); + let current_theme = state.current_theme; + state.current_theme = theme; + current_theme + }); + if current_theme != Some(theme) { + state.emit_event(WindowEvent::ThemeChanged(theme)); + } + }); + trace!("Completed `effectiveAppearDidChange:`"); +} diff --git a/vendor/tao/src/platform_impl/mod.rs b/vendor/tao/src/platform_impl/mod.rs new file mode 100644 index 0000000000..e8b62af96c --- /dev/null +++ b/vendor/tao/src/platform_impl/mod.rs @@ -0,0 +1,40 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(target_os = "windows")] +#[path = "windows/mod.rs"] +mod platform; +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +#[path = "linux/mod.rs"] +mod platform; +#[cfg(target_os = "macos")] +#[path = "macos/mod.rs"] +mod platform; +#[cfg(target_os = "android")] +#[path = "android/mod.rs"] +mod platform; +#[cfg(target_os = "ios")] +#[path = "ios/mod.rs"] +mod platform; + +pub use platform::*; + +#[cfg(all( + not(target_os = "ios"), + not(target_os = "windows"), + not(target_os = "linux"), + not(target_os = "macos"), + not(target_os = "android"), + not(target_os = "dragonfly"), + not(target_os = "freebsd"), + not(target_os = "netbsd"), + not(target_os = "openbsd"), +))] +compile_error!("The platform you're compiling for is not supported by tao"); diff --git a/vendor/tao/src/platform_impl/windows/dark_mode.rs b/vendor/tao/src/platform_impl/windows/dark_mode.rs new file mode 100644 index 0000000000..ddefdb15ea --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/dark_mode.rs @@ -0,0 +1,240 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +#![allow(non_snake_case)] + +use once_cell::sync::Lazy; +/// This is a simple implementation of support for Windows Dark Mode, +/// which is inspired by the solution in https://github.com/ysc3839/win32-darkmode +use windows::{ + core::{s, w, BOOL, PCSTR, PSTR}, + Win32::{ + Foundation::{HANDLE, HMODULE, HWND, LPARAM, WPARAM}, + Graphics::Dwm::{DwmSetWindowAttribute, DWMWINDOWATTRIBUTE}, + System::LibraryLoader::*, + UI::{Accessibility::*, Input::KeyboardAndMouse::GetActiveWindow, WindowsAndMessaging::*}, + }, +}; + +use std::ffi::c_void; + +use crate::window::Theme; + +use super::util; + +static HUXTHEME: Lazy = + Lazy::new(|| unsafe { LoadLibraryA(s!("uxtheme.dll")).unwrap_or_default().0 as _ }); + +static DARK_MODE_SUPPORTED: Lazy = Lazy::new(|| { + // We won't try to do anything for windows versions < 17763 + // (Windows 10 October 2018 update) + let v = *util::WIN_VERSION; + v.major == 10 && v.minor == 0 && v.build >= 17763 +}); + +pub fn allow_dark_mode_for_app(is_dark_mode: bool) { + if *DARK_MODE_SUPPORTED { + const UXTHEME_ALLOWDARKMODEFORAPP_ORDINAL: u16 = 135; + type AllowDarkModeForApp = unsafe extern "system" fn(bool) -> bool; + static ALLOW_DARK_MODE_FOR_APP: Lazy> = Lazy::new(|| unsafe { + if HMODULE(*HUXTHEME as _).is_invalid() { + return None; + } + + GetProcAddress( + HMODULE(*HUXTHEME as _), + PCSTR::from_raw(UXTHEME_ALLOWDARKMODEFORAPP_ORDINAL as usize as *mut _), + ) + .map(|handle| std::mem::transmute(handle)) + }); + + #[repr(C)] + enum PreferredAppMode { + Default, + AllowDark, + // ForceDark, + // ForceLight, + // Max, + } + const UXTHEME_SETPREFERREDAPPMODE_ORDINAL: u16 = 135; + type SetPreferredAppMode = unsafe extern "system" fn(PreferredAppMode) -> PreferredAppMode; + static SET_PREFERRED_APP_MODE: Lazy> = Lazy::new(|| unsafe { + if HMODULE(*HUXTHEME as _).is_invalid() { + return None; + } + + GetProcAddress( + HMODULE(*HUXTHEME as _), + PCSTR::from_raw(UXTHEME_SETPREFERREDAPPMODE_ORDINAL as usize as *mut _), + ) + .map(|handle| std::mem::transmute(handle)) + }); + + if util::WIN_VERSION.build < 18362 { + if let Some(_allow_dark_mode_for_app) = *ALLOW_DARK_MODE_FOR_APP { + unsafe { _allow_dark_mode_for_app(is_dark_mode) }; + } + } else if let Some(_set_preferred_app_mode) = *SET_PREFERRED_APP_MODE { + let mode = if is_dark_mode { + PreferredAppMode::AllowDark + } else { + PreferredAppMode::Default + }; + unsafe { _set_preferred_app_mode(mode) }; + } + + refresh_immersive_color_policy_state(); + } +} + +fn refresh_immersive_color_policy_state() { + const UXTHEME_REFRESHIMMERSIVECOLORPOLICYSTATE_ORDINAL: u16 = 104; + type RefreshImmersiveColorPolicyState = unsafe extern "system" fn(); + static REFRESH_IMMERSIVE_COLOR_POLICY_STATE: Lazy> = + Lazy::new(|| unsafe { + if HMODULE(*HUXTHEME as _).is_invalid() { + return None; + } + + GetProcAddress( + HMODULE(*HUXTHEME as _), + PCSTR::from_raw(UXTHEME_REFRESHIMMERSIVECOLORPOLICYSTATE_ORDINAL as usize as *mut _), + ) + .map(|handle| std::mem::transmute(handle)) + }); + + if let Some(_refresh_immersive_color_policy_state) = *REFRESH_IMMERSIVE_COLOR_POLICY_STATE { + unsafe { _refresh_immersive_color_policy_state() } + } +} + +/// Attempt to set a theme on a window, if necessary. +/// Returns the theme that was picked +pub fn try_window_theme( + hwnd: HWND, + preferred_theme: Option, + redraw_title_bar: bool, +) -> Theme { + if *DARK_MODE_SUPPORTED { + let is_dark_mode = match preferred_theme { + Some(theme) => theme == Theme::Dark, + None => should_use_dark_mode(), + }; + + let theme = match is_dark_mode { + true => Theme::Dark, + false => Theme::Light, + }; + + refresh_titlebar_theme_color(hwnd, is_dark_mode, redraw_title_bar); + + theme + } else { + Theme::Light + } +} + +pub fn allow_dark_mode_for_window(hwnd: HWND, is_dark_mode: bool) { + const UXTHEME_ALLOWDARKMODEFORWINDOW_ORDINAL: u16 = 133; + type AllowDarkModeForWindow = unsafe extern "system" fn(HWND, bool) -> bool; + static ALLOW_DARK_MODE_FOR_WINDOW: Lazy> = Lazy::new(|| unsafe { + if HMODULE(*HUXTHEME as _).is_invalid() { + return None; + } + + GetProcAddress( + HMODULE(*HUXTHEME as _), + PCSTR::from_raw(UXTHEME_ALLOWDARKMODEFORWINDOW_ORDINAL as usize as *mut _), + ) + .map(|handle| std::mem::transmute(handle)) + }); + + if *DARK_MODE_SUPPORTED { + if let Some(_allow_dark_mode_for_window) = *ALLOW_DARK_MODE_FOR_WINDOW { + unsafe { _allow_dark_mode_for_window(hwnd, is_dark_mode) }; + } + } +} + +fn refresh_titlebar_theme_color(hwnd: HWND, is_dark_mode: bool, redraw_title_bar: bool) { + if util::WIN_VERSION.build < 17763 { + let mut is_dark_mode_bigbool: i32 = is_dark_mode.into(); + unsafe { + let _ = SetPropW( + hwnd, + w!("UseImmersiveDarkModeColors"), + Some(HANDLE(&mut is_dark_mode_bigbool as *mut _ as _)), + ); + } + } else { + // https://github.com/MicrosoftDocs/sdk-api/pull/966/files + let dwmwa_use_immersive_dark_mode = if util::WIN_VERSION.build > 18985 { + DWMWINDOWATTRIBUTE(20) + } else { + DWMWINDOWATTRIBUTE(19) + }; + let dark_mode = BOOL::from(is_dark_mode); + unsafe { + let _ = DwmSetWindowAttribute( + hwnd, + dwmwa_use_immersive_dark_mode, + &dark_mode as *const BOOL as *const c_void, + std::mem::size_of::() as u32, + ); + if redraw_title_bar { + if GetActiveWindow() == hwnd { + DefWindowProcW(hwnd, WM_NCACTIVATE, WPARAM::default(), LPARAM::default()); + DefWindowProcW(hwnd, WM_NCACTIVATE, WPARAM(true.into()), LPARAM::default()); + } else { + DefWindowProcW(hwnd, WM_NCACTIVATE, WPARAM(true.into()), LPARAM::default()); + DefWindowProcW(hwnd, WM_NCACTIVATE, WPARAM::default(), LPARAM::default()); + } + } + } + } +} + +fn should_use_dark_mode() -> bool { + should_apps_use_dark_mode() && !is_high_contrast() +} + +fn should_apps_use_dark_mode() -> bool { + const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: u16 = 132; + type ShouldAppsUseDarkMode = unsafe extern "system" fn() -> bool; + static SHOULD_APPS_USE_DARK_MODE: Lazy> = Lazy::new(|| unsafe { + if HMODULE(*HUXTHEME as _).is_invalid() { + return None; + } + + GetProcAddress( + HMODULE(*HUXTHEME as _), + PCSTR::from_raw(UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL as usize as *mut _), + ) + .map(|handle| std::mem::transmute(handle)) + }); + + SHOULD_APPS_USE_DARK_MODE + .map(|should_apps_use_dark_mode| unsafe { (should_apps_use_dark_mode)() }) + .unwrap_or(false) +} + +fn is_high_contrast() -> bool { + const HCF_HIGHCONTRASTON: u32 = 1; + + let mut hc = HIGHCONTRASTA { + cbSize: 0, + dwFlags: Default::default(), + lpszDefaultScheme: PSTR::null(), + }; + + let ok = unsafe { + SystemParametersInfoA( + SPI_GETHIGHCONTRAST, + std::mem::size_of_val(&hc) as _, + Some(&mut hc as *mut _ as _), + Default::default(), + ) + }; + + ok.is_ok() && (HCF_HIGHCONTRASTON & hc.dwFlags.0) != 0 +} diff --git a/vendor/tao/src/platform_impl/windows/dpi.rs b/vendor/tao/src/platform_impl/windows/dpi.rs new file mode 100644 index 0000000000..e4eb427c77 --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/dpi.rs @@ -0,0 +1,109 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![allow(non_snake_case, unused_unsafe)] + +use std::sync::Once; + +use windows::Win32::{ + Foundation::HWND, + Graphics::Gdi::*, + UI::{HiDpi::*, WindowsAndMessaging::*}, +}; + +use crate::platform_impl::platform::util::{ + ENABLE_NON_CLIENT_DPI_SCALING, GET_DPI_FOR_MONITOR, GET_DPI_FOR_WINDOW, SET_PROCESS_DPI_AWARE, + SET_PROCESS_DPI_AWARENESS, SET_PROCESS_DPI_AWARENESS_CONTEXT, +}; + +pub fn become_dpi_aware() { + static ENABLE_DPI_AWARENESS: Once = Once::new(); + ENABLE_DPI_AWARENESS.call_once(|| { + unsafe { + if let Some(SetProcessDpiAwarenessContext) = *SET_PROCESS_DPI_AWARENESS_CONTEXT { + // We are on Windows 10 Anniversary Update (1607) or later. + if !SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2).as_bool() { + // V2 only works with Windows 10 Creators Update (1703). Try using the older + // V1 if we can't set V2. + let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); + } + } else if let Some(SetProcessDpiAwareness) = *SET_PROCESS_DPI_AWARENESS { + // We are on Windows 8.1 or later. + let _ = SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); + } else if let Some(SetProcessDPIAware) = *SET_PROCESS_DPI_AWARE { + // We are on Vista or later. + let _ = SetProcessDPIAware(); + } + } + }); +} + +pub fn enable_non_client_dpi_scaling(hwnd: HWND) { + unsafe { + if let Some(EnableNonClientDpiScaling) = *ENABLE_NON_CLIENT_DPI_SCALING { + let _ = EnableNonClientDpiScaling(hwnd); + } + } +} + +pub fn get_monitor_dpi(hmonitor: HMONITOR) -> Option { + unsafe { + if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { + // We are on Windows 8.1 or later. + let mut dpi_x = 0; + let mut dpi_y = 0; + if GetDpiForMonitor(hmonitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y).is_ok() { + // MSDN says that "the values of *dpiX and *dpiY are identical. You only need to + // record one of the values to determine the DPI and respond appropriately". + // https://msdn.microsoft.com/en-us/library/windows/desktop/dn280510(v=vs.85).aspx + return Some(dpi_x); + } + } + } + None +} + +pub fn dpi_to_scale_factor(dpi: u32) -> f64 { + dpi as f64 / USER_DEFAULT_SCREEN_DPI as f64 +} + +pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { + let hdc = GetDC(Some(hwnd)); + if hdc.is_invalid() { + panic!("[tao] `GetDC` returned null!"); + } + if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { + // We are on Windows 10 Anniversary Update (1607) or later. + match GetDpiForWindow(hwnd) { + 0 => USER_DEFAULT_SCREEN_DPI, // 0 is returned if hwnd is invalid + dpi => dpi, + } + } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { + // We are on Windows 8.1 or later. + let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if monitor.is_invalid() { + return USER_DEFAULT_SCREEN_DPI; + } + + let mut dpi_x = 0; + let mut dpi_y = 0; + if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y).is_ok() { + dpi_x + } else { + USER_DEFAULT_SCREEN_DPI + } + } else { + // We are on Vista or later. + if IsProcessDPIAware().as_bool() { + // If the process is DPI aware, then scaling must be handled by the application using + // this DPI value. + GetDeviceCaps(Some(hdc), LOGPIXELSX) as u32 + } else { + // If the process is DPI unaware, then scaling is performed by the OS; we thus return + // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the + // application and the WM. + USER_DEFAULT_SCREEN_DPI + } + } +} diff --git a/vendor/tao/src/platform_impl/windows/drop_handler.rs b/vendor/tao/src/platform_impl/windows/drop_handler.rs new file mode 100644 index 0000000000..d2685cd12b --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/drop_handler.rs @@ -0,0 +1,181 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{cell::UnsafeCell, ffi::OsString, os::windows::ffi::OsStringExt, path::PathBuf, ptr}; + +use windows::{ + core::implement, + Win32::{ + Foundation::{self as win32f, HWND, POINTL}, + System::{ + Com::{IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL}, + Ole::{ + IDropTarget, IDropTarget_Impl, CF_HDROP, DROPEFFECT, DROPEFFECT_COPY, DROPEFFECT_NONE, + }, + SystemServices::MODIFIERKEYS_FLAGS, + }, + UI::Shell::{DragFinish, DragQueryFileW, HDROP}, + }, +}; + +use crate::platform_impl::platform::WindowId; + +use crate::{event::Event, window::WindowId as SuperWindowId}; + +#[implement(IDropTarget)] +pub struct FileDropHandler { + window: HWND, + send_event: Box)>, + cursor_effect: UnsafeCell, + hovered_is_valid: UnsafeCell, /* If the currently hovered item is not valid there must not be any `HoveredFileCancelled` emitted */ +} + +impl FileDropHandler { + pub fn new(window: HWND, send_event: Box)>) -> FileDropHandler { + Self { + window, + send_event, + cursor_effect: DROPEFFECT_NONE.into(), + hovered_is_valid: false.into(), + } + } + + unsafe fn iterate_filenames( + data_obj: windows_core::Ref<'_, IDataObject>, + callback: F, + ) -> Option + where + F: Fn(PathBuf), + { + let drop_format = FORMATETC { + cfFormat: CF_HDROP.0, + ptd: ptr::null_mut(), + dwAspect: DVASPECT_CONTENT.0, + lindex: -1, + tymed: TYMED_HGLOBAL.0 as u32, + }; + + match data_obj + .as_ref() + .expect("Received null IDataObject") + .GetData(&drop_format) + { + Ok(medium) => { + let hglobal = medium.u.hGlobal; + let hdrop = HDROP(hglobal.0 as _); + + // The second parameter (0xFFFFFFFF) instructs the function to return the item count + let mut lpsz_file = []; + let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, Some(&mut lpsz_file)); + + for i in 0..item_count { + // Get the length of the path string NOT including the terminating null character. + // Previously, this was using a fixed size array of MAX_PATH length, but the + // Windows API allows longer paths under certain circumstances. + let character_count = DragQueryFileW(hdrop, i, Some(&mut lpsz_file)) as usize; + let str_len = character_count + 1; + + // Fill path_buf with the null-terminated file name + let mut path_buf = Vec::with_capacity(str_len); + DragQueryFileW(hdrop, i, std::mem::transmute(path_buf.spare_capacity_mut())); + path_buf.set_len(str_len); + + callback(OsString::from_wide(&path_buf[0..character_count]).into()); + } + + Some(hdrop) + } + Err(error) => { + debug!( + "{}", + match error.code() { + win32f::DV_E_FORMATETC => { + // If the dropped item is not a file this error will occur. + // In this case it is OK to return without taking further action. + "Error occured while processing dropped/hovered item: item is not a file." + } + _ => "Unexpected error occured while processing dropped/hovered item.", + } + ); + None + } + } + } +} + +#[allow(non_snake_case)] +impl IDropTarget_Impl for FileDropHandler_Impl { + fn DragEnter( + &self, + pDataObj: windows_core::Ref<'_, IDataObject>, + _grfKeyState: MODIFIERKEYS_FLAGS, + _pt: &POINTL, + pdwEffect: *mut DROPEFFECT, + ) -> windows::core::Result<()> { + use crate::event::WindowEvent::HoveredFile; + unsafe { + let hdrop = FileDropHandler::iterate_filenames(pDataObj, |filename| { + (self.send_event)(Event::WindowEvent { + window_id: SuperWindowId(WindowId(self.window.0 as _)), + event: HoveredFile(filename), + }); + }); + let hovered_is_valid = hdrop.is_some(); + let cursor_effect = if hovered_is_valid { + DROPEFFECT_COPY + } else { + DROPEFFECT_NONE + }; + *self.hovered_is_valid.get() = hovered_is_valid; + *self.cursor_effect.get() = cursor_effect; + *pdwEffect = cursor_effect; + } + Ok(()) + } + + fn DragOver( + &self, + _grfKeyState: MODIFIERKEYS_FLAGS, + _pt: &POINTL, + pdwEffect: *mut DROPEFFECT, + ) -> windows::core::Result<()> { + unsafe { + *pdwEffect = *self.cursor_effect.get(); + } + Ok(()) + } + + fn DragLeave(&self) -> windows::core::Result<()> { + use crate::event::WindowEvent::HoveredFileCancelled; + if unsafe { *self.hovered_is_valid.get() } { + (self.send_event)(Event::WindowEvent { + window_id: SuperWindowId(WindowId(self.window.0 as _)), + event: HoveredFileCancelled, + }); + } + Ok(()) + } + + fn Drop( + &self, + pDataObj: windows_core::Ref<'_, IDataObject>, + _grfKeyState: MODIFIERKEYS_FLAGS, + _pt: &POINTL, + _pdwEffect: *mut DROPEFFECT, + ) -> windows::core::Result<()> { + use crate::event::WindowEvent::DroppedFile; + unsafe { + let hdrop = FileDropHandler::iterate_filenames(pDataObj, |filename| { + (self.send_event)(Event::WindowEvent { + window_id: SuperWindowId(WindowId(self.window.0 as _)), + event: DroppedFile(filename), + }); + }); + if let Some(hdrop) = hdrop { + DragFinish(hdrop); + } + } + Ok(()) + } +} diff --git a/vendor/tao/src/platform_impl/windows/event.rs b/vendor/tao/src/platform_impl/windows/event.rs new file mode 100644 index 0000000000..b2fa29cdfd --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/event.rs @@ -0,0 +1,421 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + char, + os::raw::c_int, + ptr, + sync::atomic::{AtomicBool, AtomicPtr, Ordering}, +}; + +use crate::event::{ModifiersState, ScanCode, VirtualKeyCode}; + +use windows::Win32::{ + Foundation::{HWND, LPARAM, WPARAM}, + UI::{ + Input::KeyboardAndMouse::*, + TextServices::HKL, + WindowsAndMessaging::{self as win32wm, *}, + }, +}; + +fn key_pressed(vkey: c_int) -> bool { + unsafe { (GetKeyState(vkey) & (1 << 15)) == (1 << 15) } +} + +pub fn get_key_mods() -> ModifiersState { + let filter_out_altgr = layout_uses_altgr() && key_pressed(VK_RMENU); + + let mut mods = ModifiersState::empty(); + mods.set(ModifiersState::SHIFT, key_pressed(VK_SHIFT)); + mods.set( + ModifiersState::CTRL, + key_pressed(VK_CONTROL) && !filter_out_altgr, + ); + mods.set( + ModifiersState::ALT, + key_pressed(VK_MENU) && !filter_out_altgr, + ); + mods.set( + ModifiersState::LOGO, + key_pressed(VK_LWIN) || key_pressed(VK_RWIN), + ); + mods +} + +bitflags! { + #[derive(Default)] + pub struct ModifiersStateSide: u32 { + const LSHIFT = 0b010 << 0; + const RSHIFT = 0b001 << 0; + + const LCTRL = 0b010 << 3; + const RCTRL = 0b001 << 3; + + const LALT = 0b010 << 6; + const RALT = 0b001 << 6; + + const LLOGO = 0b010 << 9; + const RLOGO = 0b001 << 9; + } +} + +impl ModifiersStateSide { + pub fn filter_out_altgr(&self) -> ModifiersStateSide { + match layout_uses_altgr() && self.contains(Self::RALT) { + false => *self, + true => *self & !(Self::LCTRL | Self::RCTRL | Self::LALT | Self::RALT), + } + } +} + +impl From for ModifiersState { + fn from(side: ModifiersStateSide) -> Self { + let mut state = ModifiersState::default(); + state.set( + Self::SHIFT, + side.intersects(ModifiersStateSide::LSHIFT | ModifiersStateSide::RSHIFT), + ); + state.set( + Self::CTRL, + side.intersects(ModifiersStateSide::LCTRL | ModifiersStateSide::RCTRL), + ); + state.set( + Self::ALT, + side.intersects(ModifiersStateSide::LALT | ModifiersStateSide::RALT), + ); + state.set( + Self::LOGO, + side.intersects(ModifiersStateSide::LLOGO | ModifiersStateSide::RLOGO), + ); + state + } +} + +pub fn get_pressed_keys() -> impl Iterator { + let mut keyboard_state = vec![0u8; 256]; + unsafe { GetKeyboardState(keyboard_state.as_mut_ptr()) }; + keyboard_state + .into_iter() + .enumerate() + .filter(|(_, p)| (*p & (1 << 7)) != 0) // whether or not a key is pressed is communicated via the high-order bit + .map(|(i, _)| i as c_int) +} + +unsafe fn get_char(keyboard_state: &[u8; 256], v_key: u32, hkl: HKL) -> Option { + let mut unicode_bytes = [0u16; 5]; + let len = ToUnicodeEx( + v_key, + 0, + keyboard_state.as_ptr(), + unicode_bytes.as_mut_ptr(), + unicode_bytes.len() as _, + 0, + hkl, + ); + if len >= 1 { + char::decode_utf16(unicode_bytes.iter().cloned()) + .next() + .and_then(|c| c.ok()) + } else { + None + } +} + +/// Figures out if the keyboard layout has an AltGr key instead of an Alt key. +/// +/// Unfortunately, the Windows API doesn't give a way for us to conveniently figure that out. So, +/// we use a technique blatantly stolen from [the Firefox source code][source]: iterate over every +/// possible virtual key and compare the `char` output when AltGr is pressed vs when it isn't. If +/// pressing AltGr outputs characters that are different from the standard characters, the layout +/// uses AltGr. Otherwise, it doesn't. +/// +/// [source]: https://github.com/mozilla/gecko-dev/blob/265e6721798a455604328ed5262f430cfcc37c2f/widget/windows/KeyboardLayout.cpp#L4356-L4416 +fn layout_uses_altgr() -> bool { + unsafe { + static ACTIVE_LAYOUT: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + static USES_ALTGR: AtomicBool = AtomicBool::new(false); + + let hkl = GetKeyboardLayout(0); + let old_hkl = ACTIVE_LAYOUT.swap(hkl, Ordering::SeqCst); + + if hkl == old_hkl { + return USES_ALTGR.load(Ordering::SeqCst); + } + + let mut keyboard_state_altgr = [0u8; 256]; + // AltGr is an alias for Ctrl+Alt for... some reason. Whatever it is, those are the keypresses + // we have to emulate to do an AltGr test. + keyboard_state_altgr[VK_MENU as usize] = 0x80; + keyboard_state_altgr[VK_CONTROL as usize] = 0x80; + + let keyboard_state_empty = [0u8; 256]; + + for v_key in 0..=255 { + let key_noaltgr = get_char(&keyboard_state_empty, v_key, hkl); + let key_altgr = get_char(&keyboard_state_altgr, v_key, hkl); + if let (Some(noaltgr), Some(altgr)) = (key_noaltgr, key_altgr) { + if noaltgr != altgr { + USES_ALTGR.store(true, Ordering::SeqCst); + return true; + } + } + } + + USES_ALTGR.store(false, Ordering::SeqCst); + false + } +} + +pub fn vkey_to_tao_vkey(vkey: u32) -> Option { + // VK_* codes are documented here https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx + match vkey { + //win32wm::VK_LBUTTON => Some(VirtualKeyCode::Lbutton), + //win32wm::VK_RBUTTON => Some(VirtualKeyCode::Rbutton), + //win32wm::VK_CANCEL => Some(VirtualKeyCode::Cancel), + //win32wm::VK_MBUTTON => Some(VirtualKeyCode::Mbutton), + //win32wm::VK_XBUTTON1 => Some(VirtualKeyCode::Xbutton1), + //win32wm::VK_XBUTTON2 => Some(VirtualKeyCode::Xbutton2), + win32wm::VK_BACK => Some(VirtualKeyCode::Back), + win32wm::VK_TAB => Some(VirtualKeyCode::Tab), + //win32wm::VK_CLEAR => Some(VirtualKeyCode::Clear), + win32wm::VK_RETURN => Some(VirtualKeyCode::Return), + win32wm::VK_LSHIFT => Some(VirtualKeyCode::LShift), + win32wm::VK_RSHIFT => Some(VirtualKeyCode::RShift), + win32wm::VK_LCONTROL => Some(VirtualKeyCode::LControl), + win32wm::VK_RCONTROL => Some(VirtualKeyCode::RControl), + win32wm::VK_LMENU => Some(VirtualKeyCode::LAlt), + win32wm::VK_RMENU => Some(VirtualKeyCode::RAlt), + win32wm::VK_PAUSE => Some(VirtualKeyCode::Pause), + win32wm::VK_CAPITAL => Some(VirtualKeyCode::Capital), + win32wm::VK_KANA => Some(VirtualKeyCode::Kana), + //win32wm::VK_HANGUEL => Some(VirtualKeyCode::Hanguel), + //win32wm::VK_HANGUL => Some(VirtualKeyCode::Hangul), + //win32wm::VK_JUNJA => Some(VirtualKeyCode::Junja), + //win32wm::VK_FINAL => Some(VirtualKeyCode::Final), + //win32wm::VK_HANJA => Some(VirtualKeyCode::Hanja), + win32wm::VK_KANJI => Some(VirtualKeyCode::Kanji), + win32wm::VK_ESCAPE => Some(VirtualKeyCode::Escape), + win32wm::VK_CONVERT => Some(VirtualKeyCode::Convert), + win32wm::VK_NONCONVERT => Some(VirtualKeyCode::NoConvert), + //win32wm::VK_ACCEPT => Some(VirtualKeyCode::Accept), + //win32wm::VK_MODECHANGE => Some(VirtualKeyCode::Modechange), + win32wm::VK_SPACE => Some(VirtualKeyCode::Space), + win32wm::VK_PRIOR => Some(VirtualKeyCode::PageUp), + win32wm::VK_NEXT => Some(VirtualKeyCode::PageDown), + win32wm::VK_END => Some(VirtualKeyCode::End), + win32wm::VK_HOME => Some(VirtualKeyCode::Home), + win32wm::VK_LEFT => Some(VirtualKeyCode::Left), + win32wm::VK_UP => Some(VirtualKeyCode::Up), + win32wm::VK_RIGHT => Some(VirtualKeyCode::Right), + win32wm::VK_DOWN => Some(VirtualKeyCode::Down), + //win32wm::VK_SELECT => Some(VirtualKeyCode::Select), + //win32wm::VK_PRINT => Some(VirtualKeyCode::Print), + //win32wm::VK_EXECUTE => Some(VirtualKeyCode::Execute), + win32wm::VK_SNAPSHOT => Some(VirtualKeyCode::Snapshot), + win32wm::VK_INSERT => Some(VirtualKeyCode::Insert), + win32wm::VK_DELETE => Some(VirtualKeyCode::Delete), + //win32wm::VK_HELP => Some(VirtualKeyCode::Help), + 0x30 => Some(VirtualKeyCode::Key0), + 0x31 => Some(VirtualKeyCode::Key1), + 0x32 => Some(VirtualKeyCode::Key2), + 0x33 => Some(VirtualKeyCode::Key3), + 0x34 => Some(VirtualKeyCode::Key4), + 0x35 => Some(VirtualKeyCode::Key5), + 0x36 => Some(VirtualKeyCode::Key6), + 0x37 => Some(VirtualKeyCode::Key7), + 0x38 => Some(VirtualKeyCode::Key8), + 0x39 => Some(VirtualKeyCode::Key9), + 0x41 => Some(VirtualKeyCode::A), + 0x42 => Some(VirtualKeyCode::B), + 0x43 => Some(VirtualKeyCode::C), + 0x44 => Some(VirtualKeyCode::D), + 0x45 => Some(VirtualKeyCode::E), + 0x46 => Some(VirtualKeyCode::F), + 0x47 => Some(VirtualKeyCode::G), + 0x48 => Some(VirtualKeyCode::H), + 0x49 => Some(VirtualKeyCode::I), + 0x4A => Some(VirtualKeyCode::J), + 0x4B => Some(VirtualKeyCode::K), + 0x4C => Some(VirtualKeyCode::L), + 0x4D => Some(VirtualKeyCode::M), + 0x4E => Some(VirtualKeyCode::N), + 0x4F => Some(VirtualKeyCode::O), + 0x50 => Some(VirtualKeyCode::P), + 0x51 => Some(VirtualKeyCode::Q), + 0x52 => Some(VirtualKeyCode::R), + 0x53 => Some(VirtualKeyCode::S), + 0x54 => Some(VirtualKeyCode::T), + 0x55 => Some(VirtualKeyCode::U), + 0x56 => Some(VirtualKeyCode::V), + 0x57 => Some(VirtualKeyCode::W), + 0x58 => Some(VirtualKeyCode::X), + 0x59 => Some(VirtualKeyCode::Y), + 0x5A => Some(VirtualKeyCode::Z), + win32wm::VK_LWIN => Some(VirtualKeyCode::LWin), + win32wm::VK_RWIN => Some(VirtualKeyCode::RWin), + win32wm::VK_APPS => Some(VirtualKeyCode::Apps), + win32wm::VK_SLEEP => Some(VirtualKeyCode::Sleep), + win32wm::VK_NUMPAD0 => Some(VirtualKeyCode::Numpad0), + win32wm::VK_NUMPAD1 => Some(VirtualKeyCode::Numpad1), + win32wm::VK_NUMPAD2 => Some(VirtualKeyCode::Numpad2), + win32wm::VK_NUMPAD3 => Some(VirtualKeyCode::Numpad3), + win32wm::VK_NUMPAD4 => Some(VirtualKeyCode::Numpad4), + win32wm::VK_NUMPAD5 => Some(VirtualKeyCode::Numpad5), + win32wm::VK_NUMPAD6 => Some(VirtualKeyCode::Numpad6), + win32wm::VK_NUMPAD7 => Some(VirtualKeyCode::Numpad7), + win32wm::VK_NUMPAD8 => Some(VirtualKeyCode::Numpad8), + win32wm::VK_NUMPAD9 => Some(VirtualKeyCode::Numpad9), + win32wm::VK_MULTIPLY => Some(VirtualKeyCode::NumpadMultiply), + win32wm::VK_ADD => Some(VirtualKeyCode::NumpadAdd), + //win32wm::VK_SEPARATOR => Some(VirtualKeyCode::Separator), + win32wm::VK_SUBTRACT => Some(VirtualKeyCode::NumpadSubtract), + win32wm::VK_DECIMAL => Some(VirtualKeyCode::NumpadDecimal), + win32wm::VK_DIVIDE => Some(VirtualKeyCode::NumpadDivide), + win32wm::VK_F1 => Some(VirtualKeyCode::F1), + win32wm::VK_F2 => Some(VirtualKeyCode::F2), + win32wm::VK_F3 => Some(VirtualKeyCode::F3), + win32wm::VK_F4 => Some(VirtualKeyCode::F4), + win32wm::VK_F5 => Some(VirtualKeyCode::F5), + win32wm::VK_F6 => Some(VirtualKeyCode::F6), + win32wm::VK_F7 => Some(VirtualKeyCode::F7), + win32wm::VK_F8 => Some(VirtualKeyCode::F8), + win32wm::VK_F9 => Some(VirtualKeyCode::F9), + win32wm::VK_F10 => Some(VirtualKeyCode::F10), + win32wm::VK_F11 => Some(VirtualKeyCode::F11), + win32wm::VK_F12 => Some(VirtualKeyCode::F12), + win32wm::VK_F13 => Some(VirtualKeyCode::F13), + win32wm::VK_F14 => Some(VirtualKeyCode::F14), + win32wm::VK_F15 => Some(VirtualKeyCode::F15), + win32wm::VK_F16 => Some(VirtualKeyCode::F16), + win32wm::VK_F17 => Some(VirtualKeyCode::F17), + win32wm::VK_F18 => Some(VirtualKeyCode::F18), + win32wm::VK_F19 => Some(VirtualKeyCode::F19), + win32wm::VK_F20 => Some(VirtualKeyCode::F20), + win32wm::VK_F21 => Some(VirtualKeyCode::F21), + win32wm::VK_F22 => Some(VirtualKeyCode::F22), + win32wm::VK_F23 => Some(VirtualKeyCode::F23), + win32wm::VK_F24 => Some(VirtualKeyCode::F24), + win32wm::VK_NUMLOCK => Some(VirtualKeyCode::Numlock), + win32wm::VK_SCROLL => Some(VirtualKeyCode::Scroll), + win32wm::VK_BROWSER_BACK => Some(VirtualKeyCode::NavigateBackward), + win32wm::VK_BROWSER_FORWARD => Some(VirtualKeyCode::NavigateForward), + win32wm::VK_BROWSER_REFRESH => Some(VirtualKeyCode::WebRefresh), + win32wm::VK_BROWSER_STOP => Some(VirtualKeyCode::WebStop), + win32wm::VK_BROWSER_SEARCH => Some(VirtualKeyCode::WebSearch), + win32wm::VK_BROWSER_FAVORITES => Some(VirtualKeyCode::WebFavorites), + win32wm::VK_BROWSER_HOME => Some(VirtualKeyCode::WebHome), + win32wm::VK_VOLUME_MUTE => Some(VirtualKeyCode::Mute), + win32wm::VK_VOLUME_DOWN => Some(VirtualKeyCode::VolumeDown), + win32wm::VK_VOLUME_UP => Some(VirtualKeyCode::VolumeUp), + win32wm::VK_MEDIA_NEXT_TRACK => Some(VirtualKeyCode::NextTrack), + win32wm::VK_MEDIA_PREV_TRACK => Some(VirtualKeyCode::PrevTrack), + win32wm::VK_MEDIA_STOP => Some(VirtualKeyCode::MediaStop), + win32wm::VK_MEDIA_PLAY_PAUSE => Some(VirtualKeyCode::PlayPause), + win32wm::VK_LAUNCH_MAIL => Some(VirtualKeyCode::Mail), + win32wm::VK_LAUNCH_MEDIA_SELECT => Some(VirtualKeyCode::MediaSelect), + /*win32wm::VK_LAUNCH_APP1 => Some(VirtualKeyCode::Launch_app1), + win32wm::VK_LAUNCH_APP2 => Some(VirtualKeyCode::Launch_app2),*/ + win32wm::VK_OEM_PLUS => Some(VirtualKeyCode::Equals), + win32wm::VK_OEM_COMMA => Some(VirtualKeyCode::Comma), + win32wm::VK_OEM_MINUS => Some(VirtualKeyCode::Minus), + win32wm::VK_OEM_PERIOD => Some(VirtualKeyCode::Period), + win32wm::VK_OEM_1 => map_text_keys(vkey), + win32wm::VK_OEM_2 => map_text_keys(vkey), + win32wm::VK_OEM_3 => map_text_keys(vkey), + win32wm::VK_OEM_4 => map_text_keys(vkey), + win32wm::VK_OEM_5 => map_text_keys(vkey), + win32wm::VK_OEM_6 => map_text_keys(vkey), + win32wm::VK_OEM_7 => map_text_keys(vkey), + /* win32wm::VK_OEM_8 => Some(VirtualKeyCode::Oem_8), */ + win32wm::VK_OEM_102 => Some(VirtualKeyCode::OEM102), + /*win32wm::VK_PROCESSKEY => Some(VirtualKeyCode::Processkey), + win32wm::VK_PACKET => Some(VirtualKeyCode::Packet), + win32wm::VK_ATTN => Some(VirtualKeyCode::Attn), + win32wm::VK_CRSEL => Some(VirtualKeyCode::Crsel), + win32wm::VK_EXSEL => Some(VirtualKeyCode::Exsel), + win32wm::VK_EREOF => Some(VirtualKeyCode::Ereof), + win32wm::VK_PLAY => Some(VirtualKeyCode::Play), + win32wm::VK_ZOOM => Some(VirtualKeyCode::Zoom), + win32wm::VK_NONAME => Some(VirtualKeyCode::Noname), + win32wm::VK_PA1 => Some(VirtualKeyCode::Pa1), + win32wm::VK_OEM_CLEAR => Some(VirtualKeyCode::Oem_clear),*/ + _ => None, + } +} + +pub fn handle_extended_keys( + vkey: u32, + mut scancode: UINT, + extended: bool, +) -> Option<(c_int, UINT)> { + // Welcome to hell https://blog.molecular-matters.com/2011/09/05/properly-handling-keyboard-input/ + scancode = if extended { 0xE000 } else { 0x0000 } | scancode; + let vkey = match vkey { + win32wm::VK_SHIFT => unsafe { MapVirtualKeyA(scancode, MAPVK_VSC_TO_VK_EX) as _ }, + win32wm::VK_CONTROL => { + if extended { + VK_RCONTROL + } else { + VK_LCONTROL + } + } + win32wm::VK_MENU => { + if extended { + VK_RMENU + } else { + VK_LMENU + } + } + _ => { + match scancode { + // When VK_PAUSE is pressed it emits a LeftControl + NumLock scancode event sequence, but reports VK_PAUSE + // as the virtual key on both events, or VK_PAUSE on the first event or 0xFF when using raw input. + // Don't emit anything for the LeftControl event in the pair... + 0xE01D if vkey == VK_PAUSE => return None, + // ...and emit the Pause event for the second event in the pair. + 0x45 if vkey == VK_PAUSE || vkey == 0xFF as _ => { + scancode = 0xE059; + VK_PAUSE + } + // VK_PAUSE has an incorrect vkey value when used with modifiers. VK_PAUSE also reports a different + // scancode when used with modifiers than when used without + 0xE046 => { + scancode = 0xE059; + VK_PAUSE + } + // VK_SCROLL has an incorrect vkey value when used with modifiers. + 0x46 => VK_SCROLL, + _ => vkey, + } + } + }; + Some((vkey, scancode)) +} + +pub fn process_key_params( + wparam: WPARAM, + lparam: LPARAM, +) -> Option<(ScanCode, Option)> { + let scancode = ((lparam >> 16) & 0xff) as UINT; + let extended = (lparam & 0x01000000) != 0; + handle_extended_keys(wparam as _, scancode, extended) + .map(|(vkey, scancode)| (scancode, vkey_to_tao_vkey(vkey))) +} + +// This is needed as windows doesn't properly distinguish +// some virtual key codes for different keyboard layouts +fn map_text_keys(win_virtual_key: i32) -> Option { + let char_key = unsafe { MapVirtualKeyA(win_virtual_key as u32, MAPVK_VK_TO_CHAR) } & 0x7FFF; + match char::from_u32(char_key) { + Some(';') => Some(VirtualKeyCode::Semicolon), + Some('/') => Some(VirtualKeyCode::Slash), + Some('`') => Some(VirtualKeyCode::Grave), + Some('[') => Some(VirtualKeyCode::LBracket), + Some(']') => Some(VirtualKeyCode::RBracket), + Some('\'') => Some(VirtualKeyCode::Apostrophe), + Some('\\') => Some(VirtualKeyCode::Backslash), + _ => None, + } +} diff --git a/vendor/tao/src/platform_impl/windows/event_loop.rs b/vendor/tao/src/platform_impl/windows/event_loop.rs new file mode 100644 index 0000000000..240ad1b5fd --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/event_loop.rs @@ -0,0 +1,2669 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![allow(non_snake_case)] + +mod runner; + +use crossbeam_channel::{self as channel, Receiver, Sender}; +use parking_lot::Mutex; +use std::{ + cell::Cell, + collections::VecDeque, + ffi::c_void, + marker::PhantomData, + mem, panic, + rc::Rc, + sync::Arc, + thread, + time::{Duration, Instant}, +}; +use windows::{ + core::{s, BOOL, PCWSTR}, + Win32::{ + Foundation::{ + HANDLE, HINSTANCE, HWND, LPARAM, LRESULT, POINT, RECT, TRUE, WAIT_TIMEOUT, WPARAM, + }, + Graphics::Gdi::*, + System::{ + LibraryLoader::GetModuleHandleW, + Ole::{IDropTarget, RevokeDragDrop}, + Threading::{GetCurrentThreadId, INFINITE}, + }, + UI::{ + Controls::{self as win32c, HOVER_DEFAULT}, + Input::{KeyboardAndMouse::*, Pointer::*, Touch::*, *}, + Shell::{ + DefSubclassProc, RemoveWindowSubclass, SHAppBarMessage, SetWindowSubclass, ABE_BOTTOM, + ABE_LEFT, ABE_RIGHT, ABE_TOP, ABM_GETAUTOHIDEBAR, APPBARDATA, + }, + WindowsAndMessaging::{self as win32wm, *}, + }, + }, +}; + +use crate::{ + dpi::{PhysicalPosition, PhysicalSize, PixelUnit}, + error::ExternalError, + event::{DeviceEvent, Event, Force, RawKeyEvent, Touch, TouchPhase, WindowEvent}, + event_loop::{ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW}, + keyboard::{KeyCode, ModifiersState}, + monitor::MonitorHandle as RootMonitorHandle, + platform_impl::platform::{ + dark_mode::try_window_theme, + dpi::{become_dpi_aware, dpi_to_scale_factor, enable_non_client_dpi_scaling}, + keyboard::is_msg_keyboard_related, + keyboard_layout::LAYOUT_CACHE, + minimal_ime::is_msg_ime_related, + monitor::{self, MonitorHandle}, + raw_input, util, + window::set_skip_taskbar, + window_state::{CursorFlags, WindowFlags, WindowState}, + wrap_device_id, WindowId, DEVICE_ID, + }, + window::{Fullscreen, Theme, WindowId as RootWindowId}, +}; +use runner::{EventLoopRunner, EventLoopRunnerShared}; + +use super::{dpi::hwnd_dpi, util::get_system_metrics_for_dpi}; + +// This is defined in `winuser.h` as a macro that expands to `UINT_MAX` +const WHEEL_PAGESCROLL: u32 = u32::MAX; +// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfoa#:~:text=SPI_GETWHEELSCROLLLINES +const DEFAULT_SCROLL_LINES_PER_WHEEL_DELTA: isize = 3; +// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfoa#:~:text=SPI_GETWHEELSCROLLCHARS +const DEFAULT_SCROLL_CHARACTERS_PER_WHEEL_DELTA: isize = 3; + +type GetPointerFrameInfoHistory = unsafe extern "system" fn( + pointerId: u32, + entriesCount: *mut u32, + pointerCount: *mut u32, + pointerInfo: *mut POINTER_INFO, +) -> BOOL; + +type SkipPointerFrameMessages = unsafe extern "system" fn(pointerId: u32) -> BOOL; + +type GetPointerDeviceRects = unsafe extern "system" fn( + device: HANDLE, + pointerDeviceRect: *mut RECT, + displayRect: *mut RECT, +) -> BOOL; + +type GetPointerTouchInfo = + unsafe extern "system" fn(pointerId: u32, touchInfo: *mut POINTER_TOUCH_INFO) -> BOOL; + +type GetPointerPenInfo = + unsafe extern "system" fn(pointId: u32, penInfo: *mut POINTER_PEN_INFO) -> BOOL; + +lazy_static! { + static ref GET_POINTER_FRAME_INFO_HISTORY: Option = + get_function!("user32.dll", GetPointerFrameInfoHistory); + static ref SKIP_POINTER_FRAME_MESSAGES: Option = + get_function!("user32.dll", SkipPointerFrameMessages); + static ref GET_POINTER_DEVICE_RECTS: Option = + get_function!("user32.dll", GetPointerDeviceRects); + static ref GET_POINTER_TOUCH_INFO: Option = + get_function!("user32.dll", GetPointerTouchInfo); + static ref GET_POINTER_PEN_INFO: Option = + get_function!("user32.dll", GetPointerPenInfo); +} + +pub(crate) struct SubclassInput { + pub window_state: Arc>, + pub event_loop_runner: EventLoopRunnerShared, + pub _file_drop_handler: Option, + pub subclass_removed: Cell, + pub recurse_depth: Cell, + pub event_loop_preferred_theme: Arc>>, +} + +impl SubclassInput { + unsafe fn send_event(&self, event: Event<'_, T>) { + self.event_loop_runner.send_event(event); + } +} + +struct ThreadMsgTargetSubclassInput { + event_loop_runner: EventLoopRunnerShared, + user_event_receiver: Receiver, +} + +impl ThreadMsgTargetSubclassInput { + unsafe fn send_event(&self, event: Event<'_, T>) { + self.event_loop_runner.send_event(event); + } +} + +/// The result of a subclass procedure (the message handling callback) +pub(crate) enum ProcResult { + DefSubclassProc, // <- this should be the default value + DefWindowProc, + Value(LRESULT), +} + +pub struct EventLoop { + thread_msg_sender: Sender, + window_target: RootELW, + msg_hook: Option bool + 'static>>, +} + +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) any_thread: bool, + pub(crate) dpi_aware: bool, + pub(crate) msg_hook: Option bool + 'static>>, + pub(crate) preferred_theme: Option, +} + +impl Default for PlatformSpecificEventLoopAttributes { + fn default() -> Self { + Self { + any_thread: false, + dpi_aware: true, + msg_hook: None, + preferred_theme: None, + } + } +} + +#[derive(Clone)] +pub struct EventLoopWindowTarget { + thread_id: u32, + thread_msg_target: HWND, + pub(crate) preferred_theme: Arc>>, + pub(crate) runner_shared: EventLoopRunnerShared, +} + +impl EventLoop { + pub(crate) fn new(attributes: &mut PlatformSpecificEventLoopAttributes) -> EventLoop { + let thread_id = unsafe { GetCurrentThreadId() }; + + if !attributes.any_thread && thread_id != main_thread_id() { + panic!( + "Initializing the event loop outside of the main thread is a significant \ + cross-platform compatibility hazard. If you absolutely need to create an \ + EventLoop on a different thread, you can use the \ + `EventLoopBuilderExtWindows::any_thread` function." + ); + } + + if attributes.dpi_aware { + become_dpi_aware(); + } + + let thread_msg_target = create_event_target_window(); + + super::dark_mode::allow_dark_mode_for_app(true); + + let send_thread_msg_target = thread_msg_target.0 as isize; + thread::spawn(move || wait_thread(thread_id, HWND(send_thread_msg_target as _))); + let wait_thread_id = get_wait_thread_id(); + + let runner_shared = Rc::new(EventLoopRunner::new(thread_msg_target, wait_thread_id)); + + let thread_msg_sender = subclass_event_target_window(thread_msg_target, runner_shared.clone()); + raw_input::register_all_mice_and_keyboards_for_raw_input(thread_msg_target, Default::default()); + + EventLoop { + thread_msg_sender, + window_target: RootELW { + p: EventLoopWindowTarget { + thread_id, + thread_msg_target, + runner_shared, + preferred_theme: Arc::new(Mutex::new(attributes.preferred_theme)), + }, + _marker: PhantomData, + }, + msg_hook: attributes.msg_hook.take(), + } + } + + pub fn window_target(&self) -> &RootELW { + &self.window_target + } + + pub fn run(mut self, event_handler: F) -> ! + where + F: 'static + FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), + { + let exit_code = self.run_return(event_handler); + ::std::process::exit(exit_code); + } + + pub fn run_return(&mut self, mut event_handler: F) -> i32 + where + F: FnMut(Event<'_, T>, &RootELW, &mut ControlFlow), + { + let event_loop_windows_ref = &self.window_target; + + unsafe { + self + .window_target + .p + .runner_shared + .set_event_handler(move |event, control_flow| { + event_handler(event, event_loop_windows_ref, control_flow); + }); + } + + let runner = &self.window_target.p.runner_shared; + + let exit_code = unsafe { + let mut msg = MSG::default(); + + runner.poll(); + 'main: loop { + if !GetMessageW(&mut msg, None, 0, 0).as_bool() { + break 'main 0; + } + + let handled = if let Some(callback) = self.msg_hook.as_deref_mut() { + callback(&mut msg as *mut _ as *mut _) + } else { + false + }; + if !handled { + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + } + + if let Err(payload) = runner.take_panic_error() { + runner.reset_runner(); + panic::resume_unwind(payload); + } + + if let ControlFlow::ExitWithCode(code) = runner.control_flow() { + if !runner.handling_events() { + break 'main code; + } + } + } + }; + + unsafe { + runner.loop_destroyed(); + } + runner.reset_runner(); + exit_code + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + target_window: self.window_target.p.thread_msg_target, + event_send: self.thread_msg_sender.clone(), + } + } +} + +impl EventLoopWindowTarget { + #[inline(always)] + pub(crate) fn create_thread_executor(&self) -> EventLoopThreadExecutor { + EventLoopThreadExecutor { + thread_id: self.thread_id, + target_window: self.thread_msg_target, + } + } + + // TODO: Investigate opportunities for caching + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(RootMonitorHandle { inner: monitor }) + } + + pub fn monitor_from_point(&self, x: f64, y: f64) -> Option { + monitor::from_point(x, y) + } + + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Windows(rwh_05::WindowsDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06(&self) -> Result { + Ok(rwh_06::RawDisplayHandle::Windows( + rwh_06::WindowsDisplayHandle::new(), + )) + } + + pub fn set_device_event_filter(&self, filter: DeviceEventFilter) { + raw_input::register_all_mice_and_keyboards_for_raw_input(self.thread_msg_target, filter); + } + + #[inline] + pub fn cursor_position(&self) -> Result, ExternalError> { + util::cursor_position().map_err(Into::into) + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + *self.preferred_theme.lock() = theme; + self.runner_shared.owned_windows(|window| { + let _ = unsafe { SendMessageW(window, *CHANGE_THEME_MSG_ID, None, None) }; + }); + } +} + +fn main_thread_id() -> u32 { + static mut MAIN_THREAD_ID: u32 = 0; + #[used] + #[allow(non_upper_case_globals)] + #[link_section = ".CRT$XCU"] + static INIT_MAIN_THREAD_ID: unsafe fn() = { + unsafe fn initer() { + MAIN_THREAD_ID = GetCurrentThreadId(); + } + initer + }; + + unsafe { MAIN_THREAD_ID } +} + +fn get_wait_thread_id() -> u32 { + unsafe { + let mut msg = MSG::default(); + let result = GetMessageW( + &mut msg, + None, + *SEND_WAIT_THREAD_ID_MSG_ID, + *SEND_WAIT_THREAD_ID_MSG_ID, + ); + assert_eq!( + msg.message, *SEND_WAIT_THREAD_ID_MSG_ID, + "this shouldn't be possible. please open an issue with Tauri. error code: {}", + result.0 + ); + msg.lParam.0 as u32 + } +} + +fn wait_thread(parent_thread_id: u32, msg_window_id: HWND) { + unsafe { + let mut msg: MSG; + + let cur_thread_id = GetCurrentThreadId(); + let _ = PostThreadMessageW( + parent_thread_id, + *SEND_WAIT_THREAD_ID_MSG_ID, + WPARAM(0), + LPARAM(cur_thread_id as _), + ); + + let mut wait_until_opt = None; + 'main: loop { + // Zeroing out the message ensures that the `WaitUntilInstantBox` doesn't get + // double-freed if `MsgWaitForMultipleObjectsEx` returns early and there aren't + // additional messages to process. + msg = MSG::default(); + + if wait_until_opt.is_some() { + if PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } else if !GetMessageW(&mut msg, None, 0, 0).as_bool() { + break 'main; + } else { + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + } + + if msg.message == *WAIT_UNTIL_MSG_ID { + wait_until_opt = Some(*WaitUntilInstantBox::from_raw(msg.lParam.0 as *mut _)); + } else if msg.message == *CANCEL_WAIT_UNTIL_MSG_ID { + wait_until_opt = None; + } + + if let Some(wait_until) = wait_until_opt { + let now = Instant::now(); + if now < wait_until { + // MsgWaitForMultipleObjects tends to overshoot just a little bit. We subtract + // 1 millisecond from the requested time and spinlock for the remainder to + // compensate for that. + let resume_reason = MsgWaitForMultipleObjectsEx( + None, + dur2timeout(wait_until - now).saturating_sub(1), + QS_ALLEVENTS, + MWMO_INPUTAVAILABLE, + ); + if resume_reason == WAIT_TIMEOUT { + let _ = PostMessageW( + Some(msg_window_id), + *PROCESS_NEW_EVENTS_MSG_ID, + WPARAM(0), + LPARAM(0), + ); + wait_until_opt = None; + } + } else { + let _ = PostMessageW( + Some(msg_window_id), + *PROCESS_NEW_EVENTS_MSG_ID, + WPARAM(0), + LPARAM(0), + ); + wait_until_opt = None; + } + } + } + } +} + +// Implementation taken from https://github.com/rust-lang/rust/blob/db5476571d9b27c862b95c1e64764b0ac8980e23/src/libstd/sys/windows/mod.rs +fn dur2timeout(dur: Duration) -> u32 { + // Note that a duration is a (u64, u32) (seconds, nanoseconds) pair, and the + // timeouts in windows APIs are typically u32 milliseconds. To translate, we + // have two pieces to take care of: + // + // * Nanosecond precision is rounded up + // * Greater than u32::MAX milliseconds (50 days) is rounded up to INFINITE + // (never time out). + dur + .as_secs() + .checked_mul(1000) + .and_then(|ms| ms.checked_add((dur.subsec_nanos() as u64) / 1_000_000)) + .and_then(|ms| { + ms.checked_add(if dur.subsec_nanos() % 1_000_000 > 0 { + 1 + } else { + 0 + }) + }) + .map(|ms| { + if ms > u32::MAX as u64 { + INFINITE + } else { + ms as u32 + } + }) + .unwrap_or(INFINITE) +} + +impl Drop for EventLoop { + fn drop(&mut self) { + unsafe { + let _ = DestroyWindow(self.window_target.p.thread_msg_target); + } + } +} + +pub(crate) struct EventLoopThreadExecutor { + thread_id: u32, + target_window: HWND, +} + +unsafe impl Send for EventLoopThreadExecutor {} +unsafe impl Sync for EventLoopThreadExecutor {} + +impl EventLoopThreadExecutor { + /// Check to see if we're in the parent event loop's thread. + pub(super) fn in_event_loop_thread(&self) -> bool { + let cur_thread_id = unsafe { GetCurrentThreadId() }; + self.thread_id == cur_thread_id + } + + /// Executes a function in the event loop thread. If we're already in the event loop thread, + /// we just call the function directly. + /// + /// The `Inserted` can be used to inject a `WindowState` for the callback to use. The state is + /// removed automatically if the callback receives a `WM_CLOSE` message for the window. + /// + /// Note that if you are using this to change some property of a window and updating + /// `WindowState` then you should call this within the lock of `WindowState`. Otherwise the + /// events may be sent to the other thread in different order to the one in which you set + /// `WindowState`, leaving them out of sync. + /// + /// Note that we use a FnMut instead of a FnOnce because we're too lazy to create an equivalent + /// to the unstable FnBox. + pub(super) fn execute_in_thread(&self, mut function: F) + where + F: FnMut() + Send + 'static, + { + unsafe { + if self.in_event_loop_thread() { + function(); + } else { + // We double-box because the first box is a fat pointer. + let boxed = Box::new(function) as Box; + let boxed2: ThreadExecFn = Box::new(boxed); + + let raw = Box::into_raw(boxed2); + + let res = PostMessageW( + Some(self.target_window), + *EXEC_MSG_ID, + WPARAM(raw as _), + LPARAM(0), + ); + assert!( + res.is_ok(), + "PostMessage failed ; is the messages queue full?" + ); + } + } + } +} + +type ThreadExecFn = Box>; + +pub struct EventLoopProxy { + target_window: HWND, + event_send: Sender, +} +unsafe impl Send for EventLoopProxy {} +unsafe impl Sync for EventLoopProxy {} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { + target_window: self.target_window, + event_send: self.event_send.clone(), + } + } +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + unsafe { + if PostMessageW( + Some(self.target_window), + *USER_EVENT_MSG_ID, + WPARAM(0), + LPARAM(0), + ) + .is_ok() + { + self.event_send.send(event).ok(); + Ok(()) + } else { + Err(EventLoopClosed(event)) + } + } + } +} + +type WaitUntilInstantBox = Box; + +lazy_static! { + /// Message sent by the `EventLoopProxy` when we want to wake up the thread. + /// WPARAM and LPARAM are unused. + static ref USER_EVENT_MSG_ID: u32 = { + unsafe { + RegisterWindowMessageA(s!("Tao::WakeupMsg")) + } + }; + /// Message sent when we want to execute a closure in the thread. + /// WPARAM contains a Box> that must be retrieved with `Box::from_raw`, + /// and LPARAM is unused. + static ref EXEC_MSG_ID: u32 = { + unsafe { + RegisterWindowMessageA(s!("Tao::ExecMsg")) + } + }; + static ref PROCESS_NEW_EVENTS_MSG_ID: u32 = { + unsafe { + RegisterWindowMessageA(s!("Tao::ProcessNewEvents")) + } + }; + /// lparam is the wait thread's message id. + static ref SEND_WAIT_THREAD_ID_MSG_ID: u32 = { + unsafe { + RegisterWindowMessageA(s!("Tao::SendWaitThreadId")) + } + }; + /// lparam points to a `Box` signifying the time `PROCESS_NEW_EVENTS_MSG_ID` should + /// be sent. + static ref WAIT_UNTIL_MSG_ID: u32 = { + unsafe { + RegisterWindowMessageA(s!("Tao::WaitUntil")) + } + }; + static ref CANCEL_WAIT_UNTIL_MSG_ID: u32 = { + unsafe { + RegisterWindowMessageA(s!("Tao::CancelWaitUntil")) + } + }; + /// Message sent by a `Window` when it wants to be destroyed by the main thread. + /// WPARAM and LPARAM are unused. + pub static ref DESTROY_MSG_ID: u32 = { + unsafe { + RegisterWindowMessageA(s!("Tao::DestroyMsg")) + } + }; + /// WPARAM is a bool specifying the `WindowFlags::MARKER_RETAIN_STATE_ON_SIZE` flag. See the + /// documentation in the `window_state` module for more information. + pub static ref SET_RETAIN_STATE_ON_SIZE_MSG_ID: u32 = unsafe { + RegisterWindowMessageA(s!("Tao::SetRetainMaximized")) + }; + /// Message sent by event loop when event loop's prefered theme changed. + /// WPARAM and LPARAM are unused. + pub static ref CHANGE_THEME_MSG_ID: u32 = unsafe { + RegisterWindowMessageA(s!("Tao::ChangeTheme")) + }; + /// When the taskbar is created, it registers a message with the "TaskbarCreated" string and then broadcasts this message to all top-level windows + /// When the application receives this message, it should assume that any taskbar icons it added have been removed and add them again. + pub static ref S_U_TASKBAR_RESTART: u32 = unsafe { + RegisterWindowMessageA(s!("TaskbarCreated")) + }; + static ref THREAD_EVENT_TARGET_WINDOW_CLASS: Vec = unsafe { + let class_name = util::encode_wide("Tao Thread Event Target"); + + let class = WNDCLASSEXW { + cbSize: mem::size_of::() as u32, + style: Default::default(), + lpfnWndProc: Some(util::call_default_window_proc), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: HINSTANCE(GetModuleHandleW(PCWSTR::null()).unwrap_or_default().0), + hIcon: HICON::default(), + hCursor: HCURSOR::default(), // must be null in order for cursor state to work properly + hbrBackground: HBRUSH::default(), + lpszMenuName: PCWSTR::null(), + lpszClassName: PCWSTR::from_raw(class_name.as_ptr()), + hIconSm: HICON::default(), + }; + + RegisterClassExW(&class); + + class_name + }; +} + +fn create_event_target_window() -> HWND { + let window = unsafe { + CreateWindowExW( + WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED | + // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which + // we want to avoid. If you remove this style, this window won't show up in the + // taskbar *initially*, but it can show up at some later point. This can sometimes + // happen on its own after several hours have passed, although this has proven + // difficult to reproduce. Alternatively, it can be manually triggered by killing + // `explorer.exe` and then starting the process back up. + // It is unclear why the bug is triggered by waiting for several hours. + WS_EX_TOOLWINDOW, + PCWSTR::from_raw(THREAD_EVENT_TARGET_WINDOW_CLASS.clone().as_ptr()), + PCWSTR::null(), + WS_OVERLAPPED, + 0, + 0, + 0, + 0, + None, + None, + GetModuleHandleW(PCWSTR::null()).map(Into::into).ok(), + None, + ) + }; + + let window = match window { + Ok(w) => w, + Err(_) => return HWND::default(), + }; + + util::SetWindowLongPtrW( + window, + GWL_STYLE, + // The window technically has to be visible to receive WM_PAINT messages (which are used + // for delivering events during resizes), but it isn't displayed to the user because of + // the LAYERED style. + (WS_VISIBLE | WS_POPUP).0 as isize, + ); + window +} + +fn subclass_event_target_window( + window: HWND, + event_loop_runner: EventLoopRunnerShared, +) -> Sender { + unsafe { + let (tx, rx) = channel::unbounded(); + + let subclass_input = ThreadMsgTargetSubclassInput { + event_loop_runner, + user_event_receiver: rx, + }; + let input_ptr = Box::into_raw(Box::new(subclass_input)); + let subclass_result = SetWindowSubclass( + window, + Some(thread_event_target_callback::), + THREAD_EVENT_TARGET_SUBCLASS_ID, + input_ptr as usize, + ); + assert!(subclass_result.as_bool()); + + tx + } +} + +fn remove_event_target_window_subclass(window: HWND) { + let removal_result = unsafe { + RemoveWindowSubclass( + window, + Some(thread_event_target_callback::), + THREAD_EVENT_TARGET_SUBCLASS_ID, + ) + }; + assert!(removal_result.as_bool()); +} + +/// Capture mouse input, allowing `window` to receive mouse events when the cursor is outside of +/// the window. +unsafe fn capture_mouse(window: HWND, window_state: &mut WindowState) { + window_state.mouse.capture_count += 1; + SetCapture(window); +} + +/// Release mouse input, stopping windows on this thread from receiving mouse input when the cursor +/// is outside the window. +unsafe fn release_mouse(mut window_state: parking_lot::MutexGuard<'_, WindowState>) { + window_state.mouse.capture_count = window_state.mouse.capture_count.saturating_sub(1); + if window_state.mouse.capture_count == 0 { + // ReleaseCapture() causes a WM_CAPTURECHANGED where we lock the window_state. + drop(window_state); + let _ = ReleaseCapture(); + } +} + +const WINDOW_SUBCLASS_ID: usize = 0; +const THREAD_EVENT_TARGET_SUBCLASS_ID: usize = 1; +pub(crate) fn subclass_window(window: HWND, subclass_input: SubclassInput) { + subclass_input.event_loop_runner.register_window(window); + let input_ptr = Box::into_raw(Box::new(subclass_input)); + let subclass_result = unsafe { + SetWindowSubclass( + window, + Some(public_window_callback::), + WINDOW_SUBCLASS_ID, + input_ptr as usize, + ) + }; + assert!(subclass_result.as_bool()); +} + +fn remove_window_subclass(window: HWND) { + let removal_result = unsafe { + RemoveWindowSubclass( + window, + Some(public_window_callback::), + WINDOW_SUBCLASS_ID, + ) + }; + assert!(removal_result.as_bool()); +} + +fn normalize_pointer_pressure(pressure: u32) -> Option { + match pressure { + 1..=1024 => Some(Force::Normalized(pressure as f64 / 1024.0)), + _ => None, + } +} + +/// Flush redraw events for Tao's windows. +/// +/// Tao's API guarantees that all redraw events will be clustered together and dispatched all at +/// once, but the standard Windows message loop doesn't always exhibit that behavior. If multiple +/// windows have had redraws scheduled, but an input event is pushed to the message queue between +/// the `WM_PAINT` call for the first window and the `WM_PAINT` call for the second window, Windows +/// will dispatch the input event immediately instead of flushing all the redraw events. This +/// function explicitly pulls all of Tao's redraw events out of the event queue so that they +/// always all get processed in one fell swoop. +/// +/// Returns `true` if this invocation flushed all the redraw events. If this function is re-entrant, +/// it won't flush the redraw events and will return `false`. +#[must_use] +unsafe fn flush_paint_messages( + except: Option, + runner: &EventLoopRunner, +) -> bool { + if !runner.redrawing() { + runner.main_events_cleared(); + let mut msg = MSG::default(); + runner.owned_windows(|redraw_window| { + if Some(redraw_window) == except { + return; + } + + if !PeekMessageW( + &mut msg, + Some(redraw_window), + WM_PAINT, + WM_PAINT, + PM_REMOVE | PM_QS_PAINT, + ) + .as_bool() + { + return; + } + + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + }); + true + } else { + false + } +} + +unsafe fn process_control_flow(runner: &EventLoopRunner) { + match runner.control_flow() { + ControlFlow::Poll => { + let _ = PostMessageW( + Some(runner.thread_msg_target()), + *PROCESS_NEW_EVENTS_MSG_ID, + WPARAM(0), + LPARAM(0), + ); + } + ControlFlow::Wait => (), + ControlFlow::WaitUntil(until) => { + let _ = PostThreadMessageW( + runner.wait_thread_id(), + *WAIT_UNTIL_MSG_ID, + WPARAM(0), + LPARAM(Box::into_raw(WaitUntilInstantBox::new(until)) as _), + ); + } + ControlFlow::ExitWithCode(_) => (), + } +} + +/// Emit a `ModifiersChanged` event whenever modifiers have changed. +/// Returns the current modifier state +fn update_modifiers(window: HWND, subclass_input: &SubclassInput) -> ModifiersState { + use crate::event::WindowEvent::ModifiersChanged; + + let modifiers = LAYOUT_CACHE.lock().get_agnostic_mods(); + let mut window_state = subclass_input.window_state.lock(); + if window_state.modifiers_state != modifiers { + window_state.modifiers_state = modifiers; + + // Drop lock + drop(window_state); + + unsafe { + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: ModifiersChanged(modifiers), + }); + } + } + modifiers +} + +unsafe fn gain_active_focus(window: HWND, subclass_input: &SubclassInput) { + use crate::event::WindowEvent::Focused; + update_modifiers(window, subclass_input); + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: Focused(true), + }); +} + +unsafe fn lose_active_focus(window: HWND, subclass_input: &SubclassInput) { + use crate::event::WindowEvent::{Focused, ModifiersChanged}; + + subclass_input.window_state.lock().modifiers_state = ModifiersState::empty(); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: ModifiersChanged(ModifiersState::empty()), + }); + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: Focused(false), + }); +} + +/// Any window whose callback is configured to this function will have its events propagated +/// through the events loop of the thread the window was created in. +// +// This is the callback that is called by `DispatchMessage` in the events loop. +// +// Returning 0 tells the Win32 API that the message has been processed. +// FIXME: detect WM_DWMCOMPOSITIONCHANGED and call DwmEnableBlurBehindWindow if necessary +unsafe extern "system" fn public_window_callback( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + uidsubclass: usize, + subclass_input_ptr: usize, +) -> LRESULT { + let subclass_input_ptr = subclass_input_ptr as *mut SubclassInput; + let (result, subclass_removed, recurse_depth) = { + let subclass_input = &*subclass_input_ptr; + subclass_input + .recurse_depth + .set(subclass_input.recurse_depth.get() + 1); + + // Clear userdata + util::SetWindowLongPtrW(window, GWL_USERDATA, 0); + + let result = + public_window_callback_inner(window, msg, wparam, lparam, uidsubclass, subclass_input); + + let subclass_removed = subclass_input.subclass_removed.get(); + let recurse_depth = subclass_input.recurse_depth.get() - 1; + subclass_input.recurse_depth.set(recurse_depth); + + (result, subclass_removed, recurse_depth) + }; + + if subclass_removed && recurse_depth == 0 { + drop(Box::from_raw(subclass_input_ptr)) + } + + result +} + +unsafe fn public_window_callback_inner( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + _: usize, + subclass_input: &SubclassInput, +) -> LRESULT { + let _ = RedrawWindow( + Some(subclass_input.event_loop_runner.thread_msg_target()), + None, + None, + RDW_INTERNALPAINT, + ); + + let mut result = ProcResult::DefSubclassProc; + + // Send new modifiers before sending key events. + let mods_changed_callback = || match msg { + win32wm::WM_KEYDOWN | win32wm::WM_SYSKEYDOWN | win32wm::WM_KEYUP | win32wm::WM_SYSKEYUP => { + update_modifiers(window, subclass_input); + result = ProcResult::Value(LRESULT(0)); + } + _ => (), + }; + subclass_input + .event_loop_runner + .catch_unwind(mods_changed_callback) + .unwrap_or_else(|| result = ProcResult::Value(LRESULT(-1))); + + let keyboard_callback = || { + use crate::event::WindowEvent::KeyboardInput; + let is_keyboard_related = is_msg_keyboard_related(msg); + if !is_keyboard_related { + // We return early to avoid a deadlock from locking the window state + // when not appropriate. + return; + } + let events = { + let mut key_event_builders = + crate::platform_impl::platform::keyboard::KEY_EVENT_BUILDERS.lock(); + if let Some(key_event_builder) = key_event_builders.get_mut(&WindowId(window.0 as _)) { + key_event_builder.process_message(window, msg, wparam, lparam, &mut result) + } else { + Vec::new() + } + }; + for event in events { + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: KeyboardInput { + device_id: DEVICE_ID, + event: event.event, + is_synthetic: event.is_synthetic, + }, + }); + } + }; + subclass_input + .event_loop_runner + .catch_unwind(keyboard_callback) + .unwrap_or_else(|| result = ProcResult::Value(LRESULT(-1))); + + let ime_callback = || { + use crate::event::WindowEvent::ReceivedImeText; + let is_ime_related = is_msg_ime_related(msg); + if !is_ime_related { + return; + } + let text = { + let mut window_state = subclass_input.window_state.lock(); + window_state + .ime_handler + .process_message(window, msg, wparam, lparam, &mut result) + }; + if let Some(str) = text { + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: ReceivedImeText(str), + }); + } + }; + subclass_input + .event_loop_runner + .catch_unwind(ime_callback) + .unwrap_or_else(|| result = ProcResult::Value(LRESULT(-1))); + + // I decided to bind the closure to `callback` and pass it to catch_unwind rather than passing + // the closure to catch_unwind directly so that the match body indendation wouldn't change and + // the git blame and history would be preserved. + let callback = || match msg { + win32wm::WM_ENTERSIZEMOVE => { + subclass_input + .window_state + .lock() + .set_window_flags_in_place(|f| f.insert(WindowFlags::MARKER_IN_SIZE_MOVE)); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_EXITSIZEMOVE => { + let mut state = subclass_input.window_state.lock(); + if state.dragging { + state.dragging = false; + let _ = unsafe { PostMessageW(Some(window), WM_LBUTTONUP, WPARAM::default(), lparam) }; + } + state.set_window_flags_in_place(|f| f.remove(WindowFlags::MARKER_IN_SIZE_MOVE)); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_NCCREATE => { + enable_non_client_dpi_scaling(window); + } + win32wm::WM_NCLBUTTONDOWN => { + if wparam.0 == HTCAPTION as _ { + let _ = PostMessageW(Some(window), WM_MOUSEMOVE, WPARAM(0), lparam); + } + + use crate::event::WindowEvent::DecorationsClick; + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: DecorationsClick, + }); + } + + win32wm::WM_CLOSE => { + use crate::event::WindowEvent::CloseRequested; + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: CloseRequested, + }); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_DESTROY => { + use crate::event::WindowEvent::Destroyed; + let _ = RevokeDragDrop(window); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: Destroyed, + }); + subclass_input.event_loop_runner.remove_window(window); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_NCDESTROY => { + remove_window_subclass::(window); + subclass_input.subclass_removed.set(true); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_PAINT => { + if subclass_input.event_loop_runner.should_buffer() { + // this branch can happen in response to `UpdateWindow`, if win32 decides to + // redraw the window outside the normal flow of the event loop. + let _ = RedrawWindow(Some(window), None, None, RDW_INTERNALPAINT); + } else { + let managing_redraw = flush_paint_messages(Some(window), &subclass_input.event_loop_runner); + subclass_input.send_event(Event::RedrawRequested(RootWindowId(WindowId( + window.0 as _, + )))); + if managing_redraw { + subclass_input.event_loop_runner.redraw_events_cleared(); + process_control_flow(&subclass_input.event_loop_runner); + } + } + } + + win32wm::WM_ERASEBKGND => { + let w = subclass_input.window_state.lock(); + if let Some(color) = w.background_color { + let hdc = HDC(wparam.0 as *mut _); + let mut rc = RECT::default(); + if GetClientRect(window, &mut rc).is_ok() { + let brush = CreateSolidBrush(util::RGB(color.0, color.1, color.2)); + FillRect(hdc, &rc, brush); + let _ = DeleteObject(brush.into()); + + result = ProcResult::Value(LRESULT(1)); + } else { + result = ProcResult::DefSubclassProc; + } + } else { + result = ProcResult::DefSubclassProc; + } + } + + win32wm::WM_WINDOWPOSCHANGING => { + let mut window_state = subclass_input.window_state.lock(); + + if let Some(ref mut fullscreen) = window_state.fullscreen { + let window_pos = &mut *(lparam.0 as *mut WINDOWPOS); + let new_rect = RECT { + left: window_pos.x, + top: window_pos.y, + right: window_pos.x + window_pos.cx, + bottom: window_pos.y + window_pos.cy, + }; + + const NOMOVE_OR_NOSIZE: SET_WINDOW_POS_FLAGS = + SET_WINDOW_POS_FLAGS(SWP_NOMOVE.0 | SWP_NOSIZE.0); + + let new_rect = if (window_pos.flags & NOMOVE_OR_NOSIZE) != SET_WINDOW_POS_FLAGS::default() { + let cur_rect = util::get_window_rect(window) + .expect("Unexpected GetWindowRect failure; please report this error to tauri-apps/tao on GitHub"); + + match window_pos.flags & NOMOVE_OR_NOSIZE { + NOMOVE_OR_NOSIZE => None, + + SWP_NOMOVE => Some(RECT { + left: cur_rect.left, + top: cur_rect.top, + right: cur_rect.left + window_pos.cx, + bottom: cur_rect.top + window_pos.cy, + }), + + SWP_NOSIZE => Some(RECT { + left: window_pos.x, + top: window_pos.y, + right: window_pos.x - cur_rect.left + cur_rect.right, + bottom: window_pos.y - cur_rect.top + cur_rect.bottom, + }), + + _ => unreachable!(), + } + } else { + Some(new_rect) + }; + + if let Some(new_rect) = new_rect { + let new_monitor = MonitorFromRect(&new_rect, MONITOR_DEFAULTTONULL); + match fullscreen { + Fullscreen::Borderless(ref mut fullscreen_monitor) => { + if !new_monitor.is_invalid() + && fullscreen_monitor + .as_ref() + .map(|monitor| new_monitor != monitor.inner.hmonitor()) + .unwrap_or(true) + { + if let Ok(new_monitor_info) = monitor::get_monitor_info(new_monitor) { + let new_monitor_rect = new_monitor_info.monitorInfo.rcMonitor; + window_pos.x = new_monitor_rect.left; + window_pos.y = new_monitor_rect.top; + window_pos.cx = new_monitor_rect.right - new_monitor_rect.left; + window_pos.cy = new_monitor_rect.bottom - new_monitor_rect.top; + } + *fullscreen_monitor = Some(crate::monitor::MonitorHandle { + inner: MonitorHandle::new(new_monitor), + }); + } + } + Fullscreen::Exclusive(ref video_mode) => { + let old_monitor = video_mode.video_mode.monitor.hmonitor(); + if let Ok(old_monitor_info) = monitor::get_monitor_info(old_monitor) { + let old_monitor_rect = old_monitor_info.monitorInfo.rcMonitor; + window_pos.x = old_monitor_rect.left; + window_pos.y = old_monitor_rect.top; + window_pos.cx = old_monitor_rect.right - old_monitor_rect.left; + window_pos.cy = old_monitor_rect.bottom - old_monitor_rect.top; + } + } + } + } + } + + let window_flags = window_state.window_flags; + if window_flags.contains(WindowFlags::ALWAYS_ON_BOTTOM) { + let window_pos = &mut *(lparam.0 as *mut WINDOWPOS); + window_pos.hwndInsertAfter = HWND_BOTTOM; + } + + result = ProcResult::Value(LRESULT(0)); + } + + // WM_MOVE supplies client area positions, so we send Moved here instead. + win32wm::WM_WINDOWPOSCHANGED => { + use crate::event::WindowEvent::Moved; + + let windowpos = lparam.0 as *const WINDOWPOS; + if (*windowpos).flags & SWP_NOMOVE != SWP_NOMOVE { + let physical_position = PhysicalPosition::new((*windowpos).x, (*windowpos).y); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: Moved(physical_position), + }); + } + + // This is necessary for us to still get sent WM_SIZE. + result = ProcResult::DefSubclassProc; + } + + win32wm::WM_SIZE => { + use crate::event::WindowEvent::Resized; + let w = u32::from(util::LOWORD(lparam.0 as u32)); + let h = u32::from(util::HIWORD(lparam.0 as u32)); + + let physical_size = PhysicalSize::new(w, h); + let event = Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: Resized(physical_size), + }; + + { + let mut w = subclass_input.window_state.lock(); + // See WindowFlags::MARKER_RETAIN_STATE_ON_SIZE docs for info on why this `if` check exists. + if !w + .window_flags() + .contains(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE) + { + let maximized = wparam.0 == win32wm::SIZE_MAXIMIZED as _; + w.set_window_flags_in_place(|f| f.set(WindowFlags::MAXIMIZED, maximized)); + } + } + + subclass_input.send_event(event); + result = ProcResult::Value(LRESULT(0)); + } + + // this is necessary for us to maintain minimize/restore state + win32wm::WM_SYSCOMMAND => { + if wparam.0 == SC_RESTORE as _ { + let mut w = subclass_input.window_state.lock(); + w.set_window_flags_in_place(|f| f.set(WindowFlags::MINIMIZED, false)); + } + if wparam.0 == SC_MINIMIZE as _ { + let mut w = subclass_input.window_state.lock(); + w.set_window_flags_in_place(|f| f.set(WindowFlags::MINIMIZED, true)); + } + // Send `WindowEvent::Minimized` here if we decide to implement one + + if wparam.0 == SC_SCREENSAVE as _ { + let window_state = subclass_input.window_state.lock(); + if window_state.fullscreen.is_some() { + result = ProcResult::Value(LRESULT(0)); + return; + } + } + + result = ProcResult::DefWindowProc; + } + + win32wm::WM_MOUSEMOVE => { + use crate::event::WindowEvent::{CursorEntered, CursorMoved}; + let mouse_was_outside_window = { + let mut w = subclass_input.window_state.lock(); + + let was_outside_window = !w.mouse.cursor_flags().contains(CursorFlags::IN_WINDOW); + w.mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, true)) + .ok(); + was_outside_window + }; + + if mouse_was_outside_window { + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: CursorEntered { + device_id: DEVICE_ID, + }, + }); + + // Calling TrackMouseEvent in order to receive mouse leave events. + let _ = TrackMouseEvent(&mut TRACKMOUSEEVENT { + cbSize: mem::size_of::() as u32, + dwFlags: TME_LEAVE, + hwndTrack: window, + dwHoverTime: HOVER_DEFAULT, + }); + } + + let x = f64::from(util::GET_X_LPARAM(lparam)); + let y = f64::from(util::GET_Y_LPARAM(lparam)); + let position = PhysicalPosition::new(x, y); + let cursor_moved; + { + // handle spurious WM_MOUSEMOVE messages + // see https://devblogs.microsoft.com/oldnewthing/20031001-00/?p=42343 + // and http://debugandconquer.blogspot.com/2015/08/the-cause-of-spurious-mouse-move.html + let mut w = subclass_input.window_state.lock(); + cursor_moved = w.mouse.last_position != Some(position); + w.mouse.last_position = Some(position); + } + if cursor_moved { + let modifiers = update_modifiers(window, subclass_input); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: CursorMoved { + device_id: DEVICE_ID, + position, + modifiers, + }, + }); + } + + result = ProcResult::Value(LRESULT(0)); + } + + win32c::WM_MOUSELEAVE => { + use crate::event::WindowEvent::CursorLeft; + { + let mut w = subclass_input.window_state.lock(); + w.mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, false)) + .ok(); + } + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: CursorLeft { + device_id: DEVICE_ID, + }, + }); + + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_MOUSEWHEEL => { + use crate::event::MouseScrollDelta::LineDelta; + + let value = f32::from(util::GET_WHEEL_DELTA_WPARAM(wparam)); + let value = value / WHEEL_DELTA as f32; + + let modifiers = update_modifiers(window, subclass_input); + + let mut scroll_lines = DEFAULT_SCROLL_LINES_PER_WHEEL_DELTA; + + let _ = SystemParametersInfoW( + SPI_GETWHEELSCROLLLINES, + 0, + Some(&mut scroll_lines as *mut isize as *mut c_void), + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), + ); + + if scroll_lines as u32 == WHEEL_PAGESCROLL { + // TODO: figure out how to handle page scrolls + scroll_lines = DEFAULT_SCROLL_LINES_PER_WHEEL_DELTA; + } + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta: LineDelta(0.0, value * scroll_lines as f32), + phase: TouchPhase::Moved, + modifiers, + }, + }); + + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_MOUSEHWHEEL => { + use crate::event::MouseScrollDelta::LineDelta; + + let value = f32::from(util::GET_WHEEL_DELTA_WPARAM(wparam)); + let value = value / WHEEL_DELTA as f32; + + let modifiers = update_modifiers(window, subclass_input); + + let mut scroll_characters = DEFAULT_SCROLL_CHARACTERS_PER_WHEEL_DELTA; + + let _ = SystemParametersInfoW( + SPI_GETWHEELSCROLLCHARS, + 0, + Some(&mut scroll_characters as *mut isize as *mut c_void), + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), + ); + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta: LineDelta(value * scroll_characters as f32, 0.0), + phase: TouchPhase::Moved, + modifiers, + }, + }); + + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_KEYDOWN | win32wm::WM_SYSKEYDOWN => { + if msg == WM_SYSKEYDOWN && wparam.0 == usize::from(VK_F4.0) { + result = ProcResult::DefSubclassProc; + } + } + + win32wm::WM_LBUTTONDOWN => { + use crate::event::{ElementState::Pressed, MouseButton::Left, WindowEvent::MouseInput}; + + capture_mouse(window, &mut subclass_input.window_state.lock()); + + let modifiers = update_modifiers(window, subclass_input); + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: MouseInput { + device_id: DEVICE_ID, + state: Pressed, + button: Left, + modifiers, + }, + }); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_LBUTTONUP => { + use crate::event::{ElementState::Released, MouseButton::Left, WindowEvent::MouseInput}; + + release_mouse(subclass_input.window_state.lock()); + + let modifiers = update_modifiers(window, subclass_input); + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: MouseInput { + device_id: DEVICE_ID, + state: Released, + button: Left, + modifiers, + }, + }); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_RBUTTONDOWN => { + use crate::event::{ElementState::Pressed, MouseButton::Right, WindowEvent::MouseInput}; + + capture_mouse(window, &mut subclass_input.window_state.lock()); + + let modifiers = update_modifiers(window, subclass_input); + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: MouseInput { + device_id: DEVICE_ID, + state: Pressed, + button: Right, + modifiers, + }, + }); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_RBUTTONUP => { + use crate::event::{ElementState::Released, MouseButton::Right, WindowEvent::MouseInput}; + + release_mouse(subclass_input.window_state.lock()); + + let modifiers = update_modifiers(window, subclass_input); + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: MouseInput { + device_id: DEVICE_ID, + state: Released, + button: Right, + modifiers, + }, + }); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_MBUTTONDOWN => { + use crate::event::{ElementState::Pressed, MouseButton::Middle, WindowEvent::MouseInput}; + + capture_mouse(window, &mut subclass_input.window_state.lock()); + + let modifiers = update_modifiers(window, subclass_input); + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: MouseInput { + device_id: DEVICE_ID, + state: Pressed, + button: Middle, + modifiers, + }, + }); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_MBUTTONUP => { + use crate::event::{ElementState::Released, MouseButton::Middle, WindowEvent::MouseInput}; + + release_mouse(subclass_input.window_state.lock()); + + let modifiers = update_modifiers(window, subclass_input); + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: MouseInput { + device_id: DEVICE_ID, + state: Released, + button: Middle, + modifiers, + }, + }); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_XBUTTONDOWN => { + use crate::event::{ElementState::Pressed, MouseButton::Other, WindowEvent::MouseInput}; + let xbutton = util::GET_XBUTTON_WPARAM(wparam); + + capture_mouse(window, &mut subclass_input.window_state.lock()); + + let modifiers = update_modifiers(window, subclass_input); + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: MouseInput { + device_id: DEVICE_ID, + state: Pressed, + button: Other(xbutton), + modifiers, + }, + }); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_XBUTTONUP => { + use crate::event::{ElementState::Released, MouseButton::Other, WindowEvent::MouseInput}; + let xbutton = util::GET_XBUTTON_WPARAM(wparam); + + release_mouse(subclass_input.window_state.lock()); + + let modifiers = update_modifiers(window, subclass_input); + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: MouseInput { + device_id: DEVICE_ID, + state: Released, + button: Other(xbutton), + modifiers, + }, + }); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_CAPTURECHANGED => { + // lparam here is a handle to the window which is gaining mouse capture. + // If it is the same as our window, then we're essentially retaining the capture. This + // can happen if `SetCapture` is called on our window when it already has the mouse + // capture. + if lparam.0 != window.0 as _ { + subclass_input.window_state.lock().mouse.capture_count = 0; + } + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_TOUCH => { + let pcount = usize::from(util::LOWORD(wparam.0 as u32)); + let mut inputs: Vec = Vec::with_capacity(pcount); + let uninit_inputs = inputs.spare_capacity_mut(); + let htouch = HTOUCHINPUT(lparam.0 as _); + if GetTouchInputInfo( + htouch, + mem::transmute::< + &mut [std::mem::MaybeUninit], + &mut [windows::Win32::UI::Input::Touch::TOUCHINPUT], + >(uninit_inputs), + mem::size_of::() as i32, + ) + .is_ok() + { + inputs.set_len(pcount); + for input in &inputs { + let mut location = POINT { + x: input.x / 100, + y: input.y / 100, + }; + + if !ScreenToClient(window, &mut location as *mut _).as_bool() { + continue; + } + + let x = location.x as f64 + (input.x % 100) as f64 / 100f64; + let y = location.y as f64 + (input.y % 100) as f64 / 100f64; + let location = PhysicalPosition::new(x, y); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: WindowEvent::Touch(Touch { + phase: if (input.dwFlags & TOUCHEVENTF_DOWN) != Default::default() { + TouchPhase::Started + } else if (input.dwFlags & TOUCHEVENTF_UP) != Default::default() { + TouchPhase::Ended + } else if (input.dwFlags & TOUCHEVENTF_MOVE) != Default::default() { + TouchPhase::Moved + } else { + continue; + }, + location, + force: None, // WM_TOUCH doesn't support pressure information + id: input.dwID as u64, + device_id: DEVICE_ID, + }), + }); + } + } + let _ = CloseTouchInputHandle(htouch); + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_POINTERDOWN | win32wm::WM_POINTERUPDATE | win32wm::WM_POINTERUP => { + if let ( + Some(GetPointerFrameInfoHistory), + Some(SkipPointerFrameMessages), + Some(GetPointerDeviceRects), + ) = ( + *GET_POINTER_FRAME_INFO_HISTORY, + *SKIP_POINTER_FRAME_MESSAGES, + *GET_POINTER_DEVICE_RECTS, + ) { + let pointer_id = u32::from(util::LOWORD(wparam.0 as u32)); + let mut entries_count = 0_u32; + let mut pointers_count = 0_u32; + if !GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count as *mut _, + &mut pointers_count as *mut _, + std::ptr::null_mut(), + ) + .as_bool() + { + result = ProcResult::Value(LRESULT(0)); + return; + } + + let pointer_info_count = (entries_count * pointers_count) as usize; + let mut pointer_infos: Vec = Vec::with_capacity(pointer_info_count); + let uninit_pointer_infos = pointer_infos.spare_capacity_mut(); + if !GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count as *mut _, + &mut pointers_count as *mut _, + uninit_pointer_infos.as_mut_ptr() as *mut _, + ) + .as_bool() + { + result = ProcResult::Value(LRESULT(0)); + return; + } + pointer_infos.set_len(pointer_info_count); + + // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getpointerframeinfohistory + // The information retrieved appears in reverse chronological order, with the most recent entry in the first + // row of the returned array + for pointer_info in pointer_infos.iter().rev() { + let mut device_rect = mem::MaybeUninit::uninit(); + let mut display_rect = mem::MaybeUninit::uninit(); + + if !(GetPointerDeviceRects( + pointer_info.sourceDevice, + device_rect.as_mut_ptr(), + display_rect.as_mut_ptr(), + )) + .as_bool() + { + continue; + } + + let device_rect = device_rect.assume_init(); + let display_rect = display_rect.assume_init(); + + // For the most precise himetric to pixel conversion we calculate the ratio between the resolution + // of the display device (pixel) and the touch device (himetric). + let himetric_to_pixel_ratio_x = (display_rect.right - display_rect.left) as f64 + / (device_rect.right - device_rect.left) as f64; + let himetric_to_pixel_ratio_y = (display_rect.bottom - display_rect.top) as f64 + / (device_rect.bottom - device_rect.top) as f64; + + // ptHimetricLocation's origin is 0,0 even on multi-monitor setups. + // On multi-monitor setups we need to translate the himetric location to the rect of the + // display device it's attached to. + let x = display_rect.left as f64 + + pointer_info.ptHimetricLocation.x as f64 * himetric_to_pixel_ratio_x; + let y = display_rect.top as f64 + + pointer_info.ptHimetricLocation.y as f64 * himetric_to_pixel_ratio_y; + + let mut location = POINT { + x: x.floor() as i32, + y: y.floor() as i32, + }; + + if !ScreenToClient(window, &mut location as *mut _).as_bool() { + continue; + } + + let force = match pointer_info.pointerType { + win32wm::PT_TOUCH => { + let mut touch_info = mem::MaybeUninit::uninit(); + GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| { + if GetPointerTouchInfo(pointer_info.pointerId, touch_info.as_mut_ptr()).as_bool() { + normalize_pointer_pressure(touch_info.assume_init().pressure) + } else { + None + } + }) + } + win32wm::PT_PEN => { + let mut pen_info = mem::MaybeUninit::uninit(); + GET_POINTER_PEN_INFO.and_then(|GetPointerPenInfo| { + if GetPointerPenInfo(pointer_info.pointerId, pen_info.as_mut_ptr()).as_bool() { + normalize_pointer_pressure(pen_info.assume_init().pressure) + } else { + None + } + }) + } + _ => None, + }; + + let x = location.x as f64 + x.fract(); + let y = location.y as f64 + y.fract(); + let location = PhysicalPosition::new(x, y); + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: WindowEvent::Touch(Touch { + phase: if (pointer_info.pointerFlags & POINTER_FLAG_DOWN) != Default::default() { + TouchPhase::Started + } else if (pointer_info.pointerFlags & POINTER_FLAG_UP) != Default::default() { + TouchPhase::Ended + } else if (pointer_info.pointerFlags & POINTER_FLAG_UPDATE) != Default::default() { + TouchPhase::Moved + } else { + continue; + }, + location, + force, + id: pointer_info.pointerId as u64, + device_id: DEVICE_ID, + }), + }); + } + + let _ = SkipPointerFrameMessages(pointer_id); + } + + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_NCACTIVATE => { + let is_active = wparam != WPARAM(0); + let active_focus_changed = subclass_input.window_state.lock().set_active(is_active); + if active_focus_changed { + if is_active { + gain_active_focus(window, subclass_input); + } else { + lose_active_focus(window, subclass_input); + } + } + result = ProcResult::DefWindowProc; + } + + win32wm::WM_SETFOCUS => { + let active_focus_changed = subclass_input.window_state.lock().set_focused(true); + if active_focus_changed { + gain_active_focus(window, subclass_input); + } + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_KILLFOCUS => { + let active_focus_changed = subclass_input.window_state.lock().set_focused(false); + if active_focus_changed { + lose_active_focus(window, subclass_input); + } + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_SETCURSOR => { + let set_cursor_to = { + let window_state = subclass_input.window_state.lock(); + // The return value for the preceding `WM_NCHITTEST` message is conveniently + // provided through the low-order word of lParam. We use that here since + // `WM_MOUSEMOVE` seems to come after `WM_SETCURSOR` for a given cursor movement. + let in_client_area = u32::from(util::LOWORD(lparam.0 as u32)) == HTCLIENT; + if in_client_area { + Some(window_state.mouse.cursor) + } else { + None + } + }; + + match set_cursor_to { + Some(cursor) => { + if let Ok(cursor) = LoadCursorW(None, cursor.to_windows_cursor()) { + SetCursor(Some(cursor)); + } + result = ProcResult::Value(LRESULT(0)); + } + None => result = ProcResult::DefWindowProc, + } + } + + win32wm::WM_GETMINMAXINFO => { + let mmi = lparam.0 as *mut MINMAXINFO; + + let window_state = subclass_input.window_state.lock(); + let is_decorated = window_state + .window_flags() + .contains(WindowFlags::MARKER_DECORATIONS); + + let size_constraints = window_state.size_constraints; + + if size_constraints.has_min() { + let min_size = PhysicalSize::new( + size_constraints + .min_width + .unwrap_or_else(|| PixelUnit::Physical(GetSystemMetrics(SM_CXMINTRACK).into())) + .to_physical(window_state.scale_factor) + .0, + size_constraints + .min_height + .unwrap_or_else(|| PixelUnit::Physical(GetSystemMetrics(SM_CYMINTRACK).into())) + .to_physical(window_state.scale_factor) + .0, + ); + let (width, height): (u32, u32) = util::adjust_size(window, min_size, is_decorated).into(); + (*mmi).ptMinTrackSize = POINT { + x: width as i32, + y: height as i32, + }; + } + if size_constraints.has_max() { + let max_size = PhysicalSize::new( + size_constraints + .max_width + .unwrap_or_else(|| PixelUnit::Physical(GetSystemMetrics(SM_CXMAXTRACK).into())) + .to_physical(window_state.scale_factor) + .0, + size_constraints + .max_height + .unwrap_or_else(|| PixelUnit::Physical(GetSystemMetrics(SM_CYMAXTRACK).into())) + .to_physical(window_state.scale_factor) + .0, + ); + let (width, height): (u32, u32) = util::adjust_size(window, max_size, is_decorated).into(); + (*mmi).ptMaxTrackSize = POINT { + x: width as i32, + y: height as i32, + }; + } + + result = ProcResult::Value(LRESULT(0)); + } + + // Only sent on Windows 8.1 or newer. On Windows 7 and older user has to log out to change + // DPI, therefore all applications are closed while DPI is changing. + win32wm::WM_DPICHANGED => { + use crate::event::WindowEvent::ScaleFactorChanged; + + // This message actually provides two DPI values - x and y. However MSDN says that + // "you only need to use either the X-axis or the Y-axis value when scaling your + // application since they are the same". + // https://msdn.microsoft.com/en-us/library/windows/desktop/dn312083(v=vs.85).aspx + let new_dpi_x = u32::from(util::LOWORD(wparam.0 as u32)); + let new_scale_factor = dpi_to_scale_factor(new_dpi_x); + let old_scale_factor: f64; + + let (allow_resize, is_decorated) = { + let mut window_state = subclass_input.window_state.lock(); + old_scale_factor = window_state.scale_factor; + window_state.scale_factor = new_scale_factor; + + if (new_scale_factor - old_scale_factor).abs() < f64::EPSILON { + result = ProcResult::Value(LRESULT(0)); + return; + } + + let window_flags = window_state.window_flags(); + ( + window_state.fullscreen.is_none() && !window_flags.contains(WindowFlags::MAXIMIZED), + window_flags.contains(WindowFlags::MARKER_DECORATIONS), + ) + }; + + let mut style = WINDOW_STYLE(GetWindowLongW(window, GWL_STYLE) as u32); + // if the window isn't decorated, remove `WS_SIZEBOX` and `WS_CAPTION` so + // `AdjustWindowRect*` functions doesn't account for the hidden caption and borders and + // calculates a correct size for the client area. + if !is_decorated { + style &= !WS_CAPTION; + style &= !WS_SIZEBOX; + } + let style_ex = WINDOW_EX_STYLE(GetWindowLongW(window, GWL_EXSTYLE) as u32); + + // New size as suggested by Windows. + let suggested_rect = *(lparam.0 as *const RECT); + + // The window rect provided is the window's outer size, not it's inner size. However, + // win32 doesn't provide an `UnadjustWindowRectEx` function to get the client rect from + // the outer rect, so we instead adjust the window rect to get the decoration margins + // and remove them from the outer size. + let margin_left: i32; + let margin_top: i32; + // let margin_right: i32; + // let margin_bottom: i32; + { + let adjusted_rect = + util::adjust_window_rect_with_styles(window, style, style_ex, suggested_rect) + .unwrap_or(suggested_rect); + margin_left = suggested_rect.left - adjusted_rect.left; + margin_top = suggested_rect.top - adjusted_rect.top; + // margin_right = adjusted_rect.right - suggested_rect.right; + // margin_bottom = adjusted_rect.bottom - suggested_rect.bottom; + } + + let old_physical_inner_rect = { + let mut old_physical_inner_rect = RECT::default(); + let _ = GetClientRect(window, &mut old_physical_inner_rect); + let mut origin = POINT::default(); + let _ = ClientToScreen(window, &mut origin); + + old_physical_inner_rect.left += origin.x; + old_physical_inner_rect.right += origin.x; + old_physical_inner_rect.top += origin.y; + old_physical_inner_rect.bottom += origin.y; + + old_physical_inner_rect + }; + let old_physical_inner_size = PhysicalSize::new( + (old_physical_inner_rect.right - old_physical_inner_rect.left) as u32, + (old_physical_inner_rect.bottom - old_physical_inner_rect.top) as u32, + ); + + // `allow_resize` prevents us from re-applying DPI adjustment to the restored size after + // exiting fullscreen (the restored size is already DPI adjusted). + let mut new_physical_inner_size = match allow_resize { + // We calculate our own size because the default suggested rect doesn't do a great job + // of preserving the window's logical size. + true => old_physical_inner_size + .to_logical::(old_scale_factor) + .to_physical::(new_scale_factor), + false => old_physical_inner_size, + }; + + // When the "Show window contents while dragging" is turned off, there is no need to adjust the window size. + if !is_show_window_contents_while_dragging_enabled() { + new_physical_inner_size = old_physical_inner_size; + } + + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: ScaleFactorChanged { + scale_factor: new_scale_factor, + new_inner_size: &mut new_physical_inner_size, + }, + }); + + let dragging_window: bool; + + { + let window_state = subclass_input.window_state.lock(); + dragging_window = window_state + .window_flags() + .contains(WindowFlags::MARKER_IN_SIZE_MOVE); + // Unset maximized if we're changing the window's size. + if new_physical_inner_size != old_physical_inner_size { + WindowState::set_window_flags(window_state, window, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + } + } + + let new_outer_rect: RECT; + if util::WIN_VERSION.build < 22000 { + // The window position needs adjustment on Windows 10. + { + let suggested_ul = ( + suggested_rect.left + margin_left, + suggested_rect.top + margin_top, + ); + + let mut conservative_rect = RECT { + left: suggested_ul.0, + top: suggested_ul.1, + right: suggested_ul.0 + new_physical_inner_size.width as i32, + bottom: suggested_ul.1 + new_physical_inner_size.height as i32, + }; + + conservative_rect = + util::adjust_window_rect_with_styles(window, style, style_ex, conservative_rect) + .unwrap_or(conservative_rect); + + // If we're dragging the window, offset the window so that the cursor's + // relative horizontal position in the title bar is preserved. + if dragging_window { + let bias = { + let cursor_pos = { + let mut pos = POINT::default(); + let _ = GetCursorPos(&mut pos); + pos + }; + let suggested_cursor_horizontal_ratio = (cursor_pos.x - suggested_rect.left) as f64 + / (suggested_rect.right - suggested_rect.left) as f64; + + (cursor_pos.x + - (suggested_cursor_horizontal_ratio + * (conservative_rect.right - conservative_rect.left) as f64) + as i32) + - conservative_rect.left + }; + conservative_rect.left += bias; + conservative_rect.right += bias; + } + + // Check to see if the new window rect is on the monitor with the new DPI factor. + // If it isn't, offset the window so that it is. + let new_dpi_monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL); + let conservative_rect_monitor = + MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL); + new_outer_rect = { + if conservative_rect_monitor != new_dpi_monitor { + let get_monitor_rect = |monitor| { + let mut monitor_info = MONITORINFO { + cbSize: mem::size_of::() as _, + ..Default::default() + }; + let _ = GetMonitorInfoW(monitor, &mut monitor_info); + monitor_info.rcMonitor + }; + let wrong_monitor = conservative_rect_monitor; + let wrong_monitor_rect = get_monitor_rect(wrong_monitor); + let new_monitor_rect = get_monitor_rect(new_dpi_monitor); + + // The direction to nudge the window in to get the window onto the monitor with + // the new DPI factor. We calculate this by seeing which monitor edges are + // shared and nudging away from the wrong monitor based on those. + let delta_nudge_to_dpi_monitor = ( + if wrong_monitor_rect.left == new_monitor_rect.right { + -1 + } else if wrong_monitor_rect.right == new_monitor_rect.left { + 1 + } else { + 0 + }, + if wrong_monitor_rect.bottom == new_monitor_rect.top { + 1 + } else if wrong_monitor_rect.top == new_monitor_rect.bottom { + -1 + } else { + 0 + }, + ); + + let abort_after_iterations = new_monitor_rect.right - new_monitor_rect.left + + new_monitor_rect.bottom + - new_monitor_rect.top; + for _ in 0..abort_after_iterations { + conservative_rect.left += delta_nudge_to_dpi_monitor.0; + conservative_rect.right += delta_nudge_to_dpi_monitor.0; + conservative_rect.top += delta_nudge_to_dpi_monitor.1; + conservative_rect.bottom += delta_nudge_to_dpi_monitor.1; + + if MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL) == new_dpi_monitor { + break; + } + } + } + + conservative_rect + }; + } + } else { + // The suggested position is fine w/o adjustment on Windows 11. + new_outer_rect = suggested_rect + } + + let _ = SetWindowPos( + window, + None, + new_outer_rect.left, + new_outer_rect.top, + new_outer_rect.right - new_outer_rect.left, + new_outer_rect.bottom - new_outer_rect.top, + SWP_NOZORDER | SWP_NOACTIVATE, + ); + + result = ProcResult::Value(LRESULT(0)); + } + + win32wm::WM_SETTINGCHANGE => { + update_theme(subclass_input, window, true); + } + + win32wm::WM_NCCALCSIZE => { + let window_flags = subclass_input.window_state.lock().window_flags(); + let is_fullscreen = subclass_input.window_state.lock().fullscreen.is_some(); + + if wparam == WPARAM(0) || window_flags.contains(WindowFlags::MARKER_DECORATIONS) { + result = ProcResult::DefSubclassProc; + } else { + // adjust the maximized borderless window so it doesn't cover the taskbar + if util::is_maximized(window).unwrap_or(false) { + let params = &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS); + if let Ok(monitor_info) = + monitor::get_monitor_info(MonitorFromRect(¶ms.rgrc[0], MONITOR_DEFAULTTONULL)) + { + let mut rect = monitor_info.monitorInfo.rcWork; + + let mut edges = 0; + for edge in [ABE_BOTTOM, ABE_LEFT, ABE_TOP, ABE_RIGHT] { + let mut app_data = APPBARDATA { + cbSize: std::mem::size_of::() as _, + uEdge: edge, + ..Default::default() + }; + if SHAppBarMessage(ABM_GETAUTOHIDEBAR, &mut app_data) != 0 { + edges |= edge; + } + } + + // keep a 1px for taskbar auto-hide to work + if edges & ABE_BOTTOM != 0 { + rect.bottom -= 1; + } + // FIXME: + #[allow(clippy::bad_bit_mask)] + if edges & ABE_LEFT != 0 { + rect.left += 1; + } + if edges & ABE_TOP != 0 { + rect.top += 1; + } + if edges & ABE_RIGHT != 0 { + rect.right -= 1; + } + + params.rgrc[0] = rect; + } + } else if window_flags.contains(WindowFlags::MARKER_UNDECORATED_SHADOW) && !is_fullscreen { + let params = &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS); + + let insets = util::calculate_window_insets(window); + + params.rgrc[0].left += insets.left; + params.rgrc[0].top += insets.top; + params.rgrc[0].right -= insets.right; + params.rgrc[0].bottom -= insets.bottom; + } + result = ProcResult::Value(LRESULT(0)); // return 0 here to make the window borderless + } + } + + win32wm::WM_NCHITTEST => { + let window_state = subclass_input.window_state.lock(); + let window_flags = window_state.window_flags(); + + // Allow resizing unmaximized non-fullscreen undecorated window + if !window_flags.contains(WindowFlags::MARKER_DECORATIONS) + && window_flags.contains(WindowFlags::RESIZABLE) + && window_state.fullscreen.is_none() + && !util::is_maximized(window).unwrap_or(false) + { + // cursor location + let (cx, cy) = ( + util::GET_X_LPARAM(lparam) as i32, + util::GET_Y_LPARAM(lparam) as i32, + ); + + let dpi = hwnd_dpi(window); + let border_y = get_system_metrics_for_dpi(SM_CYFRAME, dpi); + + // if we have undecorated shadows, we only need to handle the top edge + if window_flags.contains(WindowFlags::MARKER_UNDECORATED_SHADOW) { + let rect = util::client_rect(window); + let mut cursor_pt = POINT { x: cx, y: cy }; + if ScreenToClient(window, &mut cursor_pt).as_bool() + && cursor_pt.y >= 0 + && cursor_pt.y <= border_y + && cursor_pt.x >= 0 + && cursor_pt.x <= rect.right + { + result = ProcResult::Value(LRESULT(HTTOP as _)); + } + } + // otherwise do full hit testing + else { + let border_x = get_system_metrics_for_dpi(SM_CXFRAME, dpi); + let rect = util::window_rect(window); + let hit_result = crate::window::hit_test( + (rect.left, rect.top, rect.right, rect.bottom), + cx, + cy, + border_x, + border_y, + ) + .map(|d| d.to_win32()); + + result = hit_result + .map(|r| ProcResult::Value(LRESULT(r as _))) + .unwrap_or(ProcResult::DefSubclassProc); + } + } else { + result = ProcResult::DefSubclassProc; + } + } + + win32wm::WM_SYSCHAR => { + // Handle system shortcut e.g. Alt+Space for window menu + result = ProcResult::DefWindowProc; + } + + _ => { + if msg == *DESTROY_MSG_ID { + let _ = DestroyWindow(window); + result = ProcResult::Value(LRESULT(0)); + } else if msg == *SET_RETAIN_STATE_ON_SIZE_MSG_ID { + let mut window_state = subclass_input.window_state.lock(); + window_state.set_window_flags_in_place(|f| { + f.set(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE, wparam.0 != 0) + }); + result = ProcResult::Value(LRESULT(0)); + } else if msg == *CHANGE_THEME_MSG_ID { + update_theme(subclass_input, window, false); + result = ProcResult::Value(LRESULT(0)); + } else if msg == *S_U_TASKBAR_RESTART { + let window_state = subclass_input.window_state.lock(); + let _ = set_skip_taskbar(window, window_state.skip_taskbar); + } + } + }; + + subclass_input + .event_loop_runner + .catch_unwind(callback) + .unwrap_or_else(|| result = ProcResult::Value(LRESULT(-1))); + + match result { + ProcResult::DefSubclassProc => DefSubclassProc(window, msg, wparam, lparam), + ProcResult::DefWindowProc => DefWindowProcW(window, msg, wparam, lparam), + ProcResult::Value(val) => val, + } +} + +fn update_theme( + subclass_input: &SubclassInput, + window: HWND, + from_settings_change_event: bool, +) { + let mut window_state = subclass_input.window_state.lock(); + let preferred_theme = window_state + .preferred_theme + .or(*subclass_input.event_loop_preferred_theme.lock()); + if from_settings_change_event && preferred_theme.is_some() { + return; + } + let new_theme = try_window_theme(window, preferred_theme, !from_settings_change_event); + if window_state.current_theme != new_theme { + window_state.current_theme = new_theme; + mem::drop(window_state); + unsafe { + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window.0 as _)), + event: WindowEvent::ThemeChanged(new_theme), + }) + }; + } +} + +fn is_show_window_contents_while_dragging_enabled() -> bool { + let mut is_enabled: BOOL = BOOL(0); + let result = unsafe { + SystemParametersInfoW( + SPI_GETDRAGFULLWINDOWS, + 0, + Option::from(&mut is_enabled as *mut _ as *mut std::ffi::c_void), + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), + ) + }; + result.is_ok() && is_enabled.0 != 0 +} + +unsafe extern "system" fn thread_event_target_callback( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + _: usize, + subclass_input_ptr: usize, +) -> LRESULT { + let subclass_input = Box::from_raw(subclass_input_ptr as *mut ThreadMsgTargetSubclassInput); + + let mut subclass_removed = false; + + // I decided to bind the closure to `callback` and pass it to catch_unwind rather than passing + // the closure to catch_unwind directly so that the match body indendation wouldn't change and + // the git blame and history would be preserved. + let callback = || match msg { + win32wm::WM_NCDESTROY => { + remove_event_target_window_subclass::(window); + subclass_removed = true; + let _ = RedrawWindow(Some(window), None, None, RDW_INTERNALPAINT); + LRESULT(0) + } + // Because WM_PAINT comes after all other messages, we use it during modal loops to detect + // when the event queue has been emptied. See `process_event` for more details. + win32wm::WM_PAINT => { + let _ = ValidateRect(Some(window), None); + // If the WM_PAINT handler in `public_window_callback` has already flushed the redraw + // events, `handling_events` will return false and we won't emit a second + // `RedrawEventsCleared` event. + if subclass_input.event_loop_runner.handling_events() { + if subclass_input.event_loop_runner.should_buffer() { + // This branch can be triggered when a nested win32 event loop is triggered + // inside of the `event_handler` callback. + let _ = RedrawWindow(Some(window), None, None, RDW_INTERNALPAINT); + } else { + if flush_paint_messages( + None, + &subclass_input.event_loop_runner + ) { + subclass_input.event_loop_runner.redraw_events_cleared(); + process_control_flow(&subclass_input.event_loop_runner); + } + } + } + + // Default WM_PAINT behaviour. This makes sure modals and popups are shown immediatly when opening them. + DefSubclassProc(window, msg, wparam, lparam) + } + + win32wm::WM_INPUT_DEVICE_CHANGE => { + let event = match wparam.0 as u32 { + win32wm::GIDC_ARRIVAL => DeviceEvent::Added, + win32wm::GIDC_REMOVAL => DeviceEvent::Removed, + _ => unreachable!(), + }; + + subclass_input.send_event(Event::DeviceEvent { + device_id: wrap_device_id(lparam.0), + event, + }); + let _ = RedrawWindow(Some(window), None, None, RDW_INTERNALPAINT); + + LRESULT(0) + } + + win32wm::WM_INPUT => { + if let Some(data) = raw_input::get_raw_input_data(HRAWINPUT(lparam.0 as _)) { + handle_raw_input(&subclass_input, data); + let _ = RedrawWindow(Some(window), None, None, RDW_INTERNALPAINT); + } + + DefSubclassProc(window, msg, wparam, lparam) + } + + // We don't process `WM_QUERYENDSESSION` yet until we introduce the same mechanism as Tauri's `ExitRequested` event + // win32wm::WM_QUERYENDSESSION => {} + win32wm::WM_ENDSESSION => { + // `wParam` is `FALSE` is for if the shutdown gets canceled, + // and we don't need to handle that case since we didn't do anything prior in response to `WM_QUERYENDSESSION` + if wparam.0 == TRUE.0 as usize { + subclass_input.event_loop_runner.loop_destroyed(); + } + // Note: after we return 0 here, Windows will shut us down + LRESULT(0) + } + + _ if msg == *USER_EVENT_MSG_ID => { + if let Ok(event) = subclass_input.user_event_receiver.recv() { + subclass_input.send_event(Event::UserEvent(event)); + } + let _ = RedrawWindow(Some(window), None, None, RDW_INTERNALPAINT); + LRESULT(0) + } + _ if msg == *EXEC_MSG_ID => { + let mut function: ThreadExecFn = Box::from_raw(wparam.0 as *mut _); + function(); + let _ = RedrawWindow(Some(window), None, None, RDW_INTERNALPAINT); + LRESULT(0) + } + _ if msg == *PROCESS_NEW_EVENTS_MSG_ID => { + let _ = PostThreadMessageW( + subclass_input.event_loop_runner.wait_thread_id(), + *CANCEL_WAIT_UNTIL_MSG_ID, + WPARAM(0), + LPARAM(0), + ); + + // if the control_flow is WaitUntil, make sure the given moment has actually passed + // before emitting NewEvents + if let ControlFlow::WaitUntil(wait_until) = subclass_input.event_loop_runner.control_flow() { + let mut msg = MSG::default(); + while Instant::now() < wait_until { + if PeekMessageW(&mut msg, None, 0, 0, PM_NOREMOVE).as_bool() { + // This works around a "feature" in PeekMessageW. If the message PeekMessageW + // gets is a WM_PAINT message that had RDW_INTERNALPAINT set (i.e. doesn't + // have an update region), PeekMessageW will remove that window from the + // redraw queue even though we told it not to remove messages from the + // queue. We fix it by re-dispatching an internal paint message to that + // window. + if msg.message == WM_PAINT { + let mut rect = RECT::default(); + if !GetUpdateRect(msg.hwnd, Some(&mut rect), false).as_bool() { + let _ = RedrawWindow(Some(msg.hwnd), None, None, RDW_INTERNALPAINT); + } + } + + break; + } + } + } + subclass_input.event_loop_runner.poll(); + let _ = RedrawWindow(Some(window), None, None, RDW_INTERNALPAINT); + LRESULT(0) + } + _ => DefSubclassProc(window, msg, wparam, lparam), + }; + + let result = subclass_input + .event_loop_runner + .catch_unwind(callback) + .unwrap_or(LRESULT(-1)); + if subclass_removed { + mem::drop(subclass_input); + } else { + // FIXME: this seems to leak intentionally? + #[allow(unused_must_use)] + Box::into_raw(subclass_input); + } + result +} + +unsafe fn handle_raw_input( + subclass_input: &ThreadMsgTargetSubclassInput, + data: RAWINPUT, +) { + use crate::event::{ + DeviceEvent::{Button, Key, Motion, MouseMotion, MouseWheel}, + ElementState::{Pressed, Released}, + MouseScrollDelta::LineDelta, + }; + + let device_id = wrap_device_id(data.header.hDevice.0 as _); + + if data.header.dwType == RIM_TYPEMOUSE.0 { + let mouse = data.data.mouse; + + if util::has_flag(mouse.usFlags.0, MOUSE_MOVE_RELATIVE.0) { + let x = mouse.lLastX as f64; + let y = mouse.lLastY as f64; + + if x != 0.0 { + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: Motion { axis: 0, value: x }, + }); + } + + if y != 0.0 { + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: Motion { axis: 1, value: y }, + }); + } + + if x != 0.0 || y != 0.0 { + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: MouseMotion { delta: (x, y) }, + }); + } + } + + if util::has_flag( + mouse.Anonymous.Anonymous.usButtonFlags, + RI_MOUSE_WHEEL as u16, + ) { + // We must cast to SHORT first, becaues `usButtonData` must be interpreted as signed. + let delta = mouse.Anonymous.Anonymous.usButtonData as i16 as f32 / WHEEL_DELTA as f32; + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: MouseWheel { + delta: LineDelta(0.0, delta), + }, + }); + } + + let button_state = + raw_input::get_raw_mouse_button_state(mouse.Anonymous.Anonymous.usButtonFlags); + // Left, middle, and right, respectively. + for (index, state) in button_state.iter().enumerate() { + if let Some(state) = *state { + // This gives us consistency with X11, since there doesn't + // seem to be anything else reasonable to do for a mouse + // button ID. + let button = (index + 1) as _; + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: Button { button, state }, + }); + } + } + } else if data.header.dwType == RIM_TYPEKEYBOARD.0 { + let keyboard = data.data.keyboard; + + let pressed = keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN; + let released = keyboard.Message == WM_KEYUP || keyboard.Message == WM_SYSKEYUP; + + if !pressed && !released { + return; + } + + let state = if pressed { Pressed } else { Released }; + let extension = { + if util::has_flag(keyboard.Flags, RI_KEY_E0 as _) { + 0xE000 + } else if util::has_flag(keyboard.Flags, RI_KEY_E1 as _) { + 0xE100 + } else { + 0x0000 + } + }; + let scancode = if keyboard.MakeCode == 0 { + // In some cases (often with media keys) the device reports a scancode of 0 but a + // valid virtual key. In these cases we obtain the scancode from the virtual key. + MapVirtualKeyW(keyboard.VKey as u32, MAPVK_VK_TO_VSC_EX) as u16 + } else { + keyboard.MakeCode | extension + }; + if scancode == 0xE11D || scancode == 0xE02A { + // At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing + // Ctrl+NumLock. + // This equvalence means that if the user presses Pause, the keyboard will emit two + // subsequent keypresses: + // 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100) + // 2, 0x0045 - Which on its own can be interpreted as Pause + // + // There's another combination which isn't quite an equivalence: + // PrtSc used to be Shift+Asterisk. This means that on some keyboards, presssing + // PrtSc (print screen) produces the following sequence: + // 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000) + // 2, 0xE037 - Which is a numpad multiply (0x37) with an exteion flag (0xE000). This on + // its own it can be interpreted as PrtSc + // + // For this reason, if we encounter the first keypress, we simply ignore it, trusting + // that there's going to be another event coming, from which we can extract the + // appropriate key. + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + return; + } + let code = if VIRTUAL_KEY(keyboard.VKey) == VK_NUMLOCK { + // Historically, the NumLock and the Pause key were one and the same physical key. + // The user could trigger Pause by pressing Ctrl+NumLock. + // Now these are often physically separate and the two keys can be differentiated by + // checking the extension flag of the scancode. NumLock is 0xE045, Pause is 0x0045. + // + // However in this event, both keys are reported as 0x0045 even on modern hardware. + // Therefore we use the virtual key instead to determine whether it's a NumLock and + // set the KeyCode accordingly. + // + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + KeyCode::NumLock + } else { + KeyCode::from_scancode(scancode as u32) + }; + if VIRTUAL_KEY(keyboard.VKey) == VK_SHIFT { + match code { + KeyCode::NumpadDecimal + | KeyCode::Numpad0 + | KeyCode::Numpad1 + | KeyCode::Numpad2 + | KeyCode::Numpad3 + | KeyCode::Numpad4 + | KeyCode::Numpad5 + | KeyCode::Numpad6 + | KeyCode::Numpad7 + | KeyCode::Numpad8 + | KeyCode::Numpad9 => { + // On Windows, holding the Shift key makes numpad keys behave as if NumLock + // wasn't active. The way this is exposed to applications by the system is that + // the application receives a fake key release event for the shift key at the + // moment when the numpad key is pressed, just before receiving the numpad key + // as well. + // + // The issue is that in the raw device event (here), the fake shift release + // event reports the numpad key as the scancode. Unfortunately, the event doesn't + // have any information to tell whether it's the left shift or the right shift + // that needs to get the fake release (or press) event so we don't forward this + // event to the application at all. + // + // For more on this, read the article by Raymond Chen, titled: + // "The shift key overrides NumLock" + // https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953 + return; + } + _ => (), + } + } + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: Key(RawKeyEvent { + physical_key: code, + state, + }), + }); + } +} diff --git a/vendor/tao/src/platform_impl/windows/event_loop/runner.rs b/vendor/tao/src/platform_impl/windows/event_loop/runner.rs new file mode 100644 index 0000000000..720b278db3 --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/event_loop/runner.rs @@ -0,0 +1,447 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + any::Any, + cell::{Cell, RefCell}, + collections::{HashSet, VecDeque}, + mem, panic, + rc::Rc, + time::Instant, +}; + +use windows::Win32::{ + Foundation::HWND, + Graphics::Gdi::{RedrawWindow, RDW_INTERNALPAINT}, +}; + +use crate::{ + dpi::PhysicalSize, + event::{Event, StartCause, WindowEvent}, + event_loop::ControlFlow, + platform_impl::platform::util, + window::WindowId, +}; + +pub(crate) type EventLoopRunnerShared = Rc>; +pub(crate) struct EventLoopRunner { + // The event loop's win32 handles + thread_msg_target: HWND, + wait_thread_id: u32, + + control_flow: Cell, + runner_state: Cell, + last_events_cleared: Cell, + + event_handler: Cell, &mut ControlFlow)>>>, + event_buffer: RefCell>>, + + owned_windows: Cell>, + + panic_error: Cell>, +} + +pub type PanicError = Box; + +/// See `move_state_to` function for details on how the state loop works. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum RunnerState { + /// The event loop has just been created, and an `Init` event must be sent. + Uninitialized, + /// The event loop is idling. + Idle, + /// The event loop is handling the OS's events and sending them to the user's callback. + /// `NewEvents` has been sent, and `MainEventsCleared` hasn't. + HandlingMainEvents, + /// The event loop is handling the redraw events and sending them to the user's callback. + /// `MainEventsCleared` has been sent, and `RedrawEventsCleared` hasn't. + HandlingRedrawEvents, + /// The event loop has been destroyed. No other events will be emitted. + Destroyed, +} + +enum BufferedEvent { + Event(Event<'static, T>), + ScaleFactorChanged(WindowId, f64, PhysicalSize), +} + +impl EventLoopRunner { + pub(crate) fn new(thread_msg_target: HWND, wait_thread_id: u32) -> EventLoopRunner { + EventLoopRunner { + thread_msg_target, + wait_thread_id, + runner_state: Cell::new(RunnerState::Uninitialized), + control_flow: Cell::new(ControlFlow::Poll), + panic_error: Cell::new(None), + last_events_cleared: Cell::new(Instant::now()), + event_handler: Cell::new(None), + event_buffer: RefCell::new(VecDeque::new()), + owned_windows: Cell::new(HashSet::new()), + } + } + + pub(crate) unsafe fn set_event_handler(&self, f: F) + where + F: FnMut(Event<'_, T>, &mut ControlFlow), + { + let old_event_handler = self.event_handler.replace(mem::transmute::< + Option, &mut ControlFlow)>>, + Option, &mut ControlFlow)>>, + >(Some(Box::new(f)))); + assert!(old_event_handler.is_none()); + } + + pub(crate) fn reset_runner(&self) { + let EventLoopRunner { + thread_msg_target: _, + wait_thread_id: _, + runner_state, + panic_error, + control_flow, + last_events_cleared: _, + event_handler, + event_buffer: _, + owned_windows: _, + } = self; + runner_state.set(RunnerState::Uninitialized); + panic_error.set(None); + control_flow.set(ControlFlow::Poll); + event_handler.set(None); + } +} + +/// State retrieval functions. +impl EventLoopRunner { + pub fn thread_msg_target(&self) -> HWND { + self.thread_msg_target + } + + pub fn wait_thread_id(&self) -> u32 { + self.wait_thread_id + } + + pub fn redrawing(&self) -> bool { + self.runner_state.get() == RunnerState::HandlingRedrawEvents + } + + pub fn take_panic_error(&self) -> Result<(), PanicError> { + match self.panic_error.take() { + Some(err) => Err(err), + None => Ok(()), + } + } + + pub fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub fn handling_events(&self) -> bool { + self.runner_state.get() != RunnerState::Idle + } + + pub fn should_buffer(&self) -> bool { + let handler = self.event_handler.take(); + let should_buffer = handler.is_none(); + self.event_handler.set(handler); + should_buffer + } +} + +/// Misc. functions +impl EventLoopRunner { + pub fn catch_unwind(&self, f: impl FnOnce() -> R) -> Option { + let panic_error = self.panic_error.take(); + if panic_error.is_none() { + let result = panic::catch_unwind(panic::AssertUnwindSafe(f)); + + // Check to see if the panic error was set in a re-entrant call to catch_unwind inside + // of `f`. If it was, that error takes priority. If it wasn't, check if our call to + // catch_unwind caught any panics and set panic_error appropriately. + match self.panic_error.take() { + None => match result { + Ok(r) => Some(r), + Err(e) => { + self.panic_error.set(Some(e)); + None + } + }, + Some(e) => { + self.panic_error.set(Some(e)); + None + } + } + } else { + self.panic_error.set(panic_error); + None + } + } + pub fn register_window(&self, window: HWND) { + let mut owned_windows = self.owned_windows.take(); + owned_windows.insert(window.0 as _); + self.owned_windows.set(owned_windows); + } + + pub fn remove_window(&self, window: HWND) { + let mut owned_windows = self.owned_windows.take(); + owned_windows.remove(&(window.0 as _)); + self.owned_windows.set(owned_windows); + } + + pub fn owned_windows(&self, mut f: impl FnMut(HWND)) { + let mut owned_windows = self.owned_windows.take(); + for hwnd in &owned_windows { + f(HWND(*hwnd as _)); + } + let new_owned_windows = self.owned_windows.take(); + owned_windows.extend(&new_owned_windows); + self.owned_windows.set(owned_windows); + } +} + +/// Event dispatch functions. +impl EventLoopRunner { + pub(crate) unsafe fn poll(&self) { + self.move_state_to(RunnerState::HandlingMainEvents); + } + + pub(crate) unsafe fn send_event(&self, event: Event<'_, T>) { + if let Event::RedrawRequested(_) = event { + if self.runner_state.get() != RunnerState::HandlingRedrawEvents { + warn!("RedrawRequested dispatched without explicit MainEventsCleared"); + self.move_state_to(RunnerState::HandlingRedrawEvents); + } + self.call_event_handler(event); + } else if self.should_buffer() { + // If the runner is already borrowed, we're in the middle of an event loop invocation. Add + // the event to a buffer to be processed later. + self + .event_buffer + .borrow_mut() + .push_back(BufferedEvent::from_event(event)) + } else { + self.move_state_to(RunnerState::HandlingMainEvents); + self.call_event_handler(event); + self.dispatch_buffered_events(); + } + } + + pub(crate) unsafe fn main_events_cleared(&self) { + self.move_state_to(RunnerState::HandlingRedrawEvents); + } + + pub(crate) unsafe fn redraw_events_cleared(&self) { + self.move_state_to(RunnerState::Idle); + } + + pub(crate) unsafe fn loop_destroyed(&self) { + self.move_state_to(RunnerState::Destroyed); + } + + unsafe fn call_event_handler(&self, event: Event<'_, T>) { + self.catch_unwind(|| { + let mut control_flow = self.control_flow.take(); + let mut event_handler = self.event_handler.take() + .expect("either event handler is re-entrant (likely), or no event handler is registered (very unlikely)"); + + if let ControlFlow::ExitWithCode(code) = control_flow { + event_handler(event, &mut ControlFlow::ExitWithCode(code)); + } else { + event_handler(event, &mut control_flow); + } + + assert!(self.event_handler.replace(Some(event_handler)).is_none()); + self.control_flow.set(control_flow); + }); + } + + unsafe fn dispatch_buffered_events(&self) { + loop { + // We do this instead of using a `while let` loop because if we use a `while let` + // loop the reference returned `borrow_mut()` doesn't get dropped until the end + // of the loop's body and attempts to add events to the event buffer while in + // `process_event` will fail. + let buffered_event_opt = self.event_buffer.borrow_mut().pop_front(); + match buffered_event_opt { + Some(e) => e.dispatch_event(|e| self.call_event_handler(e)), + None => break, + } + } + } + + /// Dispatch control flow events (`NewEvents`, `MainEventsCleared`, `RedrawEventsCleared`, and + /// `LoopDestroyed`) as necessary to bring the internal `RunnerState` to the new runner state. + /// + /// The state transitions are defined as follows: + /// + /// ```text + /// Uninitialized + /// | + /// V + /// HandlingMainEvents + /// ^ | + /// | V + /// Idle <--- HandlingRedrawEvents + /// | + /// V + /// Destroyed + /// ``` + /// + /// Attempting to transition back to `Uninitialized` will result in a panic. Attempting to + /// transition *from* `Destroyed` will also reuslt in a panic. Transitioning to the current + /// state is a no-op. Even if the `new_runner_state` isn't the immediate next state in the + /// runner state machine (e.g. `self.runner_state == HandlingMainEvents` and + /// `new_runner_state == Idle`), the intermediate state transitions will still be executed. + unsafe fn move_state_to(&self, new_runner_state: RunnerState) { + use RunnerState::{Destroyed, HandlingMainEvents, HandlingRedrawEvents, Idle, Uninitialized}; + + match ( + self.runner_state.replace(new_runner_state), + new_runner_state, + ) { + (Uninitialized, Uninitialized) + | (Idle, Idle) + | (HandlingMainEvents, HandlingMainEvents) + | (HandlingRedrawEvents, HandlingRedrawEvents) + | (Destroyed, Destroyed) => (), + + // State transitions that initialize the event loop. + (Uninitialized, HandlingMainEvents) => { + self.call_new_events(true); + } + (Uninitialized, HandlingRedrawEvents) => { + self.call_new_events(true); + self.call_event_handler(Event::MainEventsCleared); + } + (Uninitialized, Idle) => { + self.call_new_events(true); + self.call_event_handler(Event::MainEventsCleared); + self.call_redraw_events_cleared(); + } + (Uninitialized, Destroyed) => { + self.call_new_events(true); + self.call_event_handler(Event::MainEventsCleared); + self.call_redraw_events_cleared(); + self.call_event_handler(Event::LoopDestroyed); + } + (_, Uninitialized) => panic!("cannot move state to Uninitialized"), + + // State transitions that start the event handling process. + (Idle, HandlingMainEvents) => { + self.call_new_events(false); + } + (Idle, HandlingRedrawEvents) => { + self.call_new_events(false); + self.call_event_handler(Event::MainEventsCleared); + } + (Idle, Destroyed) => { + self.call_event_handler(Event::LoopDestroyed); + } + + (HandlingMainEvents, HandlingRedrawEvents) => { + self.call_event_handler(Event::MainEventsCleared); + } + (HandlingMainEvents, Idle) => { + warn!("RedrawEventsCleared emitted without explicit MainEventsCleared"); + self.call_event_handler(Event::MainEventsCleared); + self.call_redraw_events_cleared(); + } + (HandlingMainEvents, Destroyed) => { + self.call_event_handler(Event::MainEventsCleared); + self.call_redraw_events_cleared(); + self.call_event_handler(Event::LoopDestroyed); + } + + (HandlingRedrawEvents, Idle) => { + self.call_redraw_events_cleared(); + } + (HandlingRedrawEvents, HandlingMainEvents) => { + warn!("NewEvents emitted without explicit RedrawEventsCleared"); + self.call_redraw_events_cleared(); + self.call_new_events(false); + } + (HandlingRedrawEvents, Destroyed) => { + self.call_redraw_events_cleared(); + self.call_event_handler(Event::LoopDestroyed); + } + + (Destroyed, _) => panic!("cannot move state from Destroyed"), + } + } + + unsafe fn call_new_events(&self, init: bool) { + let start_cause = match (init, self.control_flow()) { + (true, _) => StartCause::Init, + (false, ControlFlow::Poll) => StartCause::Poll, + (false, ControlFlow::ExitWithCode(_)) | (false, ControlFlow::Wait) => { + StartCause::WaitCancelled { + requested_resume: None, + start: self.last_events_cleared.get(), + } + } + (false, ControlFlow::WaitUntil(requested_resume)) => { + if Instant::now() < requested_resume { + StartCause::WaitCancelled { + requested_resume: Some(requested_resume), + start: self.last_events_cleared.get(), + } + } else { + StartCause::ResumeTimeReached { + requested_resume, + start: self.last_events_cleared.get(), + } + } + } + }; + self.call_event_handler(Event::NewEvents(start_cause)); + self.dispatch_buffered_events(); + let _ = RedrawWindow(Some(self.thread_msg_target), None, None, RDW_INTERNALPAINT); + } + + unsafe fn call_redraw_events_cleared(&self) { + self.call_event_handler(Event::RedrawEventsCleared); + self.last_events_cleared.set(Instant::now()); + } +} + +impl BufferedEvent { + pub fn from_event(event: Event<'_, T>) -> BufferedEvent { + match event { + Event::WindowEvent { + event: + WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + }, + window_id, + } => BufferedEvent::ScaleFactorChanged(window_id, scale_factor, *new_inner_size), + event => BufferedEvent::Event(event.to_static().unwrap()), + } + } + + pub fn dispatch_event(self, dispatch: impl FnOnce(Event<'_, T>)) { + match self { + Self::Event(event) => dispatch(event), + Self::ScaleFactorChanged(window_id, scale_factor, mut new_inner_size) => { + let os_inner_size = new_inner_size; + + dispatch(Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size: &mut new_inner_size, + }, + }); + + if new_inner_size != os_inner_size { + util::set_inner_size_physical( + HWND(window_id.0 .0 as _), + new_inner_size.width as _, + new_inner_size.height as _, + true, + ); + } + } + } + } +} diff --git a/vendor/tao/src/platform_impl/windows/icon.rs b/vendor/tao/src/platform_impl/windows/icon.rs new file mode 100644 index 0000000000..1af14ed86d --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/icon.rs @@ -0,0 +1,170 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{fmt, io, iter::once, mem, os::windows::ffi::OsStrExt, path::Path, sync::Arc}; + +use windows::{ + core::PCWSTR, + Win32::{ + Foundation::{HWND, LPARAM, WPARAM}, + System::LibraryLoader::*, + UI::WindowsAndMessaging::*, + }, +}; + +use crate::{dpi::PhysicalSize, icon::*}; + +impl Pixel { + fn to_bgra(&mut self) { + mem::swap(&mut self.r, &mut self.b); + } +} + +impl RgbaIcon { + fn into_windows_icon(self) -> Result { + let mut rgba = self.rgba; + let pixel_count = rgba.len() / PIXEL_SIZE; + let mut and_mask = Vec::with_capacity(pixel_count); + let pixels = + unsafe { std::slice::from_raw_parts_mut(rgba.as_mut_ptr() as *mut Pixel, pixel_count) }; + for pixel in pixels { + and_mask.push(pixel.a.wrapping_sub(u8::MAX)); // invert alpha channel + pixel.to_bgra(); + } + assert_eq!(and_mask.len(), pixel_count); + let handle = unsafe { + CreateIcon( + None, + self.width as i32, + self.height as i32, + 1, + (PIXEL_SIZE * 8) as u8, + and_mask.as_ptr(), + rgba.as_ptr(), + ) + }; + Ok(WinIcon::from_handle( + handle.map_err(|_| BadIcon::OsError(io::Error::last_os_error()))?, + )) + } +} + +#[non_exhaustive] +#[derive(Debug)] +pub enum IconType { + Small = ICON_SMALL as isize, + Big = ICON_BIG as isize, +} + +#[derive(Debug)] +struct RaiiIcon { + handle: HICON, +} + +#[derive(Clone)] +pub struct WinIcon { + inner: Arc, +} + +unsafe impl Send for WinIcon {} + +impl WinIcon { + pub fn as_raw_handle(&self) -> HICON { + self.inner.handle + } + + pub fn from_path>( + path: P, + size: Option>, + ) -> Result { + let wide_path: Vec = path + .as_ref() + .as_os_str() + .encode_wide() + .chain(once(0)) + .collect(); + + // width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size + let (width, height) = size.map(Into::into).unwrap_or((0, 0)); + + let handle = unsafe { + LoadImageW( + None, + PCWSTR::from_raw(wide_path.as_ptr()), + IMAGE_ICON, + width, + height, + LR_DEFAULTSIZE | LR_LOADFROMFILE, + ) + } + .map(|handle| HICON(handle.0)); + Ok(WinIcon::from_handle( + handle.map_err(|_| BadIcon::OsError(io::Error::last_os_error()))?, + )) + } + + pub fn from_resource(resource_id: u16, size: Option>) -> Result { + // width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size + let (width, height) = size.map(Into::into).unwrap_or((0, 0)); + let handle = unsafe { + LoadImageW( + GetModuleHandleW(PCWSTR::null()).map(Into::into).ok(), + PCWSTR::from_raw(resource_id as usize as *const u16), + IMAGE_ICON, + width, + height, + LR_DEFAULTSIZE, + ) + } + .map(|handle| HICON(handle.0)); + Ok(WinIcon::from_handle( + handle.map_err(|_| BadIcon::OsError(io::Error::last_os_error()))?, + )) + } + + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + let rgba_icon = RgbaIcon::from_rgba(rgba, width, height)?; + rgba_icon.into_windows_icon() + } + + pub fn set_for_window(&self, hwnd: HWND, icon_type: IconType) { + unsafe { + SendMessageW( + hwnd, + WM_SETICON, + Some(WPARAM(icon_type as _)), + Some(LPARAM(self.as_raw_handle().0 as _)), + ); + } + } + + fn from_handle(handle: HICON) -> Self { + Self { + inner: Arc::new(RaiiIcon { handle }), + } + } +} + +impl Drop for RaiiIcon { + fn drop(&mut self) { + let _ = unsafe { DestroyIcon(self.handle) }; + } +} + +impl fmt::Debug for WinIcon { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + (*self.inner).fmt(formatter) + } +} + +pub fn unset_for_window(hwnd: HWND, icon_type: IconType) { + unsafe { + SendMessageW( + hwnd, + WM_SETICON, + Some(WPARAM(icon_type as _)), + Some(LPARAM(0)), + ); + } +} diff --git a/vendor/tao/src/platform_impl/windows/keyboard.rs b/vendor/tao/src/platform_impl/windows/keyboard.rs new file mode 100644 index 0000000000..be565ce5d9 --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/keyboard.rs @@ -0,0 +1,817 @@ +use parking_lot::{Mutex, MutexGuard}; +use std::{ + char, + collections::{HashMap, HashSet}, + ffi::OsString, + mem::MaybeUninit, + os::windows::ffi::OsStringExt, +}; + +use windows::Win32::{ + Foundation::{HWND, LPARAM, LRESULT, WPARAM}, + UI::{ + Input::KeyboardAndMouse::{self as win32km, *}, + WindowsAndMessaging::{self as win32wm, *}, + }, +}; + +use unicode_segmentation::UnicodeSegmentation; + +use crate::{ + event::{ElementState, KeyEvent}, + keyboard::{Key, KeyCode, KeyLocation, NativeKeyCode}, + platform_impl::{ + platform::{ + event_loop::ProcResult, + keyboard_layout::{get_or_insert_str, Layout, LayoutCache, WindowsModifiers, LAYOUT_CACHE}, + KeyEventExtra, + }, + WindowId, + }, +}; + +pub fn is_msg_keyboard_related(msg: u32) -> bool { + let is_keyboard_msg = (WM_KEYFIRST..=WM_KEYLAST).contains(&msg); + + is_keyboard_msg || msg == WM_SETFOCUS || msg == WM_KILLFOCUS +} + +pub type ExScancode = u16; + +pub struct MessageAsKeyEvent { + pub event: KeyEvent, + pub is_synthetic: bool, +} + +lazy_static! { + pub(crate) static ref KEY_EVENT_BUILDERS: Mutex> = + Mutex::new(HashMap::new()); +} + +/// Stores information required to make `KeyEvent`s. +/// +/// A single Tao `KeyEvent` contains information which the Windows API passes to the application +/// in multiple window messages. In other words: a Tao `KeyEvent` cannot be built from a single +/// window message. Therefore, this type keeps track of certain information from previous events so +/// that a `KeyEvent` can be constructed when the last event related to a keypress is received. +/// +/// `PeekMessage` is sometimes used to determine whether the next window message still belongs to the +/// current keypress. If it doesn't and the current state represents a key event waiting to be +/// dispatched, then said event is considered complete and is dispatched. +/// +/// The sequence of window messages for a key press event is the following: +/// - Exactly one WM_KEYDOWN / WM_SYSKEYDOWN +/// - Zero or one WM_DEADCHAR / WM_SYSDEADCHAR +/// - Zero or more WM_CHAR / WM_SYSCHAR. These messages each come with a UTF-16 code unit which when +/// put together in the sequence they arrived in, forms the text which is the result of pressing the +/// key. +/// +/// Key release messages are a bit different due to the fact that they don't contribute to +/// text input. The "sequence" only consists of one WM_KEYUP / WM_SYSKEYUP event. +#[derive(Default)] +pub struct KeyEventBuilder { + event_info: Option, +} +impl KeyEventBuilder { + /// Call this function for every window message. + /// Returns Some() if this window message completes a KeyEvent. + /// Returns None otherwise. + pub(crate) fn process_message( + &mut self, + hwnd: HWND, + msg_kind: u32, + wparam: WPARAM, + lparam: LPARAM, + result: &mut ProcResult, + ) -> Vec { + match msg_kind { + win32wm::WM_SETFOCUS => { + // synthesize keydown events + let kbd_state = get_async_kbd_state(); + let key_events = self.synthesize_kbd_state(ElementState::Pressed, &kbd_state); + if !key_events.is_empty() { + return key_events; + } + } + win32wm::WM_KILLFOCUS => { + // sythesize keyup events + let kbd_state = get_kbd_state(); + let key_events = self.synthesize_kbd_state(ElementState::Released, &kbd_state); + if !key_events.is_empty() { + return key_events; + } + } + win32wm::WM_KEYDOWN | win32wm::WM_SYSKEYDOWN => { + if msg_kind == WM_SYSKEYDOWN && wparam.0 == usize::from(VK_F4.0) { + // Don't dispatch Alt+F4 to the application. + // This is handled in `event_loop.rs` + return vec![]; + } + + if msg_kind == win32wm::WM_SYSKEYDOWN { + *result = ProcResult::DefSubclassProc; + } else { + *result = ProcResult::Value(LRESULT(0)); + } + + let mut layouts = LAYOUT_CACHE.lock(); + let event_info = + PartialKeyEventInfo::from_message(wparam, lparam, ElementState::Pressed, &mut layouts); + + let mut next_msg = MaybeUninit::uninit(); + let peek_retval = unsafe { + PeekMessageW( + next_msg.as_mut_ptr(), + Some(hwnd), + WM_KEYFIRST, + WM_KEYLAST, + PM_NOREMOVE, + ) + }; + let has_next_key_message = peek_retval.as_bool(); + self.event_info = None; + let mut finished_event_info = Some(event_info); + if has_next_key_message { + let next_msg = unsafe { next_msg.assume_init() }; + let next_msg_kind = next_msg.message; + let next_belongs_to_this = !matches!( + next_msg_kind, + win32wm::WM_KEYDOWN | win32wm::WM_SYSKEYDOWN | win32wm::WM_KEYUP | win32wm::WM_SYSKEYUP + ); + if next_belongs_to_this { + self.event_info = finished_event_info.take(); + } else { + let (_, layout) = layouts.get_current_layout(); + let is_fake = { + let curr_event = finished_event_info.as_ref().unwrap(); + is_current_fake(curr_event, next_msg, layout) + }; + if is_fake { + finished_event_info = None; + } + } + } + if let Some(event_info) = finished_event_info { + let ev = event_info.finalize(&mut layouts.strings); + return vec![MessageAsKeyEvent { + event: ev, + is_synthetic: false, + }]; + } + } + win32wm::WM_DEADCHAR | win32wm::WM_SYSDEADCHAR => { + *result = ProcResult::Value(LRESULT(0)); + // At this point, we know that there isn't going to be any more events related to + // this key press + let event_info = self.event_info.take().unwrap(); + let mut layouts = LAYOUT_CACHE.lock(); + let ev = event_info.finalize(&mut layouts.strings); + return vec![MessageAsKeyEvent { + event: ev, + is_synthetic: false, + }]; + } + win32wm::WM_CHAR | win32wm::WM_SYSCHAR => { + if self.event_info.is_none() { + trace!("Received a CHAR message but no `event_info` was available. The message is probably IME, returning."); + return vec![]; + } + *result = ProcResult::Value(LRESULT(0)); + let is_high_surrogate = (0xD800..=0xDBFF).contains(&wparam.0); + let is_low_surrogate = (0xDC00..=0xDFFF).contains(&wparam.0); + + let is_utf16 = is_high_surrogate || is_low_surrogate; + + let more_char_coming; + unsafe { + let mut next_msg = MaybeUninit::uninit(); + let has_message = PeekMessageW( + next_msg.as_mut_ptr(), + Some(hwnd), + WM_KEYFIRST, + WM_KEYLAST, + PM_NOREMOVE, + ); + let has_message = has_message.as_bool(); + if !has_message { + more_char_coming = false; + } else { + let next_msg = next_msg.assume_init().message; + more_char_coming = next_msg == WM_CHAR || next_msg == WM_SYSCHAR; + } + } + + if is_utf16 { + if let Some(ev_info) = self.event_info.as_mut() { + ev_info.utf16parts.push(wparam.0 as u16); + } + } else { + // In this case, wparam holds a UTF-32 character. + // Let's encode it as UTF-16 and append it to the end of `utf16parts` + let utf16parts = match self.event_info.as_mut() { + Some(ev_info) => &mut ev_info.utf16parts, + None => { + warn!("The event_info was None when it was expected to be some"); + return vec![]; + } + }; + let start_offset = utf16parts.len(); + let new_size = utf16parts.len() + 2; + utf16parts.resize(new_size, 0); + if let Some(ch) = char::from_u32(wparam.0 as u32) { + let encode_len = ch.encode_utf16(&mut utf16parts[start_offset..]).len(); + let new_size = start_offset + encode_len; + utf16parts.resize(new_size, 0); + } + } + if !more_char_coming { + let mut event_info = match self.event_info.take() { + Some(ev_info) => ev_info, + None => { + warn!("The event_info was None when it was expected to be some"); + return vec![]; + } + }; + let mut layouts = LAYOUT_CACHE.lock(); + // It's okay to call `ToUnicode` here, because at this point the dead key + // is already consumed by the character. + let kbd_state = get_kbd_state(); + let mod_state = WindowsModifiers::active_modifiers(&kbd_state); + + let (_, layout) = layouts.get_current_layout(); + let ctrl_on = if layout.has_alt_graph { + let alt_on = mod_state.contains(WindowsModifiers::ALT); + !alt_on && mod_state.contains(WindowsModifiers::CONTROL) + } else { + mod_state.contains(WindowsModifiers::CONTROL) + }; + + // If Ctrl is not pressed, just use the text with all + // modifiers because that already consumed the dead key. Otherwise, + // we would interpret the character incorrectly, missing the dead key. + if !ctrl_on { + event_info.text = PartialText::System(event_info.utf16parts.clone()); + } else { + let mod_no_ctrl = mod_state.remove_only_ctrl(); + let num_lock_on = kbd_state[usize::from(VK_NUMLOCK.0)] & 1 != 0; + let vkey = event_info.vkey; + let scancode = event_info.scancode; + let keycode = event_info.code; + let key = layout.get_key(mod_no_ctrl, num_lock_on, vkey, scancode, keycode); + event_info.text = PartialText::Text(key.to_text()); + } + let ev = event_info.finalize(&mut layouts.strings); + return vec![MessageAsKeyEvent { + event: ev, + is_synthetic: false, + }]; + } + } + win32wm::WM_KEYUP | win32wm::WM_SYSKEYUP => { + if msg_kind == win32wm::WM_SYSKEYUP { + *result = ProcResult::DefSubclassProc; + } else { + *result = ProcResult::Value(LRESULT(0)); + } + + let mut layouts = LAYOUT_CACHE.lock(); + let event_info = + PartialKeyEventInfo::from_message(wparam, lparam, ElementState::Released, &mut layouts); + let mut next_msg = MaybeUninit::uninit(); + let peek_retval = unsafe { + PeekMessageW( + next_msg.as_mut_ptr(), + Some(hwnd), + WM_KEYFIRST, + WM_KEYLAST, + PM_NOREMOVE, + ) + }; + let has_next_key_message = peek_retval.as_bool(); + let mut valid_event_info = Some(event_info); + if has_next_key_message { + let next_msg = unsafe { next_msg.assume_init() }; + let (_, layout) = layouts.get_current_layout(); + let is_fake = { + let event_info = valid_event_info.as_ref().unwrap(); + is_current_fake(event_info, next_msg, layout) + }; + if is_fake { + valid_event_info = None; + } + } + if let Some(event_info) = valid_event_info { + let event = event_info.finalize(&mut layouts.strings); + return vec![MessageAsKeyEvent { + event, + is_synthetic: false, + }]; + } + } + _ => (), + } + + Vec::new() + } + + fn synthesize_kbd_state( + &mut self, + key_state: ElementState, + kbd_state: &[u8; 256], + ) -> Vec { + let mut key_events = Vec::new(); + + let mut layouts = LAYOUT_CACHE.lock(); + let (locale_id, _) = layouts.get_current_layout(); + + let is_key_pressed = |vk: VIRTUAL_KEY| &kbd_state[usize::from(vk.0)] & 0x80 != 0; + + // Is caps-lock active? Note that this is different from caps-lock + // being held down. + let caps_lock_on = kbd_state[usize::from(VK_CAPITAL.0)] & 1 != 0; + let num_lock_on = kbd_state[usize::from(VK_NUMLOCK.0)] & 1 != 0; + + // We are synthesizing the press event for caps-lock first for the following reasons: + // 1. If caps-lock is *not* held down but *is* active, then we have to + // synthesize all printable keys, respecting the caps-lock state. + // 2. If caps-lock is held down, we could choose to sythesize its + // keypress after every other key, in which case all other keys *must* + // be sythesized as if the caps-lock state was be the opposite + // of what it currently is. + // -- + // For the sake of simplicity we are choosing to always sythesize + // caps-lock first, and always use the current caps-lock state + // to determine the produced text + if is_key_pressed(VK_CAPITAL) { + let event = self.create_synthetic( + VK_CAPITAL, + key_state, + caps_lock_on, + num_lock_on, + locale_id, + &mut layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + let do_non_modifier = |key_events: &mut Vec<_>, layouts: &mut _| { + for vk in 0..256 { + let vk = VIRTUAL_KEY(vk); + match vk { + win32km::VK_CONTROL + | win32km::VK_LCONTROL + | win32km::VK_RCONTROL + | win32km::VK_SHIFT + | win32km::VK_LSHIFT + | win32km::VK_RSHIFT + | win32km::VK_MENU + | win32km::VK_LMENU + | win32km::VK_RMENU + | win32km::VK_CAPITAL => continue, + _ => (), + } + if !is_key_pressed(vk) { + continue; + } + let event = self.create_synthetic( + vk, + key_state, + caps_lock_on, + num_lock_on, + locale_id as HKL, + layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + }; + let do_modifier = |key_events: &mut Vec<_>, layouts: &mut _| { + const CLEAR_MODIFIER_VKS: [VIRTUAL_KEY; 6] = [ + VK_LCONTROL, + VK_LSHIFT, + VK_LMENU, + VK_RCONTROL, + VK_RSHIFT, + VK_RMENU, + ]; + for vk in CLEAR_MODIFIER_VKS.iter() { + if is_key_pressed(*vk) { + let event = self.create_synthetic( + *vk, + key_state, + caps_lock_on, + num_lock_on, + locale_id as HKL, + layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + } + }; + + // Be cheeky and sequence modifier and non-modifier + // key events such that non-modifier keys are not affected + // by modifiers (except for caps-lock) + match key_state { + ElementState::Pressed => { + do_non_modifier(&mut key_events, &mut layouts); + do_modifier(&mut key_events, &mut layouts); + } + ElementState::Released => { + do_modifier(&mut key_events, &mut layouts); + do_non_modifier(&mut key_events, &mut layouts); + } + } + + key_events + } + + fn create_synthetic( + &self, + vk: VIRTUAL_KEY, + key_state: ElementState, + caps_lock_on: bool, + num_lock_on: bool, + locale_id: HKL, + layouts: &mut MutexGuard<'_, LayoutCache>, + ) -> Option { + let scancode = + unsafe { MapVirtualKeyExW(u32::from(vk.0), MAPVK_VK_TO_VSC_EX, Some(locale_id)) }; + if scancode == 0 { + return None; + } + let scancode = scancode as ExScancode; + let code = KeyCode::from_scancode(scancode as u32); + let mods = if caps_lock_on { + WindowsModifiers::CAPS_LOCK + } else { + WindowsModifiers::empty() + }; + let layout = layouts.layouts.get(&(locale_id.0 as _)).unwrap(); + let logical_key = layout.get_key(mods, num_lock_on, vk, scancode, code); + let key_without_modifiers = + layout.get_key(WindowsModifiers::empty(), false, vk, scancode, code); + let text = if key_state == ElementState::Pressed { + logical_key.to_text() + } else { + None + }; + let event_info = PartialKeyEventInfo { + vkey: vk, + logical_key: PartialLogicalKey::This(logical_key.clone()), + key_without_modifiers, + key_state, + scancode, + is_repeat: false, + code, + location: get_location(scancode, locale_id), + utf16parts: Vec::with_capacity(8), + text: PartialText::Text(text), + }; + + let mut event = event_info.finalize(&mut layouts.strings); + event.logical_key = logical_key; + event.platform_specific.text_with_all_modifiers = text; + Some(MessageAsKeyEvent { + event, + is_synthetic: true, + }) + } +} + +enum PartialText { + // Unicode + System(Vec), + Text(Option<&'static str>), +} + +enum PartialLogicalKey { + /// Use the text provided by the WM_CHAR messages and report that as a `Character` variant. If + /// the text consists of multiple grapheme clusters (user-precieved characters) that means that + /// dead key could not be combined with the second input, and in that case we should fall back + /// to using what would have without a dead-key input. + TextOr(Key<'static>), + + /// Use the value directly provided by this variant + This(Key<'static>), +} + +struct PartialKeyEventInfo { + vkey: VIRTUAL_KEY, + scancode: ExScancode, + key_state: ElementState, + is_repeat: bool, + code: KeyCode, + location: KeyLocation, + logical_key: PartialLogicalKey, + + key_without_modifiers: Key<'static>, + + /// The UTF-16 code units of the text that was produced by the keypress event. + /// This take all modifiers into account. Including CTRL + utf16parts: Vec, + + text: PartialText, +} + +impl PartialKeyEventInfo { + fn from_message( + wparam: WPARAM, + lparam: LPARAM, + state: ElementState, + layouts: &mut MutexGuard<'_, LayoutCache>, + ) -> Self { + const NO_MODS: WindowsModifiers = WindowsModifiers::empty(); + + let (_, layout) = layouts.get_current_layout(); + let lparam_struct = destructure_key_lparam(lparam); + let vkey = VIRTUAL_KEY(wparam.0 as u16); + let scancode = if lparam_struct.scancode == 0 { + // In some cases (often with media keys) the device reports a scancode of 0 but a + // valid virtual key. In these cases we obtain the scancode from the virtual key. + unsafe { + MapVirtualKeyExW( + u32::from(vkey.0), + MAPVK_VK_TO_VSC_EX, + Some(HKL(layout.hkl as _)), + ) as u16 + } + } else { + new_ex_scancode(lparam_struct.scancode, lparam_struct.extended) + }; + let code = KeyCode::from_scancode(scancode as u32); + let location = get_location(scancode, HKL(layout.hkl as _)); + + let kbd_state = get_kbd_state(); + let mods = WindowsModifiers::active_modifiers(&kbd_state); + let mods_without_ctrl = mods.remove_only_ctrl(); + let num_lock_on = kbd_state[usize::from(VK_NUMLOCK.0)] & 1 != 0; + + // On Windows Ctrl+NumLock = Pause (and apparently Ctrl+Pause -> NumLock). In these cases + // the KeyCode still stores the real key, so in the name of consistency across platforms, we + // circumvent this mapping and force the key values to match the keycode. + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + let code_as_key = if mods.contains(WindowsModifiers::CONTROL) { + match code { + KeyCode::NumLock => Some(Key::NumLock), + KeyCode::Pause => Some(Key::Pause), + _ => None, + } + } else { + None + }; + + let preliminary_logical_key = + layout.get_key(mods_without_ctrl, num_lock_on, vkey, scancode, code); + let key_is_char = matches!(preliminary_logical_key, Key::Character(_)); + let is_pressed = state == ElementState::Pressed; + + let logical_key = if let Some(key) = code_as_key.clone() { + PartialLogicalKey::This(key) + } else if is_pressed && key_is_char && !mods.contains(WindowsModifiers::CONTROL) { + // In some cases we want to use the UNICHAR text for logical_key in order to allow + // dead keys to have an effect on the character reported by `logical_key`. + PartialLogicalKey::TextOr(preliminary_logical_key) + } else { + PartialLogicalKey::This(preliminary_logical_key) + }; + let key_without_modifiers = if let Some(key) = code_as_key { + key + } else { + match layout.get_key(NO_MODS, false, vkey, scancode, code) { + // We convert dead keys into their character. + // The reason for this is that `key_without_modifiers` is designed for key-bindings, + // but the US International layout treats `'` (apostrophe) as a dead key and the + // reguar US layout treats it a character. In order for a single binding + // configuration to work with both layouts, we forward each dead key as a character. + Key::Dead(k) => { + if let Some(ch) = k { + // I'm avoiding the heap allocation. I don't want to talk about it :( + let mut utf8 = [0; 4]; + let s = ch.encode_utf8(&mut utf8); + let static_str = get_or_insert_str(&mut layouts.strings, s); + Key::Character(static_str) + } else { + Key::Unidentified(NativeKeyCode::Unidentified) + } + } + key => key, + } + }; + + PartialKeyEventInfo { + vkey, + scancode, + key_state: state, + logical_key, + key_without_modifiers, + is_repeat: lparam_struct.is_repeat, + code, + location, + utf16parts: Vec::with_capacity(8), + text: PartialText::System(Vec::new()), + } + } + + fn finalize(self, strings: &mut HashSet<&'static str>) -> KeyEvent { + let mut char_with_all_modifiers = None; + if !self.utf16parts.is_empty() { + let os_string = OsString::from_wide(&self.utf16parts); + if let Ok(string) = os_string.into_string() { + let static_str = get_or_insert_str(strings, string); + char_with_all_modifiers = Some(static_str); + } + } + + // The text without Ctrl + let mut text = None; + match self.text { + PartialText::System(wide) => { + if !wide.is_empty() { + let os_string = OsString::from_wide(&wide); + if let Ok(string) = os_string.into_string() { + let static_str = get_or_insert_str(strings, string); + text = Some(static_str); + } + } + } + PartialText::Text(s) => { + text = s; + } + } + + let logical_key = match self.logical_key { + PartialLogicalKey::TextOr(fallback) => match text { + Some(s) => { + if s.grapheme_indices(true).count() > 1 { + fallback + } else { + Key::Character(s) + } + } + None => Key::Unidentified(NativeKeyCode::Windows(self.scancode)), + }, + PartialLogicalKey::This(v) => v, + }; + + KeyEvent { + physical_key: self.code, + logical_key, + text, + location: self.location, + state: self.key_state, + repeat: self.is_repeat, + platform_specific: KeyEventExtra { + text_with_all_modifiers: char_with_all_modifiers, + key_without_modifiers: self.key_without_modifiers, + }, + } + } +} + +#[derive(Debug, Copy, Clone)] +struct KeyLParam { + pub scancode: u8, + pub extended: bool, + + /// This is `previous_state XOR transition_state`. See the lParam for WM_KEYDOWN and WM_KEYUP for further details. + pub is_repeat: bool, +} + +fn destructure_key_lparam(lparam: LPARAM) -> KeyLParam { + let previous_state = (lparam.0 >> 30) & 0x01; + let transition_state = (lparam.0 >> 31) & 0x01; + KeyLParam { + scancode: ((lparam.0 >> 16) & 0xFF) as u8, + extended: ((lparam.0 >> 24) & 0x01) != 0, + is_repeat: (previous_state ^ transition_state) != 0, + } +} + +#[inline] +fn new_ex_scancode(scancode: u8, extended: bool) -> ExScancode { + (scancode as u16) | (if extended { 0xE000 } else { 0 }) +} + +#[inline] +fn ex_scancode_from_lparam(lparam: LPARAM) -> ExScancode { + let lparam = destructure_key_lparam(lparam); + new_ex_scancode(lparam.scancode, lparam.extended) +} + +/// Gets the keyboard state as reported by messages that have been removed from the event queue. +/// See also: get_async_kbd_state +fn get_kbd_state() -> [u8; 256] { + unsafe { + let mut kbd_state: MaybeUninit<[u8; 256]> = MaybeUninit::uninit(); + let _ = GetKeyboardState(&mut *kbd_state.as_mut_ptr()); + kbd_state.assume_init() + } +} + +/// Gets the current keyboard state regardless of whether the corresponding keyboard events have +/// been removed from the event queue. See also: get_kbd_state +fn get_async_kbd_state() -> [u8; 256] { + unsafe { + let mut kbd_state: [u8; 256] = [0; 256]; + for (vk, state) in kbd_state.iter_mut().enumerate() { + let vk = VIRTUAL_KEY(vk as u16); + let async_state = GetAsyncKeyState(i32::from(vk.0)); + let is_down = (async_state & (1 << 15)) != 0; + if is_down { + *state = 0x80; + } + + if matches!( + vk, + win32km::VK_CAPITAL | win32km::VK_NUMLOCK | win32km::VK_SCROLL + ) { + // Toggle states aren't reported by `GetAsyncKeyState` + let toggle_state = GetKeyState(i32::from(vk.0)); + let is_active = (toggle_state & 1) != 0; + *state |= if is_active { 1 } else { 0 }; + } + } + kbd_state + } +} + +/// On windows, AltGr == Ctrl + Alt +/// +/// Due to this equivalence, the system generates a fake Ctrl key-press (and key-release) preceeding +/// every AltGr key-press (and key-release). We check if the current event is a Ctrl event and if +/// the next event is a right Alt (AltGr) event. If this is the case, the current event must be the +/// fake Ctrl event. +fn is_current_fake(curr_info: &PartialKeyEventInfo, next_msg: MSG, layout: &Layout) -> bool { + let curr_is_ctrl = matches!(curr_info.logical_key, PartialLogicalKey::This(Key::Control)); + if layout.has_alt_graph { + let next_code = ex_scancode_from_lparam(next_msg.lParam); + let next_is_altgr = next_code == 0xE038; // 0xE038 is right alt + if curr_is_ctrl && next_is_altgr { + return true; + } + } + false +} + +fn get_location(scancode: ExScancode, hkl: HKL) -> KeyLocation { + const VK_ABNT_C2: VIRTUAL_KEY = win32km::VK_ABNT_C2; + + let extension = 0xE000; + let extended = (scancode & extension) == extension; + let vkey = + VIRTUAL_KEY(unsafe { MapVirtualKeyExW(scancode as u32, MAPVK_VSC_TO_VK_EX, Some(hkl)) } as u16); + + // Use the native VKEY and the extended flag to cover most cases + // This is taken from the `druid` GUI library, specifically + // druid-shell/src/platform/windows/keyboard.rs + match vkey { + win32km::VK_LSHIFT | win32km::VK_LCONTROL | win32km::VK_LMENU | win32km::VK_LWIN => { + KeyLocation::Left + } + win32km::VK_RSHIFT | win32km::VK_RCONTROL | win32km::VK_RMENU | win32km::VK_RWIN => { + KeyLocation::Right + } + win32km::VK_RETURN if extended => KeyLocation::Numpad, + win32km::VK_INSERT + | win32km::VK_DELETE + | win32km::VK_END + | win32km::VK_DOWN + | win32km::VK_NEXT + | win32km::VK_LEFT + | win32km::VK_CLEAR + | win32km::VK_RIGHT + | win32km::VK_HOME + | win32km::VK_UP + | win32km::VK_PRIOR => { + if extended { + KeyLocation::Standard + } else { + KeyLocation::Numpad + } + } + win32km::VK_NUMPAD0 + | win32km::VK_NUMPAD1 + | win32km::VK_NUMPAD2 + | win32km::VK_NUMPAD3 + | win32km::VK_NUMPAD4 + | win32km::VK_NUMPAD5 + | win32km::VK_NUMPAD6 + | win32km::VK_NUMPAD7 + | win32km::VK_NUMPAD8 + | win32km::VK_NUMPAD9 + | win32km::VK_DECIMAL + | win32km::VK_DIVIDE + | win32km::VK_MULTIPLY + | win32km::VK_SUBTRACT + | win32km::VK_ADD + | VK_ABNT_C2 => KeyLocation::Numpad, + _ => KeyLocation::Standard, + } +} diff --git a/vendor/tao/src/platform_impl/windows/keyboard_layout.rs b/vendor/tao/src/platform_impl/windows/keyboard_layout.rs new file mode 100644 index 0000000000..ec42c4bf54 --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/keyboard_layout.rs @@ -0,0 +1,983 @@ +use parking_lot::Mutex; +use std::{ + collections::{hash_map::Entry, HashMap, HashSet}, + ffi::OsString, + os::windows::ffi::OsStringExt, +}; + +use lazy_static::lazy_static; + +use windows::Win32::{ + System::SystemServices::{LANG_JAPANESE, LANG_KOREAN}, + UI::Input::KeyboardAndMouse::{self as win32km, *}, +}; + +use super::keyboard::ExScancode; +use crate::{ + keyboard::{Key, KeyCode, ModifiersState, NativeKeyCode}, + platform_impl::platform::util, +}; + +lazy_static! { + pub(crate) static ref LAYOUT_CACHE: Mutex = Mutex::new(LayoutCache::default()); +} + +fn key_pressed(vkey: VIRTUAL_KEY) -> bool { + unsafe { (GetKeyState(u32::from(vkey.0) as i32) & (1 << 15)) == (1 << 15) } +} + +const NUMPAD_VKEYS: [VIRTUAL_KEY; 16] = [ + VK_NUMPAD0, + VK_NUMPAD1, + VK_NUMPAD2, + VK_NUMPAD3, + VK_NUMPAD4, + VK_NUMPAD5, + VK_NUMPAD6, + VK_NUMPAD7, + VK_NUMPAD8, + VK_NUMPAD9, + VK_MULTIPLY, + VK_ADD, + VK_SEPARATOR, + VK_SUBTRACT, + VK_DECIMAL, + VK_DIVIDE, +]; + +lazy_static! { + static ref NUMPAD_KEYCODES: HashSet = { + let mut keycodes = HashSet::new(); + keycodes.insert(KeyCode::Numpad0); + keycodes.insert(KeyCode::Numpad1); + keycodes.insert(KeyCode::Numpad2); + keycodes.insert(KeyCode::Numpad3); + keycodes.insert(KeyCode::Numpad4); + keycodes.insert(KeyCode::Numpad5); + keycodes.insert(KeyCode::Numpad6); + keycodes.insert(KeyCode::Numpad7); + keycodes.insert(KeyCode::Numpad8); + keycodes.insert(KeyCode::Numpad9); + keycodes.insert(KeyCode::NumpadMultiply); + keycodes.insert(KeyCode::NumpadAdd); + keycodes.insert(KeyCode::NumpadComma); + keycodes.insert(KeyCode::NumpadSubtract); + keycodes.insert(KeyCode::NumpadDecimal); + keycodes.insert(KeyCode::NumpadDivide); + keycodes + }; +} + +bitflags! { + #[derive(Clone, Copy, Eq, PartialEq, Hash)] + pub struct WindowsModifiers : u8 { + const SHIFT = 1 << 0; + const CONTROL = 1 << 1; + const ALT = 1 << 2; + const CAPS_LOCK = 1 << 3; + const FLAGS_END = 1 << 4; + } +} + +impl WindowsModifiers { + pub fn active_modifiers(key_state: &[u8; 256]) -> WindowsModifiers { + let shift = key_state[usize::from(VK_SHIFT.0)] & 0x80 != 0; + let lshift = key_state[usize::from(VK_LSHIFT.0)] & 0x80 != 0; + let rshift = key_state[usize::from(VK_RSHIFT.0)] & 0x80 != 0; + + let control = key_state[usize::from(VK_CONTROL.0)] & 0x80 != 0; + let lcontrol = key_state[usize::from(VK_LCONTROL.0)] & 0x80 != 0; + let rcontrol = key_state[usize::from(VK_RCONTROL.0)] & 0x80 != 0; + + let alt = key_state[usize::from(VK_MENU.0)] & 0x80 != 0; + let lalt = key_state[usize::from(VK_LMENU.0)] & 0x80 != 0; + let ralt = key_state[usize::from(VK_RMENU.0)] & 0x80 != 0; + + let caps = key_state[usize::from(VK_CAPITAL.0)] & 0x01 != 0; + + let mut result = WindowsModifiers::empty(); + if shift || lshift || rshift { + result.insert(WindowsModifiers::SHIFT); + } + if control || lcontrol || rcontrol { + result.insert(WindowsModifiers::CONTROL); + } + if alt || lalt || ralt { + result.insert(WindowsModifiers::ALT); + } + if caps { + result.insert(WindowsModifiers::CAPS_LOCK); + } + + result + } + + pub fn apply_to_kbd_state(self, key_state: &mut [u8; 256]) { + if self.intersects(Self::SHIFT) { + key_state[usize::from(VK_SHIFT.0)] |= 0x80; + } else { + key_state[usize::from(VK_SHIFT.0)] &= !0x80; + key_state[usize::from(VK_LSHIFT.0)] &= !0x80; + key_state[usize::from(VK_RSHIFT.0)] &= !0x80; + } + if self.intersects(Self::CONTROL) { + key_state[usize::from(VK_CONTROL.0)] |= 0x80; + } else { + key_state[usize::from(VK_CONTROL.0)] &= !0x80; + key_state[usize::from(VK_LCONTROL.0)] &= !0x80; + key_state[usize::from(VK_RCONTROL.0)] &= !0x80; + } + if self.intersects(Self::ALT) { + key_state[usize::from(VK_MENU.0)] |= 0x80; + } else { + key_state[usize::from(VK_MENU.0)] &= !0x80; + key_state[usize::from(VK_LMENU.0)] &= !0x80; + key_state[usize::from(VK_RMENU.0)] &= !0x80; + } + if self.intersects(Self::CAPS_LOCK) { + key_state[usize::from(VK_CAPITAL.0)] |= 0x01; + } else { + key_state[usize::from(VK_CAPITAL.0)] &= !0x01; + } + } + + /// Removes the control modifier if the alt modifier is not present. + /// This is useful because on Windows: (Control + Alt) == AltGr + /// but we don't want to interfere with the AltGr state. + pub fn remove_only_ctrl(mut self) -> WindowsModifiers { + if !self.contains(WindowsModifiers::ALT) { + self.remove(WindowsModifiers::CONTROL); + } + self + } +} + +pub(crate) struct Layout { + pub hkl: isize, + + /// Maps numpad keys from Windows virtual key to a `Key`. + /// + /// This is useful because some numpad keys generate different charcaters based on the locale. + /// For example `VK_DECIMAL` is sometimes "." and sometimes ",". Note: numpad-specific virtual + /// keys are only produced by Windows when the NumLock is active. + /// + /// Making this field separate from the `keys` field saves having to add NumLock as a modifier + /// to `WindowsModifiers`, which would double the number of items in keys. + pub numlock_on_keys: HashMap>, + /// Like `numlock_on_keys` but this will map to the key that would be produced if numlock was + /// off. The keys of this map are identical to the keys of `numlock_on_keys`. + pub numlock_off_keys: HashMap>, + + /// Maps a modifier state to group of key strings + /// We're not using `ModifiersState` here because that object cannot express caps lock, + /// but we need to handle caps lock too. + /// + /// This map shouldn't need to exist. + /// However currently this seems to be the only good way + /// of getting the label for the pressed key. Note that calling `ToUnicode` + /// just when the key is pressed/released would be enough if `ToUnicode` wouldn't + /// change the keyboard state (it clears the dead key). There is a flag to prevent + /// changing the state, but that flag requires Windows 10, version 1607 or newer) + pub keys: HashMap>>, + pub has_alt_graph: bool, +} + +impl Layout { + pub fn get_key( + &self, + mods: WindowsModifiers, + num_lock_on: bool, + vkey: VIRTUAL_KEY, + scancode: ExScancode, + keycode: KeyCode, + ) -> Key<'static> { + let native_code = NativeKeyCode::Windows(scancode); + + let unknown_alt = vkey == VK_MENU; + if !unknown_alt { + // Here we try using the virtual key directly but if the virtual key doesn't distinguish + // between left and right alt, we can't report AltGr. Therefore, we only do this if the + // key is not the "unknown alt" key. + // + // The reason for using the virtual key directly is that `MapVirtualKeyExW` (used when + // building the keys map) sometimes maps virtual keys to odd scancodes that don't match + // the scancode coming from the KEYDOWN message for the same key. For example: `VK_LEFT` + // is mapped to `0x004B`, but the scancode for the left arrow is `0xE04B`. + let key_from_vkey = + vkey_to_non_char_key(vkey, native_code, HKL(self.hkl as _), self.has_alt_graph); + + if !matches!(key_from_vkey, Key::Unidentified(_)) { + return key_from_vkey; + } + } + if num_lock_on { + if let Some(key) = self.numlock_on_keys.get(&vkey.0) { + return key.clone(); + } + } else if let Some(key) = self.numlock_off_keys.get(&vkey.0) { + return key.clone(); + } + + if let Some(keys) = self.keys.get(&mods) { + if let Some(key) = keys.get(&keycode) { + return key.clone(); + } + } + Key::Unidentified(native_code) + } +} + +#[derive(Default)] +pub(crate) struct LayoutCache { + /// Maps locale identifiers (HKL) to layouts + pub layouts: HashMap, + pub strings: HashSet<&'static str>, +} + +impl LayoutCache { + /// Checks whether the current layout is already known and + /// prepares the layout if it isn't known. + /// The current layout is then returned. + pub fn get_current_layout<'a>(&'a mut self) -> (HKL, &'a Layout) { + let locale_id = unsafe { GetKeyboardLayout(0) }; + match self.layouts.entry(locale_id.0 as _) { + Entry::Occupied(entry) => (locale_id, entry.into_mut()), + Entry::Vacant(entry) => { + let layout = Self::prepare_layout(&mut self.strings, locale_id); + (locale_id, entry.insert(layout)) + } + } + } + + pub fn get_agnostic_mods(&mut self) -> ModifiersState { + let (_, layout) = self.get_current_layout(); + let filter_out_altgr = layout.has_alt_graph && key_pressed(VK_RMENU); + let mut mods = ModifiersState::empty(); + mods.set(ModifiersState::SHIFT, key_pressed(VK_SHIFT)); + mods.set( + ModifiersState::CONTROL, + key_pressed(VK_CONTROL) && !filter_out_altgr, + ); + mods.set( + ModifiersState::ALT, + key_pressed(VK_MENU) && !filter_out_altgr, + ); + mods.set( + ModifiersState::SUPER, + key_pressed(VK_LWIN) || key_pressed(VK_RWIN), + ); + mods + } + + fn prepare_layout(strings: &mut HashSet<&'static str>, locale_id: HKL) -> Layout { + let mut layout = Layout { + hkl: locale_id.0 as _, + numlock_on_keys: Default::default(), + numlock_off_keys: Default::default(), + keys: Default::default(), + has_alt_graph: false, + }; + + // We initialize the keyboard state with all zeros to + // simulate a scenario when no modifier is active. + let mut key_state = [0u8; 256]; + + // `MapVirtualKeyExW` maps (non-numpad-specific) virtual keys to scancodes as if numlock + // was off. We rely on this behavior to find all virtual keys which are not numpad-specific + // but map to the numpad. + // + // src_vkey: VK ==> scancode: u16 (on the numpad) + // + // Then we convert the source virtual key into a `Key` and the scancode into a virtual key + // to get the reverse mapping. + // + // src_vkey: VK ==> scancode: u16 (on the numpad) + // || || + // \/ \/ + // map_value: Key <- map_vkey: VK + layout.numlock_off_keys.reserve(NUMPAD_KEYCODES.len()); + for vk in 0_u16..256 { + let scancode = + unsafe { MapVirtualKeyExW(u32::from(vk), MAPVK_VK_TO_VSC_EX, Some(locale_id as HKL)) }; + if scancode == 0 { + continue; + } + let vk = VIRTUAL_KEY(vk); + let keycode = KeyCode::from_scancode(scancode); + if !is_numpad_specific(vk) && NUMPAD_KEYCODES.contains(&keycode) { + let native_code = NativeKeyCode::Windows(scancode as u16); + let map_vkey = keycode_to_vkey(keycode, locale_id); + if map_vkey == Default::default() { + continue; + } + let map_value = vkey_to_non_char_key(vk, native_code, locale_id, false); + if matches!(map_value, Key::Unidentified(_)) { + continue; + } + layout.numlock_off_keys.insert(map_vkey.0, map_value); + } + } + + layout.numlock_on_keys.reserve(NUMPAD_VKEYS.len()); + for vk in NUMPAD_VKEYS.iter() { + let scancode = + unsafe { MapVirtualKeyExW(u32::from(vk.0), MAPVK_VK_TO_VSC_EX, Some(locale_id as HKL)) }; + let unicode = Self::to_unicode_string(&key_state, *vk, scancode, locale_id); + if let ToUnicodeResult::Str(s) = unicode { + let static_str = get_or_insert_str(strings, s); + layout + .numlock_on_keys + .insert(vk.0, Key::Character(static_str)); + } + } + + // Iterate through every combination of modifiers + let mods_end = WindowsModifiers::FLAGS_END.bits(); + for mod_state in 0..mods_end { + let mut keys_for_this_mod = HashMap::with_capacity(256); + + let mod_state = WindowsModifiers::from_bits_truncate(mod_state); + mod_state.apply_to_kbd_state(&mut key_state); + + // Virtual key values are in the domain [0, 255]. + // This is reinforced by the fact that the keyboard state array has 256 + // elements. This array is allowed to be indexed by virtual key values + // giving the key state for the virtual key used for indexing. + for vk in 0_u16..256 { + let scancode = + unsafe { MapVirtualKeyExW(u32::from(vk), MAPVK_VK_TO_VSC_EX, Some(locale_id)) }; + if scancode == 0 { + continue; + } + let vk = VIRTUAL_KEY(vk); + let native_code = NativeKeyCode::Windows(scancode as ExScancode); + let key_code = KeyCode::from_scancode(scancode); + // Let's try to get the key from just the scancode and vk + // We don't necessarily know yet if AltGraph is present on this layout so we'll + // assume it isn't. Then we'll do a second pass where we set the "AltRight" keys to + // "AltGr" in case we find out that there's an AltGraph. + let preliminary_key = vkey_to_non_char_key(vk, native_code, locale_id, false); + match preliminary_key { + Key::Unidentified(_) => (), + _ => { + keys_for_this_mod.insert(key_code, preliminary_key); + continue; + } + } + + let unicode = Self::to_unicode_string(&key_state, vk, scancode, locale_id); + let key = match unicode { + ToUnicodeResult::Str(str) => { + let static_str = get_or_insert_str(strings, str); + Key::Character(static_str) + } + ToUnicodeResult::Dead(dead_char) => { + //#[cfg(debug_assertions)] println!("{:?} - {:?} produced dead {:?}", key_code, mod_state, dead_char); + Key::Dead(dead_char) + } + ToUnicodeResult::None => { + let has_alt = mod_state.contains(WindowsModifiers::ALT); + let has_ctrl = mod_state.contains(WindowsModifiers::CONTROL); + // HACK: `ToUnicodeEx` seems to fail getting the string for the numpad + // divide key, so we handle that explicitly here + if !has_alt && !has_ctrl && key_code == KeyCode::NumpadDivide { + Key::Character("/") + } else { + // Just use the unidentified key, we got earlier + preliminary_key + } + } + }; + + // Check for alt graph. + // The logic is that if a key pressed with no modifier produces + // a different `Character` from when it's pressed with CTRL+ALT then the layout + // has AltGr. + let ctrl_alt: WindowsModifiers = WindowsModifiers::CONTROL | WindowsModifiers::ALT; + let is_in_ctrl_alt = mod_state == ctrl_alt; + if !layout.has_alt_graph && is_in_ctrl_alt { + // Unwrapping here because if we are in the ctrl+alt modifier state + // then the alt modifier state must have come before. + let simple_keys = layout.keys.get(&WindowsModifiers::empty()).unwrap(); + if let Some(Key::Character(key_no_altgr)) = simple_keys.get(&key_code) { + if let Key::Character(key) = key.clone() { + layout.has_alt_graph = key != *key_no_altgr; + } + } + } + + keys_for_this_mod.insert(key_code, key); + } + layout.keys.insert(mod_state, keys_for_this_mod); + } + + // Second pass: replace right alt keys with AltGr if the layout has alt graph + if layout.has_alt_graph { + for mod_state in 0..mods_end { + let mod_state = WindowsModifiers::from_bits_truncate(mod_state); + if let Some(keys) = layout.keys.get_mut(&mod_state) { + if let Some(key) = keys.get_mut(&KeyCode::AltRight) { + *key = Key::AltGraph; + } + } + } + } + + layout + } + + fn to_unicode_string( + key_state: &[u8; 256], + vkey: VIRTUAL_KEY, + scancode: u32, + locale_id: HKL, + ) -> ToUnicodeResult { + unsafe { + let mut label_wide = [0u16; 8]; + let mut wide_len = ToUnicodeEx( + u32::from(vkey.0), + scancode, + key_state, + &mut label_wide, + 0, + Some(locale_id), + ); + if wide_len < 0 { + // If it's dead, we run `ToUnicode` again to consume the dead-key + wide_len = ToUnicodeEx( + u32::from(vkey.0), + scancode, + key_state, + &mut label_wide, + 0, + Some(locale_id), + ); + if wide_len > 0 { + let os_string = OsString::from_wide(&label_wide[0..wide_len as usize]); + if let Ok(label_str) = os_string.into_string() { + if let Some(ch) = label_str.chars().next() { + return ToUnicodeResult::Dead(Some(ch)); + } + } + } + return ToUnicodeResult::Dead(None); + } + if wide_len > 0 { + let os_string = OsString::from_wide(&label_wide[0..wide_len as usize]); + if let Ok(label_str) = os_string.into_string() { + return ToUnicodeResult::Str(label_str); + } + } + } + ToUnicodeResult::None + } +} + +pub fn get_or_insert_str(strings: &mut HashSet<&'static str>, string: T) -> &'static str +where + T: AsRef, + String: From, +{ + { + let str_ref = string.as_ref(); + if let Some(&existing) = strings.get(str_ref) { + return existing; + } + } + let leaked = Box::leak(Box::from(String::from(string))); + strings.insert(leaked); + leaked +} + +#[derive(Debug, Clone, Eq, PartialEq)] +enum ToUnicodeResult { + Str(String), + Dead(Option), + None, +} + +fn is_numpad_specific(vk: VIRTUAL_KEY) -> bool { + matches!( + vk, + win32km::VK_NUMPAD0 + | win32km::VK_NUMPAD1 + | win32km::VK_NUMPAD2 + | win32km::VK_NUMPAD3 + | win32km::VK_NUMPAD4 + | win32km::VK_NUMPAD5 + | win32km::VK_NUMPAD6 + | win32km::VK_NUMPAD7 + | win32km::VK_NUMPAD8 + | win32km::VK_NUMPAD9 + | win32km::VK_ADD + | win32km::VK_SUBTRACT + | win32km::VK_DIVIDE + | win32km::VK_DECIMAL + | win32km::VK_SEPARATOR + ) +} + +fn keycode_to_vkey(keycode: KeyCode, hkl: HKL) -> VIRTUAL_KEY { + let primary_lang_id = util::PRIMARYLANGID(hkl); + let is_korean = primary_lang_id == LANG_KOREAN; + let is_japanese = primary_lang_id == LANG_JAPANESE; + + match keycode { + KeyCode::Backquote => VIRTUAL_KEY::default(), + KeyCode::Backslash => VIRTUAL_KEY::default(), + KeyCode::BracketLeft => VIRTUAL_KEY::default(), + KeyCode::BracketRight => VIRTUAL_KEY::default(), + KeyCode::Comma => VIRTUAL_KEY::default(), + KeyCode::Digit0 => VIRTUAL_KEY::default(), + KeyCode::Digit1 => VIRTUAL_KEY::default(), + KeyCode::Digit2 => VIRTUAL_KEY::default(), + KeyCode::Digit3 => VIRTUAL_KEY::default(), + KeyCode::Digit4 => VIRTUAL_KEY::default(), + KeyCode::Digit5 => VIRTUAL_KEY::default(), + KeyCode::Digit6 => VIRTUAL_KEY::default(), + KeyCode::Digit7 => VIRTUAL_KEY::default(), + KeyCode::Digit8 => VIRTUAL_KEY::default(), + KeyCode::Digit9 => VIRTUAL_KEY::default(), + KeyCode::Equal => VIRTUAL_KEY::default(), + KeyCode::IntlBackslash => VIRTUAL_KEY::default(), + KeyCode::IntlRo => VIRTUAL_KEY::default(), + KeyCode::IntlYen => VIRTUAL_KEY::default(), + KeyCode::KeyA => VIRTUAL_KEY::default(), + KeyCode::KeyB => VIRTUAL_KEY::default(), + KeyCode::KeyC => VIRTUAL_KEY::default(), + KeyCode::KeyD => VIRTUAL_KEY::default(), + KeyCode::KeyE => VIRTUAL_KEY::default(), + KeyCode::KeyF => VIRTUAL_KEY::default(), + KeyCode::KeyG => VIRTUAL_KEY::default(), + KeyCode::KeyH => VIRTUAL_KEY::default(), + KeyCode::KeyI => VIRTUAL_KEY::default(), + KeyCode::KeyJ => VIRTUAL_KEY::default(), + KeyCode::KeyK => VIRTUAL_KEY::default(), + KeyCode::KeyL => VIRTUAL_KEY::default(), + KeyCode::KeyM => VIRTUAL_KEY::default(), + KeyCode::KeyN => VIRTUAL_KEY::default(), + KeyCode::KeyO => VIRTUAL_KEY::default(), + KeyCode::KeyP => VIRTUAL_KEY::default(), + KeyCode::KeyQ => VIRTUAL_KEY::default(), + KeyCode::KeyR => VIRTUAL_KEY::default(), + KeyCode::KeyS => VIRTUAL_KEY::default(), + KeyCode::KeyT => VIRTUAL_KEY::default(), + KeyCode::KeyU => VIRTUAL_KEY::default(), + KeyCode::KeyV => VIRTUAL_KEY::default(), + KeyCode::KeyW => VIRTUAL_KEY::default(), + KeyCode::KeyX => VIRTUAL_KEY::default(), + KeyCode::KeyY => VIRTUAL_KEY::default(), + KeyCode::KeyZ => VIRTUAL_KEY::default(), + KeyCode::Minus => VIRTUAL_KEY::default(), + KeyCode::Period => VIRTUAL_KEY::default(), + KeyCode::Quote => VIRTUAL_KEY::default(), + KeyCode::Semicolon => VIRTUAL_KEY::default(), + KeyCode::Slash => VIRTUAL_KEY::default(), + KeyCode::AltLeft => VK_LMENU, + KeyCode::AltRight => VK_RMENU, + KeyCode::Backspace => VK_BACK, + KeyCode::CapsLock => VK_CAPITAL, + KeyCode::ContextMenu => VK_APPS, + KeyCode::ControlLeft => VK_LCONTROL, + KeyCode::ControlRight => VK_RCONTROL, + KeyCode::Enter => VK_RETURN, + KeyCode::SuperLeft => VK_LWIN, + KeyCode::SuperRight => VK_RWIN, + KeyCode::ShiftLeft => VK_RSHIFT, + KeyCode::ShiftRight => VK_LSHIFT, + KeyCode::Space => VK_SPACE, + KeyCode::Tab => VK_TAB, + KeyCode::Convert => VK_CONVERT, + KeyCode::KanaMode => VK_KANA, + KeyCode::Lang1 if is_korean => VK_HANGUL, + KeyCode::Lang1 if is_japanese => VK_KANA, + KeyCode::Lang2 if is_korean => VK_HANJA, + KeyCode::Lang2 if is_japanese => VIRTUAL_KEY::default(), + KeyCode::Lang3 if is_japanese => VK_OEM_FINISH, + KeyCode::Lang4 if is_japanese => VIRTUAL_KEY::default(), + KeyCode::Lang5 if is_japanese => VIRTUAL_KEY::default(), + KeyCode::NonConvert => VK_NONCONVERT, + KeyCode::Delete => VK_DELETE, + KeyCode::End => VK_END, + KeyCode::Help => VK_HELP, + KeyCode::Home => VK_HOME, + KeyCode::Insert => VK_INSERT, + KeyCode::PageDown => VK_NEXT, + KeyCode::PageUp => VK_PRIOR, + KeyCode::ArrowDown => VK_DOWN, + KeyCode::ArrowLeft => VK_LEFT, + KeyCode::ArrowRight => VK_RIGHT, + KeyCode::ArrowUp => VK_UP, + KeyCode::NumLock => VK_NUMLOCK, + KeyCode::Numpad0 => VK_NUMPAD0, + KeyCode::Numpad1 => VK_NUMPAD1, + KeyCode::Numpad2 => VK_NUMPAD2, + KeyCode::Numpad3 => VK_NUMPAD3, + KeyCode::Numpad4 => VK_NUMPAD4, + KeyCode::Numpad5 => VK_NUMPAD5, + KeyCode::Numpad6 => VK_NUMPAD6, + KeyCode::Numpad7 => VK_NUMPAD7, + KeyCode::Numpad8 => VK_NUMPAD8, + KeyCode::Numpad9 => VK_NUMPAD9, + KeyCode::NumpadAdd => VK_ADD, + KeyCode::NumpadBackspace => VK_BACK, + KeyCode::NumpadClear => VK_CLEAR, + KeyCode::NumpadClearEntry => VIRTUAL_KEY::default(), + KeyCode::NumpadComma => VK_SEPARATOR, + KeyCode::NumpadDecimal => VK_DECIMAL, + KeyCode::NumpadDivide => VK_DIVIDE, + KeyCode::NumpadEnter => VK_RETURN, + KeyCode::NumpadEqual => VIRTUAL_KEY::default(), + KeyCode::NumpadHash => VIRTUAL_KEY::default(), + KeyCode::NumpadMemoryAdd => VIRTUAL_KEY::default(), + KeyCode::NumpadMemoryClear => VIRTUAL_KEY::default(), + KeyCode::NumpadMemoryRecall => VIRTUAL_KEY::default(), + KeyCode::NumpadMemoryStore => VIRTUAL_KEY::default(), + KeyCode::NumpadMemorySubtract => VIRTUAL_KEY::default(), + KeyCode::NumpadMultiply => VK_MULTIPLY, + KeyCode::NumpadParenLeft => VIRTUAL_KEY::default(), + KeyCode::NumpadParenRight => VIRTUAL_KEY::default(), + KeyCode::NumpadStar => VIRTUAL_KEY::default(), + KeyCode::NumpadSubtract => VK_SUBTRACT, + KeyCode::Escape => VK_ESCAPE, + KeyCode::Fn => VIRTUAL_KEY::default(), + KeyCode::FnLock => VIRTUAL_KEY::default(), + KeyCode::PrintScreen => VK_SNAPSHOT, + KeyCode::ScrollLock => VK_SCROLL, + KeyCode::Pause => VK_PAUSE, + KeyCode::BrowserBack => VK_BROWSER_BACK, + KeyCode::BrowserFavorites => VK_BROWSER_FAVORITES, + KeyCode::BrowserForward => VK_BROWSER_FORWARD, + KeyCode::BrowserHome => VK_BROWSER_HOME, + KeyCode::BrowserRefresh => VK_BROWSER_REFRESH, + KeyCode::BrowserSearch => VK_BROWSER_SEARCH, + KeyCode::BrowserStop => VK_BROWSER_STOP, + KeyCode::Eject => VIRTUAL_KEY::default(), + KeyCode::LaunchApp1 => VK_LAUNCH_APP1, + KeyCode::LaunchApp2 => VK_LAUNCH_APP2, + KeyCode::LaunchMail => VK_LAUNCH_MAIL, + KeyCode::MediaPlayPause => VK_MEDIA_PLAY_PAUSE, + KeyCode::MediaSelect => VK_LAUNCH_MEDIA_SELECT, + KeyCode::MediaStop => VK_MEDIA_STOP, + KeyCode::MediaTrackNext => VK_MEDIA_NEXT_TRACK, + KeyCode::MediaTrackPrevious => VK_MEDIA_PREV_TRACK, + KeyCode::Power => VIRTUAL_KEY::default(), + KeyCode::Sleep => VIRTUAL_KEY::default(), + KeyCode::AudioVolumeDown => VK_VOLUME_DOWN, + KeyCode::AudioVolumeMute => VK_VOLUME_MUTE, + KeyCode::AudioVolumeUp => VK_VOLUME_UP, + KeyCode::WakeUp => VIRTUAL_KEY::default(), + KeyCode::Hyper => VIRTUAL_KEY::default(), + KeyCode::Turbo => VIRTUAL_KEY::default(), + KeyCode::Abort => VIRTUAL_KEY::default(), + KeyCode::Resume => VIRTUAL_KEY::default(), + KeyCode::Suspend => VIRTUAL_KEY::default(), + KeyCode::Again => VIRTUAL_KEY::default(), + KeyCode::Copy => VIRTUAL_KEY::default(), + KeyCode::Cut => VIRTUAL_KEY::default(), + KeyCode::Find => VIRTUAL_KEY::default(), + KeyCode::Open => VIRTUAL_KEY::default(), + KeyCode::Paste => VIRTUAL_KEY::default(), + KeyCode::Props => VIRTUAL_KEY::default(), + KeyCode::Select => VK_SELECT, + KeyCode::Undo => VIRTUAL_KEY::default(), + KeyCode::Hiragana => VIRTUAL_KEY::default(), + KeyCode::Katakana => VIRTUAL_KEY::default(), + KeyCode::F1 => VK_F1, + KeyCode::F2 => VK_F2, + KeyCode::F3 => VK_F3, + KeyCode::F4 => VK_F4, + KeyCode::F5 => VK_F5, + KeyCode::F6 => VK_F6, + KeyCode::F7 => VK_F7, + KeyCode::F8 => VK_F8, + KeyCode::F9 => VK_F9, + KeyCode::F10 => VK_F10, + KeyCode::F11 => VK_F11, + KeyCode::F12 => VK_F12, + KeyCode::F13 => VK_F13, + KeyCode::F14 => VK_F14, + KeyCode::F15 => VK_F15, + KeyCode::F16 => VK_F16, + KeyCode::F17 => VK_F17, + KeyCode::F18 => VK_F18, + KeyCode::F19 => VK_F19, + KeyCode::F20 => VK_F20, + KeyCode::F21 => VK_F21, + KeyCode::F22 => VK_F22, + KeyCode::F23 => VK_F23, + KeyCode::F24 => VK_F24, + KeyCode::F25 => VIRTUAL_KEY::default(), + KeyCode::F26 => VIRTUAL_KEY::default(), + KeyCode::F27 => VIRTUAL_KEY::default(), + KeyCode::F28 => VIRTUAL_KEY::default(), + KeyCode::F29 => VIRTUAL_KEY::default(), + KeyCode::F30 => VIRTUAL_KEY::default(), + KeyCode::F31 => VIRTUAL_KEY::default(), + KeyCode::F32 => VIRTUAL_KEY::default(), + KeyCode::F33 => VIRTUAL_KEY::default(), + KeyCode::F34 => VIRTUAL_KEY::default(), + KeyCode::F35 => VIRTUAL_KEY::default(), + KeyCode::Unidentified(_) => VIRTUAL_KEY::default(), + _ => VIRTUAL_KEY::default(), + } +} + +/// This converts virtual keys to `Key`s. Only virtual keys which can be unambiguously converted to +/// a `Key`, with only the information passed in as arguments, are converted. +/// +/// In other words: this function does not need to "prepare" the current layout in order to do +/// the conversion, but as such it cannot convert certain keys, like language-specific character keys. +/// +/// The result includes all non-character keys defined within `Key` plus characters from numpad keys. +/// For example, backspace and tab are included. +fn vkey_to_non_char_key( + vkey: VIRTUAL_KEY, + native_code: NativeKeyCode, + hkl: HKL, + has_alt_graph: bool, +) -> Key<'static> { + // List of the Web key names and their corresponding platform-native key names: + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + + let primary_lang_id = util::PRIMARYLANGID(hkl); + let is_korean = primary_lang_id == LANG_KOREAN; + let is_japanese = primary_lang_id == LANG_JAPANESE; + + match vkey { + win32km::VK_LBUTTON => Key::Unidentified(NativeKeyCode::Unidentified), // Mouse + win32km::VK_RBUTTON => Key::Unidentified(NativeKeyCode::Unidentified), // Mouse + + // I don't think this can be represented with a Key + win32km::VK_CANCEL => Key::Unidentified(native_code), + + win32km::VK_MBUTTON => Key::Unidentified(NativeKeyCode::Unidentified), // Mouse + win32km::VK_XBUTTON1 => Key::Unidentified(NativeKeyCode::Unidentified), // Mouse + win32km::VK_XBUTTON2 => Key::Unidentified(NativeKeyCode::Unidentified), // Mouse + win32km::VK_BACK => Key::Backspace, + win32km::VK_TAB => Key::Tab, + win32km::VK_CLEAR => Key::Clear, + win32km::VK_RETURN => Key::Enter, + win32km::VK_SHIFT => Key::Shift, + win32km::VK_CONTROL => Key::Control, + win32km::VK_MENU => Key::Alt, + win32km::VK_PAUSE => Key::Pause, + win32km::VK_CAPITAL => Key::CapsLock, + + //win32km::VK_HANGEUL => Key::HangulMode, // Deprecated in favour of VK_HANGUL + + // VK_HANGUL and VK_KANA are defined as the same constant, therefore + // we use appropriate conditions to differentate between them + win32km::VK_HANGUL if is_korean => Key::HangulMode, + win32km::VK_KANA if is_japanese => Key::KanaMode, + + win32km::VK_JUNJA => Key::JunjaMode, + win32km::VK_FINAL => Key::FinalMode, + + // VK_HANJA and VK_KANJI are defined as the same constant, therefore + // we use appropriate conditions to differentate between them + win32km::VK_HANJA if is_korean => Key::HanjaMode, + win32km::VK_KANJI if is_japanese => Key::KanjiMode, + + win32km::VK_ESCAPE => Key::Escape, + win32km::VK_CONVERT => Key::Convert, + win32km::VK_NONCONVERT => Key::NonConvert, + win32km::VK_ACCEPT => Key::Accept, + win32km::VK_MODECHANGE => Key::ModeChange, + win32km::VK_SPACE => Key::Space, + win32km::VK_PRIOR => Key::PageUp, + win32km::VK_NEXT => Key::PageDown, + win32km::VK_END => Key::End, + win32km::VK_HOME => Key::Home, + win32km::VK_LEFT => Key::ArrowLeft, + win32km::VK_UP => Key::ArrowUp, + win32km::VK_RIGHT => Key::ArrowRight, + win32km::VK_DOWN => Key::ArrowDown, + win32km::VK_SELECT => Key::Select, + win32km::VK_PRINT => Key::Print, + win32km::VK_EXECUTE => Key::Execute, + win32km::VK_SNAPSHOT => Key::PrintScreen, + win32km::VK_INSERT => Key::Insert, + win32km::VK_DELETE => Key::Delete, + win32km::VK_HELP => Key::Help, + win32km::VK_LWIN => Key::Super, + win32km::VK_RWIN => Key::Super, + win32km::VK_APPS => Key::ContextMenu, + win32km::VK_SLEEP => Key::Standby, + + // Numpad keys produce characters + win32km::VK_NUMPAD0 => Key::Unidentified(native_code), + win32km::VK_NUMPAD1 => Key::Unidentified(native_code), + win32km::VK_NUMPAD2 => Key::Unidentified(native_code), + win32km::VK_NUMPAD3 => Key::Unidentified(native_code), + win32km::VK_NUMPAD4 => Key::Unidentified(native_code), + win32km::VK_NUMPAD5 => Key::Unidentified(native_code), + win32km::VK_NUMPAD6 => Key::Unidentified(native_code), + win32km::VK_NUMPAD7 => Key::Unidentified(native_code), + win32km::VK_NUMPAD8 => Key::Unidentified(native_code), + win32km::VK_NUMPAD9 => Key::Unidentified(native_code), + win32km::VK_MULTIPLY => Key::Unidentified(native_code), + win32km::VK_ADD => Key::Unidentified(native_code), + win32km::VK_SEPARATOR => Key::Unidentified(native_code), + win32km::VK_SUBTRACT => Key::Unidentified(native_code), + win32km::VK_DECIMAL => Key::Unidentified(native_code), + win32km::VK_DIVIDE => Key::Unidentified(native_code), + + win32km::VK_F1 => Key::F1, + win32km::VK_F2 => Key::F2, + win32km::VK_F3 => Key::F3, + win32km::VK_F4 => Key::F4, + win32km::VK_F5 => Key::F5, + win32km::VK_F6 => Key::F6, + win32km::VK_F7 => Key::F7, + win32km::VK_F8 => Key::F8, + win32km::VK_F9 => Key::F9, + win32km::VK_F10 => Key::F10, + win32km::VK_F11 => Key::F11, + win32km::VK_F12 => Key::F12, + win32km::VK_F13 => Key::F13, + win32km::VK_F14 => Key::F14, + win32km::VK_F15 => Key::F15, + win32km::VK_F16 => Key::F16, + win32km::VK_F17 => Key::F17, + win32km::VK_F18 => Key::F18, + win32km::VK_F19 => Key::F19, + win32km::VK_F20 => Key::F20, + win32km::VK_F21 => Key::F21, + win32km::VK_F22 => Key::F22, + win32km::VK_F23 => Key::F23, + win32km::VK_F24 => Key::F24, + win32km::VK_NAVIGATION_VIEW => Key::Unidentified(native_code), + win32km::VK_NAVIGATION_MENU => Key::Unidentified(native_code), + win32km::VK_NAVIGATION_UP => Key::Unidentified(native_code), + win32km::VK_NAVIGATION_DOWN => Key::Unidentified(native_code), + win32km::VK_NAVIGATION_LEFT => Key::Unidentified(native_code), + win32km::VK_NAVIGATION_RIGHT => Key::Unidentified(native_code), + win32km::VK_NAVIGATION_ACCEPT => Key::Unidentified(native_code), + win32km::VK_NAVIGATION_CANCEL => Key::Unidentified(native_code), + win32km::VK_NUMLOCK => Key::NumLock, + win32km::VK_SCROLL => Key::ScrollLock, + win32km::VK_OEM_NEC_EQUAL => Key::Unidentified(native_code), + //win32km::VK_OEM_FJ_JISHO => Key::Unidentified(native_code), // Conflicts with `VK_OEM_NEC_EQUAL` + win32km::VK_OEM_FJ_MASSHOU => Key::Unidentified(native_code), + win32km::VK_OEM_FJ_TOUROKU => Key::Unidentified(native_code), + win32km::VK_OEM_FJ_LOYA => Key::Unidentified(native_code), + win32km::VK_OEM_FJ_ROYA => Key::Unidentified(native_code), + win32km::VK_LSHIFT => Key::Shift, + win32km::VK_RSHIFT => Key::Shift, + win32km::VK_LCONTROL => Key::Control, + win32km::VK_RCONTROL => Key::Control, + win32km::VK_LMENU => Key::Alt, + win32km::VK_RMENU => { + if has_alt_graph { + Key::AltGraph + } else { + Key::Alt + } + } + win32km::VK_BROWSER_BACK => Key::BrowserBack, + win32km::VK_BROWSER_FORWARD => Key::BrowserForward, + win32km::VK_BROWSER_REFRESH => Key::BrowserRefresh, + win32km::VK_BROWSER_STOP => Key::BrowserStop, + win32km::VK_BROWSER_SEARCH => Key::BrowserSearch, + win32km::VK_BROWSER_FAVORITES => Key::BrowserFavorites, + win32km::VK_BROWSER_HOME => Key::BrowserHome, + win32km::VK_VOLUME_MUTE => Key::AudioVolumeMute, + win32km::VK_VOLUME_DOWN => Key::AudioVolumeDown, + win32km::VK_VOLUME_UP => Key::AudioVolumeUp, + win32km::VK_MEDIA_NEXT_TRACK => Key::MediaTrackNext, + win32km::VK_MEDIA_PREV_TRACK => Key::MediaTrackPrevious, + win32km::VK_MEDIA_STOP => Key::MediaStop, + win32km::VK_MEDIA_PLAY_PAUSE => Key::MediaPlayPause, + win32km::VK_LAUNCH_MAIL => Key::LaunchMail, + win32km::VK_LAUNCH_MEDIA_SELECT => Key::LaunchMediaPlayer, + win32km::VK_LAUNCH_APP1 => Key::LaunchApplication1, + win32km::VK_LAUNCH_APP2 => Key::LaunchApplication2, + + // This function only converts "non-printable" + win32km::VK_OEM_1 => Key::Unidentified(native_code), + win32km::VK_OEM_PLUS => Key::Unidentified(native_code), + win32km::VK_OEM_COMMA => Key::Unidentified(native_code), + win32km::VK_OEM_MINUS => Key::Unidentified(native_code), + win32km::VK_OEM_PERIOD => Key::Unidentified(native_code), + win32km::VK_OEM_2 => Key::Unidentified(native_code), + win32km::VK_OEM_3 => Key::Unidentified(native_code), + + win32km::VK_GAMEPAD_A => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_B => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_X => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_Y => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_RIGHT_SHOULDER => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_LEFT_SHOULDER => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_LEFT_TRIGGER => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_RIGHT_TRIGGER => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_DPAD_UP => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_DPAD_DOWN => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_DPAD_LEFT => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_DPAD_RIGHT => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_MENU => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_VIEW => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_LEFT_THUMBSTICK_BUTTON => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_RIGHT_THUMBSTICK_BUTTON => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_LEFT_THUMBSTICK_UP => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_LEFT_THUMBSTICK_DOWN => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_LEFT_THUMBSTICK_RIGHT => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_LEFT_THUMBSTICK_LEFT => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_RIGHT_THUMBSTICK_UP => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_RIGHT_THUMBSTICK_DOWN => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_RIGHT_THUMBSTICK_RIGHT => Key::Unidentified(native_code), + win32km::VK_GAMEPAD_RIGHT_THUMBSTICK_LEFT => Key::Unidentified(native_code), + + // This function only converts "non-printable" + win32km::VK_OEM_4 => Key::Unidentified(native_code), + win32km::VK_OEM_5 => Key::Unidentified(native_code), + win32km::VK_OEM_6 => Key::Unidentified(native_code), + win32km::VK_OEM_7 => Key::Unidentified(native_code), + win32km::VK_OEM_8 => Key::Unidentified(native_code), + win32km::VK_OEM_AX => Key::Unidentified(native_code), + win32km::VK_OEM_102 => Key::Unidentified(native_code), + + win32km::VK_ICO_HELP => Key::Unidentified(native_code), + win32km::VK_ICO_00 => Key::Unidentified(native_code), + + win32km::VK_PROCESSKEY => Key::Process, + + win32km::VK_ICO_CLEAR => Key::Unidentified(native_code), + win32km::VK_PACKET => Key::Unidentified(native_code), + win32km::VK_OEM_RESET => Key::Unidentified(native_code), + win32km::VK_OEM_JUMP => Key::Unidentified(native_code), + win32km::VK_OEM_PA1 => Key::Unidentified(native_code), + win32km::VK_OEM_PA2 => Key::Unidentified(native_code), + win32km::VK_OEM_PA3 => Key::Unidentified(native_code), + win32km::VK_OEM_WSCTRL => Key::Unidentified(native_code), + win32km::VK_OEM_CUSEL => Key::Unidentified(native_code), + + win32km::VK_OEM_ATTN => Key::Attn, + win32km::VK_OEM_FINISH => { + if is_japanese { + Key::Katakana + } else { + // This matches IE and Firefox behaviour according to + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + // At the time of writing, there is no `Key::Finish` variant as + // Finish is not mentionned at https://w3c.github.io/uievents-key/ + // Also see: https://github.com/pyfisch/keyboard-types/issues/9 + Key::Unidentified(native_code) + } + } + win32km::VK_OEM_COPY => Key::Copy, + win32km::VK_OEM_AUTO => Key::Hankaku, + win32km::VK_OEM_ENLW => Key::Zenkaku, + win32km::VK_OEM_BACKTAB => Key::Romaji, + win32km::VK_ATTN => Key::KanaMode, + win32km::VK_CRSEL => Key::CrSel, + win32km::VK_EXSEL => Key::ExSel, + win32km::VK_EREOF => Key::EraseEof, + win32km::VK_PLAY => Key::Play, + win32km::VK_ZOOM => Key::ZoomToggle, + win32km::VK_NONAME => Key::Unidentified(native_code), + win32km::VK_PA1 => Key::Unidentified(native_code), + win32km::VK_OEM_CLEAR => Key::Clear, + _ => Key::Unidentified(native_code), + } +} diff --git a/vendor/tao/src/platform_impl/windows/keycode.rs b/vendor/tao/src/platform_impl/windows/keycode.rs new file mode 100644 index 0000000000..df8855bb0c --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/keycode.rs @@ -0,0 +1,338 @@ +use crate::{ + keyboard::{KeyCode, NativeKeyCode}, + platform_impl::platform::util, +}; +use windows::Win32::{ + System::SystemServices::LANG_KOREAN, UI::Input::KeyboardAndMouse::GetKeyboardLayout, +}; + +pub fn keycode_to_scancode(code: KeyCode) -> Option { + // See `from_scancode` for more info + let hkl = unsafe { GetKeyboardLayout(0) }; + + let primary_lang_id = util::PRIMARYLANGID(hkl); + let is_korean = primary_lang_id == LANG_KOREAN; + + match code { + KeyCode::Backquote => Some(0x0029), + KeyCode::Backslash => Some(0x002B), + KeyCode::Backspace => Some(0x000E), + KeyCode::BracketLeft => Some(0x001A), + KeyCode::BracketRight => Some(0x001B), + KeyCode::Comma => Some(0x0033), + KeyCode::Digit0 => Some(0x000B), + KeyCode::Digit1 => Some(0x0002), + KeyCode::Digit2 => Some(0x0003), + KeyCode::Digit3 => Some(0x0004), + KeyCode::Digit4 => Some(0x0005), + KeyCode::Digit5 => Some(0x0006), + KeyCode::Digit6 => Some(0x0007), + KeyCode::Digit7 => Some(0x0008), + KeyCode::Digit8 => Some(0x0009), + KeyCode::Digit9 => Some(0x000A), + KeyCode::Equal => Some(0x000D), + KeyCode::IntlBackslash => Some(0x0056), + KeyCode::IntlRo => Some(0x0073), + KeyCode::IntlYen => Some(0x007D), + KeyCode::KeyA => Some(0x001E), + KeyCode::KeyB => Some(0x0030), + KeyCode::KeyC => Some(0x002E), + KeyCode::KeyD => Some(0x0020), + KeyCode::KeyE => Some(0x0012), + KeyCode::KeyF => Some(0x0021), + KeyCode::KeyG => Some(0x0022), + KeyCode::KeyH => Some(0x0023), + KeyCode::KeyI => Some(0x0017), + KeyCode::KeyJ => Some(0x0024), + KeyCode::KeyK => Some(0x0025), + KeyCode::KeyL => Some(0x0026), + KeyCode::KeyM => Some(0x0032), + KeyCode::KeyN => Some(0x0031), + KeyCode::KeyO => Some(0x0018), + KeyCode::KeyP => Some(0x0019), + KeyCode::KeyQ => Some(0x0010), + KeyCode::KeyR => Some(0x0013), + KeyCode::KeyS => Some(0x001F), + KeyCode::KeyT => Some(0x0014), + KeyCode::KeyU => Some(0x0016), + KeyCode::KeyV => Some(0x002F), + KeyCode::KeyW => Some(0x0011), + KeyCode::KeyX => Some(0x002D), + KeyCode::KeyY => Some(0x0015), + KeyCode::KeyZ => Some(0x002C), + KeyCode::Minus => Some(0x000C), + KeyCode::Period => Some(0x0034), + KeyCode::Quote => Some(0x0028), + KeyCode::Semicolon => Some(0x0027), + KeyCode::Slash => Some(0x0035), + KeyCode::AltLeft => Some(0x0038), + KeyCode::AltRight => Some(0xE038), + KeyCode::CapsLock => Some(0x003A), + KeyCode::ContextMenu => Some(0xE05D), + KeyCode::ControlLeft => Some(0x001D), + KeyCode::ControlRight => Some(0xE01D), + KeyCode::Enter => Some(0x001C), + KeyCode::SuperLeft => Some(0xE05B), + KeyCode::SuperRight => Some(0xE05C), + KeyCode::ShiftLeft => Some(0x002A), + KeyCode::ShiftRight => Some(0x0036), + KeyCode::Space => Some(0x0039), + KeyCode::Tab => Some(0x000F), + KeyCode::Convert => Some(0x0079), + KeyCode::Lang1 => { + if is_korean { + Some(0xE0F2) + } else { + Some(0x0072) + } + } + KeyCode::Lang2 => { + if is_korean { + Some(0xE0F1) + } else { + Some(0x0071) + } + } + KeyCode::KanaMode => Some(0x0070), + KeyCode::NonConvert => Some(0x007B), + KeyCode::Delete => Some(0xE053), + KeyCode::End => Some(0xE04F), + KeyCode::Home => Some(0xE047), + KeyCode::Insert => Some(0xE052), + KeyCode::PageDown => Some(0xE051), + KeyCode::PageUp => Some(0xE049), + KeyCode::ArrowDown => Some(0xE050), + KeyCode::ArrowLeft => Some(0xE04B), + KeyCode::ArrowRight => Some(0xE04D), + KeyCode::ArrowUp => Some(0xE048), + KeyCode::NumLock => Some(0xE045), + KeyCode::Numpad0 => Some(0x0052), + KeyCode::Numpad1 => Some(0x004F), + KeyCode::Numpad2 => Some(0x0050), + KeyCode::Numpad3 => Some(0x0051), + KeyCode::Numpad4 => Some(0x004B), + KeyCode::Numpad5 => Some(0x004C), + KeyCode::Numpad6 => Some(0x004D), + KeyCode::Numpad7 => Some(0x0047), + KeyCode::Numpad8 => Some(0x0048), + KeyCode::Numpad9 => Some(0x0049), + KeyCode::NumpadAdd => Some(0x004E), + KeyCode::NumpadComma => Some(0x007E), + KeyCode::NumpadDecimal => Some(0x0053), + KeyCode::NumpadDivide => Some(0xE035), + KeyCode::NumpadEnter => Some(0xE01C), + KeyCode::NumpadEqual => Some(0x0059), + KeyCode::NumpadMultiply => Some(0x0037), + KeyCode::NumpadSubtract => Some(0x004A), + KeyCode::Escape => Some(0x0001), + KeyCode::F1 => Some(0x003B), + KeyCode::F2 => Some(0x003C), + KeyCode::F3 => Some(0x003D), + KeyCode::F4 => Some(0x003E), + KeyCode::F5 => Some(0x003F), + KeyCode::F6 => Some(0x0040), + KeyCode::F7 => Some(0x0041), + KeyCode::F8 => Some(0x0042), + KeyCode::F9 => Some(0x0043), + KeyCode::F10 => Some(0x0044), + KeyCode::F11 => Some(0x0057), + KeyCode::F12 => Some(0x0058), + KeyCode::F13 => Some(0x0064), + KeyCode::F14 => Some(0x0065), + KeyCode::F15 => Some(0x0066), + KeyCode::F16 => Some(0x0067), + KeyCode::F17 => Some(0x0068), + KeyCode::F18 => Some(0x0069), + KeyCode::F19 => Some(0x006A), + KeyCode::F20 => Some(0x006B), + KeyCode::F21 => Some(0x006C), + KeyCode::F22 => Some(0x006D), + KeyCode::F23 => Some(0x006E), + KeyCode::F24 => Some(0x0076), + KeyCode::PrintScreen => Some(0xE037), + //KeyCode::PrintScreen => Some(0x0054), // Alt + PrintScreen + KeyCode::ScrollLock => Some(0x0046), + KeyCode::Pause => Some(0x0045), + //KeyCode::Pause => Some(0xE046), // Ctrl + Pause + KeyCode::BrowserBack => Some(0xE06A), + KeyCode::BrowserFavorites => Some(0xE066), + KeyCode::BrowserForward => Some(0xE069), + KeyCode::BrowserHome => Some(0xE032), + KeyCode::BrowserRefresh => Some(0xE067), + KeyCode::BrowserSearch => Some(0xE065), + KeyCode::BrowserStop => Some(0xE068), + KeyCode::LaunchApp1 => Some(0xE06B), + KeyCode::LaunchApp2 => Some(0xE021), + KeyCode::LaunchMail => Some(0xE06C), + KeyCode::MediaPlayPause => Some(0xE022), + KeyCode::MediaSelect => Some(0xE06D), + KeyCode::MediaStop => Some(0xE024), + KeyCode::MediaTrackNext => Some(0xE019), + KeyCode::MediaTrackPrevious => Some(0xE010), + KeyCode::Power => Some(0xE05E), + KeyCode::AudioVolumeDown => Some(0xE02E), + KeyCode::AudioVolumeMute => Some(0xE020), + KeyCode::AudioVolumeUp => Some(0xE030), + KeyCode::Unidentified(NativeKeyCode::Windows(scancode)) => Some(scancode as u32), + _ => None, + } +} + +pub fn keycode_from_scancode(scancode: u32) -> KeyCode { + // See: https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html + // and: https://www.w3.org/TR/uievents-code/ + // and: The widget/NativeKeyToDOMCodeName.h file in the firefox source + + match scancode { + 0x0029 => KeyCode::Backquote, + 0x002B => KeyCode::Backslash, + 0x000E => KeyCode::Backspace, + 0x001A => KeyCode::BracketLeft, + 0x001B => KeyCode::BracketRight, + 0x0033 => KeyCode::Comma, + 0x000B => KeyCode::Digit0, + 0x0002 => KeyCode::Digit1, + 0x0003 => KeyCode::Digit2, + 0x0004 => KeyCode::Digit3, + 0x0005 => KeyCode::Digit4, + 0x0006 => KeyCode::Digit5, + 0x0007 => KeyCode::Digit6, + 0x0008 => KeyCode::Digit7, + 0x0009 => KeyCode::Digit8, + 0x000A => KeyCode::Digit9, + 0x000D => KeyCode::Equal, + 0x0056 => KeyCode::IntlBackslash, + 0x0073 => KeyCode::IntlRo, + 0x007D => KeyCode::IntlYen, + 0x001E => KeyCode::KeyA, + 0x0030 => KeyCode::KeyB, + 0x002E => KeyCode::KeyC, + 0x0020 => KeyCode::KeyD, + 0x0012 => KeyCode::KeyE, + 0x0021 => KeyCode::KeyF, + 0x0022 => KeyCode::KeyG, + 0x0023 => KeyCode::KeyH, + 0x0017 => KeyCode::KeyI, + 0x0024 => KeyCode::KeyJ, + 0x0025 => KeyCode::KeyK, + 0x0026 => KeyCode::KeyL, + 0x0032 => KeyCode::KeyM, + 0x0031 => KeyCode::KeyN, + 0x0018 => KeyCode::KeyO, + 0x0019 => KeyCode::KeyP, + 0x0010 => KeyCode::KeyQ, + 0x0013 => KeyCode::KeyR, + 0x001F => KeyCode::KeyS, + 0x0014 => KeyCode::KeyT, + 0x0016 => KeyCode::KeyU, + 0x002F => KeyCode::KeyV, + 0x0011 => KeyCode::KeyW, + 0x002D => KeyCode::KeyX, + 0x0015 => KeyCode::KeyY, + 0x002C => KeyCode::KeyZ, + 0x000C => KeyCode::Minus, + 0x0034 => KeyCode::Period, + 0x0028 => KeyCode::Quote, + 0x0027 => KeyCode::Semicolon, + 0x0035 => KeyCode::Slash, + 0x0038 => KeyCode::AltLeft, + 0xE038 => KeyCode::AltRight, + 0x003A => KeyCode::CapsLock, + 0xE05D => KeyCode::ContextMenu, + 0x001D => KeyCode::ControlLeft, + 0xE01D => KeyCode::ControlRight, + 0x001C => KeyCode::Enter, + 0xE05B => KeyCode::SuperLeft, + 0xE05C => KeyCode::SuperRight, + 0x002A => KeyCode::ShiftLeft, + 0x0036 => KeyCode::ShiftRight, + 0x0039 => KeyCode::Space, + 0x000F => KeyCode::Tab, + 0x0079 => KeyCode::Convert, + 0x0072 => KeyCode::Lang1, // for non-Korean layout + 0xE0F2 => KeyCode::Lang1, // for Korean layout + 0x0071 => KeyCode::Lang2, // for non-Korean layout + 0xE0F1 => KeyCode::Lang2, // for Korean layout + 0x0070 => KeyCode::KanaMode, + 0x007B => KeyCode::NonConvert, + 0xE053 => KeyCode::Delete, + 0xE04F => KeyCode::End, + 0xE047 => KeyCode::Home, + 0xE052 => KeyCode::Insert, + 0xE051 => KeyCode::PageDown, + 0xE049 => KeyCode::PageUp, + 0xE050 => KeyCode::ArrowDown, + 0xE04B => KeyCode::ArrowLeft, + 0xE04D => KeyCode::ArrowRight, + 0xE048 => KeyCode::ArrowUp, + 0xE045 => KeyCode::NumLock, + 0x0052 => KeyCode::Numpad0, + 0x004F => KeyCode::Numpad1, + 0x0050 => KeyCode::Numpad2, + 0x0051 => KeyCode::Numpad3, + 0x004B => KeyCode::Numpad4, + 0x004C => KeyCode::Numpad5, + 0x004D => KeyCode::Numpad6, + 0x0047 => KeyCode::Numpad7, + 0x0048 => KeyCode::Numpad8, + 0x0049 => KeyCode::Numpad9, + 0x004E => KeyCode::NumpadAdd, + 0x007E => KeyCode::NumpadComma, + 0x0053 => KeyCode::NumpadDecimal, + 0xE035 => KeyCode::NumpadDivide, + 0xE01C => KeyCode::NumpadEnter, + 0x0059 => KeyCode::NumpadEqual, + 0x0037 => KeyCode::NumpadMultiply, + 0x004A => KeyCode::NumpadSubtract, + 0x0001 => KeyCode::Escape, + 0x003B => KeyCode::F1, + 0x003C => KeyCode::F2, + 0x003D => KeyCode::F3, + 0x003E => KeyCode::F4, + 0x003F => KeyCode::F5, + 0x0040 => KeyCode::F6, + 0x0041 => KeyCode::F7, + 0x0042 => KeyCode::F8, + 0x0043 => KeyCode::F9, + 0x0044 => KeyCode::F10, + 0x0057 => KeyCode::F11, + 0x0058 => KeyCode::F12, + 0x0064 => KeyCode::F13, + 0x0065 => KeyCode::F14, + 0x0066 => KeyCode::F15, + 0x0067 => KeyCode::F16, + 0x0068 => KeyCode::F17, + 0x0069 => KeyCode::F18, + 0x006A => KeyCode::F19, + 0x006B => KeyCode::F20, + 0x006C => KeyCode::F21, + 0x006D => KeyCode::F22, + 0x006E => KeyCode::F23, + 0x0076 => KeyCode::F24, + 0xE037 => KeyCode::PrintScreen, + 0x0054 => KeyCode::PrintScreen, // Alt + PrintScreen + 0x0046 => KeyCode::ScrollLock, + 0x0045 => KeyCode::Pause, + 0xE046 => KeyCode::Pause, // Ctrl + Pause + 0xE06A => KeyCode::BrowserBack, + 0xE066 => KeyCode::BrowserFavorites, + 0xE069 => KeyCode::BrowserForward, + 0xE032 => KeyCode::BrowserHome, + 0xE067 => KeyCode::BrowserRefresh, + 0xE065 => KeyCode::BrowserSearch, + 0xE068 => KeyCode::BrowserStop, + 0xE06B => KeyCode::LaunchApp1, + 0xE021 => KeyCode::LaunchApp2, + 0xE06C => KeyCode::LaunchMail, + 0xE022 => KeyCode::MediaPlayPause, + 0xE06D => KeyCode::MediaSelect, + 0xE024 => KeyCode::MediaStop, + 0xE019 => KeyCode::MediaTrackNext, + 0xE010 => KeyCode::MediaTrackPrevious, + 0xE05E => KeyCode::Power, + 0xE02E => KeyCode::AudioVolumeDown, + 0xE020 => KeyCode::AudioVolumeMute, + 0xE030 => KeyCode::AudioVolumeUp, + _ => KeyCode::Unidentified(NativeKeyCode::Windows(scancode as u16)), + } +} diff --git a/vendor/tao/src/platform_impl/windows/minimal_ime.rs b/vendor/tao/src/platform_impl/windows/minimal_ime.rs new file mode 100644 index 0000000000..0e0f1d2180 --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/minimal_ime.rs @@ -0,0 +1,88 @@ +use std::mem::MaybeUninit; + +use windows::Win32::{ + Foundation::{HWND, LPARAM, LRESULT, WPARAM}, + UI::WindowsAndMessaging::{self as win32wm, *}, +}; + +use crate::platform_impl::platform::event_loop::ProcResult; + +pub fn is_msg_ime_related(msg_kind: u32) -> bool { + matches!( + msg_kind, + win32wm::WM_IME_COMPOSITION + | win32wm::WM_IME_COMPOSITIONFULL + | win32wm::WM_IME_STARTCOMPOSITION + | win32wm::WM_IME_ENDCOMPOSITION + | win32wm::WM_IME_CHAR + | win32wm::WM_CHAR + | win32wm::WM_SYSCHAR + ) +} + +pub struct MinimalIme { + // True if we're currently receiving messages belonging to a finished IME session. + getting_ime_text: bool, + + utf16parts: Vec, +} +impl Default for MinimalIme { + fn default() -> Self { + MinimalIme { + getting_ime_text: false, + utf16parts: Vec::with_capacity(16), + } + } +} +impl MinimalIme { + pub(crate) fn process_message( + &mut self, + hwnd: HWND, + msg_kind: u32, + wparam: WPARAM, + _lparam: LPARAM, + result: &mut ProcResult, + ) -> Option { + match msg_kind { + win32wm::WM_IME_ENDCOMPOSITION => { + self.getting_ime_text = true; + } + win32wm::WM_CHAR | win32wm::WM_SYSCHAR => { + *result = ProcResult::Value(LRESULT(0)); + if self.getting_ime_text { + self.utf16parts.push(wparam.0 as u16); + + let more_char_coming; + unsafe { + let mut next_msg = MaybeUninit::uninit(); + let has_message = PeekMessageW( + next_msg.as_mut_ptr(), + Some(hwnd), + WM_KEYFIRST, + WM_KEYLAST, + PM_NOREMOVE, + ); + let has_message = has_message.as_bool(); + if !has_message { + more_char_coming = false; + } else { + let next_msg = next_msg.assume_init().message; + more_char_coming = next_msg == WM_CHAR || next_msg == WM_SYSCHAR; + } + } + if !more_char_coming { + let result = String::from_utf16(&self.utf16parts).ok(); + self.utf16parts.clear(); + self.getting_ime_text = false; + return result; + } + } else { + return String::from_utf16(&[wparam.0 as u16]).ok(); + } + } + _ => (), + } + + None + } +} diff --git a/vendor/tao/src/platform_impl/windows/mod.rs b/vendor/tao/src/platform_impl/windows/mod.rs new file mode 100644 index 0000000000..8cd4d97a1e --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/mod.rs @@ -0,0 +1,157 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use windows::Win32::{ + Foundation::{HANDLE, HWND}, + UI::WindowsAndMessaging::HMENU, +}; + +pub(crate) use self::{ + event_loop::{ + EventLoop, EventLoopProxy, EventLoopWindowTarget, PlatformSpecificEventLoopAttributes, + }, + icon::WinIcon, + keycode::{keycode_from_scancode, keycode_to_scancode}, + monitor::{MonitorHandle, VideoMode}, + window::Window, +}; + +pub use self::icon::WinIcon as PlatformIcon; + +use crate::{event::DeviceId as RootDeviceId, icon::Icon, keyboard::Key}; +mod keycode; + +#[non_exhaustive] +#[derive(Clone)] +pub enum Parent { + None, + ChildOf(HWND), + OwnedBy(HWND), +} + +#[derive(Clone)] +pub struct PlatformSpecificWindowBuilderAttributes { + pub parent: Parent, + pub menu: Option, + pub taskbar_icon: Option, + pub skip_taskbar: bool, + pub window_classname: String, + pub no_redirection_bitmap: bool, + pub drag_and_drop: bool, + pub decoration_shadow: bool, + pub rtl: bool, +} + +impl Default for PlatformSpecificWindowBuilderAttributes { + fn default() -> Self { + Self { + parent: Parent::None, + menu: None, + taskbar_icon: None, + no_redirection_bitmap: false, + drag_and_drop: true, + skip_taskbar: false, + window_classname: "Window Class".to_string(), + decoration_shadow: true, + rtl: false, + } + } +} + +unsafe impl Send for PlatformSpecificWindowBuilderAttributes {} +unsafe impl Sync for PlatformSpecificWindowBuilderAttributes {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(isize); + +impl DeviceId { + pub unsafe fn dummy() -> Self { + DeviceId(0) + } +} + +impl DeviceId { + pub fn persistent_identifier(&self) -> Option { + if self.0 != 0 { + raw_input::get_raw_input_device_name(HANDLE(self.0 as _)) + } else { + None + } + } +} + +#[non_exhaustive] +#[derive(Debug)] +pub enum OsError { + #[allow(unused)] + CreationError(&'static str), + IoError(std::io::Error), +} +impl std::error::Error for OsError {} + +impl std::fmt::Display for OsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OsError::CreationError(e) => f.pad(e), + OsError::IoError(e) => f.pad(&e.to_string()), + } + } +} + +impl From for OsError { + fn from(value: windows::core::Error) -> Self { + OsError::IoError(value.into()) + } +} + +impl From for crate::error::OsError { + fn from(value: windows::core::Error) -> Self { + os_error!(OsError::IoError(value.into())) + } +} + +impl From for crate::error::ExternalError { + fn from(value: windows::core::Error) -> Self { + crate::error::ExternalError::Os(os_error!(OsError::IoError(value.into()))) + } +} + +// Constant device ID, to be removed when this backend is updated to report real device IDs. +const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId(0)); + +fn wrap_device_id(id: isize) -> RootDeviceId { + RootDeviceId(DeviceId(id)) +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option<&'static str>, + pub key_without_modifiers: Key<'static>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(isize); +unsafe impl Send for WindowId {} +unsafe impl Sync for WindowId {} + +impl WindowId { + pub unsafe fn dummy() -> Self { + WindowId(0) + } +} + +#[macro_use] +mod util; +mod dark_mode; +mod dpi; +mod drop_handler; +mod event_loop; +mod icon; +mod keyboard; +mod keyboard_layout; +mod minimal_ime; +mod monitor; +mod raw_input; +mod window; +mod window_state; diff --git a/vendor/tao/src/platform_impl/windows/monitor.rs b/vendor/tao/src/platform_impl/windows/monitor.rs new file mode 100644 index 0000000000..1f5b54cd44 --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/monitor.rs @@ -0,0 +1,270 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use windows::{ + core::{BOOL, PCWSTR}, + Win32::{ + Foundation::{HWND, LPARAM, POINT, RECT}, + Graphics::Gdi::*, + UI::WindowsAndMessaging::USER_DEFAULT_SCREEN_DPI, + }, +}; + +use std::{ + collections::{BTreeSet, VecDeque}, + io, mem, +}; + +use super::util; +use crate::{ + dpi::{PhysicalPosition, PhysicalSize}, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform_impl::platform::{ + dpi::{dpi_to_scale_factor, get_monitor_dpi}, + window::Window, + }, +}; + +#[derive(Clone)] +pub struct VideoMode { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, + pub(crate) monitor: MonitorHandle, + pub(crate) native_video_mode: DEVMODEW, +} + +impl PartialEq for VideoMode { + fn eq(&self, other: &Self) -> bool { + self.size == other.size + && self.bit_depth == other.bit_depth + && self.refresh_rate == other.refresh_rate + && self.monitor == other.monitor + } +} + +impl Eq for VideoMode {} + +impl std::hash::Hash for VideoMode { + fn hash(&self, state: &mut H) { + self.size.hash(state); + self.bit_depth.hash(state); + self.refresh_rate.hash(state); + self.monitor.hash(state); + } +} + +impl std::fmt::Debug for VideoMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VideoMode") + .field("size", &self.size) + .field("bit_depth", &self.bit_depth) + .field("refresh_rate", &self.refresh_rate) + .field("monitor", &self.monitor) + .finish() + } +} + +impl VideoMode { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: self.monitor.clone(), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub struct MonitorHandle(isize); + +unsafe extern "system" fn monitor_enum_proc( + hmonitor: HMONITOR, + _hdc: HDC, + _place: *mut RECT, + data: LPARAM, +) -> BOOL { + let monitors = data.0 as *mut VecDeque; + (*monitors).push_back(MonitorHandle::new(hmonitor)); + true.into() // continue enumeration +} + +pub fn available_monitors() -> VecDeque { + let mut monitors: VecDeque = VecDeque::new(); + unsafe { + let _ = EnumDisplayMonitors( + None, + None, + Some(monitor_enum_proc), + LPARAM(&mut monitors as *mut _ as _), + ); + } + monitors +} + +pub fn primary_monitor() -> MonitorHandle { + const ORIGIN: POINT = POINT { x: 0, y: 0 }; + let hmonitor = unsafe { MonitorFromPoint(ORIGIN, MONITOR_DEFAULTTOPRIMARY) }; + MonitorHandle::new(hmonitor) +} + +pub fn current_monitor(hwnd: HWND) -> MonitorHandle { + let hmonitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + MonitorHandle::new(hmonitor) +} + +pub fn from_point(x: f64, y: f64) -> Option { + let hmonitor = unsafe { + MonitorFromPoint( + POINT { + x: x as i32, + y: y as i32, + }, + MONITOR_DEFAULTTONULL, + ) + }; + if !hmonitor.is_invalid() { + Some(MonitorHandle::new(hmonitor)) + } else { + None + } +} + +impl Window { + pub fn available_monitors(&self) -> VecDeque { + available_monitors() + } + + pub fn primary_monitor(&self) -> Option { + let monitor = primary_monitor(); + Some(RootMonitorHandle { inner: monitor }) + } + + pub fn monitor_from_point(&self, x: f64, y: f64) -> Option { + from_point(x, y).map(|inner| RootMonitorHandle { inner }) + } +} + +pub(crate) fn get_monitor_info(hmonitor: HMONITOR) -> Result { + let mut monitor_info = MONITORINFOEXW::default(); + monitor_info.monitorInfo.cbSize = mem::size_of::() as u32; + let status = unsafe { + GetMonitorInfoW( + hmonitor, + &mut monitor_info as *mut MONITORINFOEXW as *mut MONITORINFO, + ) + }; + if !status.as_bool() { + Err(io::Error::last_os_error()) + } else { + Ok(monitor_info) + } +} + +impl MonitorHandle { + pub(crate) fn new(hmonitor: HMONITOR) -> Self { + MonitorHandle(hmonitor.0 as _) + } + + #[inline] + pub fn name(&self) -> Option { + let monitor_info = get_monitor_info(self.hmonitor()).unwrap(); + Some(util::wchar_ptr_to_string(PCWSTR::from_raw( + monitor_info.szDevice.as_ptr(), + ))) + } + + #[inline] + pub fn native_identifier(&self) -> String { + self.name().unwrap() + } + + #[inline] + pub fn hmonitor(&self) -> HMONITOR { + HMONITOR(self.0 as _) + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + let monitor_info = get_monitor_info(self.hmonitor()).unwrap(); + PhysicalSize { + width: (monitor_info.monitorInfo.rcMonitor.right - monitor_info.monitorInfo.rcMonitor.left) + as u32, + height: (monitor_info.monitorInfo.rcMonitor.bottom - monitor_info.monitorInfo.rcMonitor.top) + as u32, + } + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + let monitor_info = get_monitor_info(self.hmonitor()).unwrap(); + PhysicalPosition { + x: monitor_info.monitorInfo.rcMonitor.left, + y: monitor_info.monitorInfo.rcMonitor.top, + } + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + dpi_to_scale_factor(self.dpi()) + } + + pub fn dpi(&self) -> u32 { + get_monitor_dpi(self.hmonitor()).unwrap_or(USER_DEFAULT_SCREEN_DPI) + } + + #[inline] + pub fn video_modes(&self) -> impl Iterator { + // EnumDisplaySettingsExW can return duplicate values (or some of the + // fields are probably changing, but we aren't looking at those fields + // anyway), so we're using a BTreeSet deduplicate + let mut modes = BTreeSet::new(); + let mut i = 0; + + loop { + unsafe { + let monitor_info = get_monitor_info(self.hmonitor()).unwrap(); + let device_name = PCWSTR::from_raw(monitor_info.szDevice.as_ptr()); + let mut mode: DEVMODEW = mem::zeroed(); + mode.dmSize = mem::size_of_val(&mode) as u16; + if !EnumDisplaySettingsExW( + device_name, + ENUM_DISPLAY_SETTINGS_MODE(i), + &mut mode, + ENUM_DISPLAY_SETTINGS_FLAGS(0), + ) + .as_bool() + { + break; + } + i += 1; + + let required_fields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; + assert!(mode.dmFields & required_fields == required_fields); + + modes.insert(RootVideoMode { + video_mode: VideoMode { + size: (mode.dmPelsWidth, mode.dmPelsHeight), + bit_depth: mode.dmBitsPerPel as u16, + refresh_rate: mode.dmDisplayFrequency as u16, + monitor: self.clone(), + native_video_mode: mode, + }, + }); + } + } + + modes.into_iter() + } +} diff --git a/vendor/tao/src/platform_impl/windows/raw_input.rs b/vendor/tao/src/platform_impl/windows/raw_input.rs new file mode 100644 index 0000000000..2ca23b5e44 --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/raw_input.rs @@ -0,0 +1,217 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::mem::{self, size_of}; + +use windows::Win32::{ + Devices::HumanInterfaceDevice::*, + Foundation::{HANDLE, HWND}, + UI::{ + Input::{self as win32i, *}, + WindowsAndMessaging::*, + }, +}; + +use crate::{event::ElementState, event_loop::DeviceEventFilter, platform_impl::platform::util}; + +#[allow(dead_code)] +pub fn get_raw_input_device_list() -> Option> { + let list_size = size_of::() as u32; + + let mut num_devices = 0; + let status = unsafe { GetRawInputDeviceList(None, &mut num_devices, list_size) }; + + if status == u32::MAX { + return None; + } + + let mut buffer = Vec::with_capacity(num_devices as _); + + let num_stored = + unsafe { GetRawInputDeviceList(Some(buffer.as_ptr() as _), &mut num_devices, list_size) }; + + if num_stored == u32::MAX { + return None; + } + + debug_assert_eq!(num_devices, num_stored); + + unsafe { buffer.set_len(num_devices as _) }; + + Some(buffer) +} + +#[non_exhaustive] +#[allow(dead_code)] +pub enum RawDeviceInfo { + Mouse(RID_DEVICE_INFO_MOUSE), + Keyboard(RID_DEVICE_INFO_KEYBOARD), + Hid(RID_DEVICE_INFO_HID), +} + +impl From for RawDeviceInfo { + fn from(info: RID_DEVICE_INFO) -> Self { + unsafe { + match info.dwType { + win32i::RIM_TYPEMOUSE => RawDeviceInfo::Mouse(info.Anonymous.mouse), + win32i::RIM_TYPEKEYBOARD => RawDeviceInfo::Keyboard(info.Anonymous.keyboard), + win32i::RIM_TYPEHID => RawDeviceInfo::Hid(info.Anonymous.hid), + _ => unreachable!(), + } + } + } +} + +#[allow(dead_code)] +pub fn get_raw_input_device_info(handle: HANDLE) -> Option { + let mut info: RID_DEVICE_INFO = unsafe { mem::zeroed() }; + let info_size = size_of::() as u32; + + info.cbSize = info_size; + + let mut minimum_size = 0; + let status = unsafe { + GetRawInputDeviceInfoW( + Some(handle), + RIDI_DEVICEINFO, + Some(&mut info as *mut _ as _), + &mut minimum_size, + ) + }; + + if status == u32::MAX || status == 0 { + return None; + } + + debug_assert_eq!(info_size, status); + + Some(info.into()) +} + +pub fn get_raw_input_device_name(handle: HANDLE) -> Option { + let mut minimum_size = 0; + let status = + unsafe { GetRawInputDeviceInfoW(Some(handle), RIDI_DEVICENAME, None, &mut minimum_size) }; + + if status != 0 { + return None; + } + + let mut name: Vec = Vec::with_capacity(minimum_size as _); + + let status = unsafe { + GetRawInputDeviceInfoW( + Some(handle), + RIDI_DEVICENAME, + Some(name.as_ptr() as _), + &mut minimum_size, + ) + }; + + if status == u32::MAX || status == 0 { + return None; + } + + debug_assert_eq!(minimum_size, status); + + unsafe { name.set_len(minimum_size as _) }; + + Some(util::wchar_to_string(&name)) +} + +pub fn register_raw_input_devices(devices: &[RAWINPUTDEVICE]) -> bool { + let device_size = size_of::() as u32; + unsafe { RegisterRawInputDevices(devices, device_size) }.is_ok() +} + +pub fn register_all_mice_and_keyboards_for_raw_input( + mut window_handle: HWND, + filter: DeviceEventFilter, +) -> bool { + // RIDEV_DEVNOTIFY: receive hotplug events + // RIDEV_INPUTSINK: receive events even if we're not in the foreground + // RIDEV_REMOVE: don't receive device events (requires NULL hwndTarget) + let flags = match filter { + DeviceEventFilter::Always => { + window_handle = HWND(std::ptr::null_mut()); + RIDEV_REMOVE + } + DeviceEventFilter::Unfocused => RIDEV_DEVNOTIFY, + DeviceEventFilter::Never => RIDEV_DEVNOTIFY | RIDEV_INPUTSINK, + }; + + let devices: [RAWINPUTDEVICE; 2] = [ + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: HID_USAGE_GENERIC_MOUSE, + dwFlags: flags, + hwndTarget: window_handle, + }, + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: HID_USAGE_GENERIC_KEYBOARD, + dwFlags: flags, + hwndTarget: window_handle, + }, + ]; + + register_raw_input_devices(&devices) +} + +pub fn get_raw_input_data(handle: HRAWINPUT) -> Option { + let mut data: RAWINPUT = unsafe { mem::zeroed() }; + let mut data_size = size_of::() as u32; + let header_size = size_of::() as u32; + + let status = unsafe { + GetRawInputData( + handle, + RID_INPUT, + Some(&mut data as *mut _ as _), + &mut data_size, + header_size, + ) + }; + + if status == u32::MAX || status == 0 { + return None; + } + + Some(data) +} + +fn button_flags_to_element_state( + button_flags: u16, + down_flag: u32, + up_flag: u32, +) -> Option { + // We assume the same button won't be simultaneously pressed and released. + if util::has_flag(button_flags, down_flag as u16) { + Some(ElementState::Pressed) + } else if util::has_flag(button_flags, up_flag as u16) { + Some(ElementState::Released) + } else { + None + } +} + +pub fn get_raw_mouse_button_state(button_flags: u16) -> [Option; 3] { + [ + button_flags_to_element_state( + button_flags, + RI_MOUSE_LEFT_BUTTON_DOWN, + RI_MOUSE_LEFT_BUTTON_UP, + ), + button_flags_to_element_state( + button_flags, + RI_MOUSE_MIDDLE_BUTTON_DOWN, + RI_MOUSE_MIDDLE_BUTTON_UP, + ), + button_flags_to_element_state( + button_flags, + RI_MOUSE_RIGHT_BUTTON_DOWN, + RI_MOUSE_RIGHT_BUTTON_UP, + ), + ] +} diff --git a/vendor/tao/src/platform_impl/windows/util.rs b/vendor/tao/src/platform_impl/windows/util.rs new file mode 100644 index 0000000000..5605603dd0 --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/util.rs @@ -0,0 +1,511 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + io, + iter::once, + mem, + ops::BitAnd, + os::windows::prelude::OsStrExt, + slice, + sync::atomic::{AtomicBool, Ordering}, +}; + +use crate::{ + dpi::{PhysicalPosition, PhysicalSize}, + window::CursorIcon, +}; + +use once_cell::sync::Lazy; +use windows::{ + core::{BOOL, HRESULT, PCSTR, PCWSTR}, + Win32::{ + Foundation::{COLORREF, FARPROC, HWND, LPARAM, LRESULT, POINT, RECT, WPARAM}, + Globalization::lstrlenW, + Graphics::Gdi::{ClientToScreen, InvalidateRgn, HMONITOR}, + System::LibraryLoader::*, + UI::{ + HiDpi::*, + Input::KeyboardAndMouse::*, + WindowsAndMessaging::{self as win32wm, *}, + }, + }, +}; + +pub fn has_flag(bitset: T, flag: T) -> bool +where + T: Copy + PartialEq + BitAnd, +{ + bitset & flag == flag +} + +pub fn wchar_to_string(wchar: &[u16]) -> String { + String::from_utf16_lossy(wchar) +} + +pub fn wchar_ptr_to_string(wchar: PCWSTR) -> String { + let len = unsafe { lstrlenW(wchar) } as usize; + let wchar_slice = unsafe { slice::from_raw_parts(wchar.0, len) }; + wchar_to_string(wchar_slice) +} + +pub fn encode_wide(string: impl AsRef) -> Vec { + string.as_ref().encode_wide().chain(once(0)).collect() +} + +fn win_to_err BOOL>(f: F) -> Result<(), io::Error> { + if f().as_bool() { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + +pub unsafe fn get_window_rect(hwnd: HWND) -> Option { + let mut rect = std::mem::zeroed(); + GetWindowRect(hwnd, &mut rect).ok().map(|_| rect) +} + +pub fn get_client_rect(hwnd: HWND) -> Result { + let mut rect = RECT::default(); + let mut top_left = POINT::default(); + + unsafe { + win_to_err(|| ClientToScreen(hwnd, &mut top_left))?; + GetClientRect(hwnd, &mut rect)?; + } + + rect.left += top_left.x; + rect.top += top_left.y; + rect.right += top_left.x; + rect.bottom += top_left.y; + + Ok(rect) +} + +pub fn adjust_size(hwnd: HWND, size: PhysicalSize, is_decorated: bool) -> PhysicalSize { + let (width, height): (u32, u32) = size.into(); + let rect = RECT { + left: 0, + right: width as i32, + top: 0, + bottom: height as i32, + }; + let rect = adjust_window_rect(hwnd, rect, is_decorated).unwrap_or(rect); + PhysicalSize::new((rect.right - rect.left) as _, (rect.bottom - rect.top) as _) +} + +pub(crate) fn set_inner_size_physical(window: HWND, x: i32, y: i32, is_decorated: bool) { + unsafe { + let rect = adjust_window_rect( + window, + RECT { + top: 0, + left: 0, + bottom: y, + right: x, + }, + is_decorated, + ) + .expect("adjust_window_rect failed"); + + let outer_x = (rect.right - rect.left).abs(); + let outer_y = (rect.top - rect.bottom).abs(); + let _ = SetWindowPos( + window, + None, + 0, + 0, + outer_x, + outer_y, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOREPOSITION | SWP_NOMOVE | SWP_NOACTIVATE, + ); + let _ = InvalidateRgn(window, None, false); + } +} + +pub fn adjust_window_rect(hwnd: HWND, rect: RECT, is_decorated: bool) -> Option { + unsafe { + let mut style = WINDOW_STYLE(GetWindowLongW(hwnd, GWL_STYLE) as u32); + // if the window isn't decorated, remove `WS_SIZEBOX` and `WS_CAPTION` so + // `AdjustWindowRect*` functions doesn't account for the hidden caption and borders and + // calculates a correct size for the client area. + if !is_decorated { + style &= !WS_CAPTION; + style &= !WS_SIZEBOX; + } + let style_ex = WINDOW_EX_STYLE(GetWindowLongW(hwnd, GWL_EXSTYLE) as u32); + adjust_window_rect_with_styles(hwnd, style, style_ex, rect) + } +} + +pub fn adjust_window_rect_with_styles( + hwnd: HWND, + style: WINDOW_STYLE, + style_ex: WINDOW_EX_STYLE, + mut rect: RECT, +) -> Option { + let b_menu = !unsafe { GetMenu(hwnd) }.is_invalid(); + + if let (Some(get_dpi_for_window), Some(adjust_window_rect_ex_for_dpi)) = + (*GET_DPI_FOR_WINDOW, *ADJUST_WINDOW_RECT_EX_FOR_DPI) + { + let dpi = unsafe { get_dpi_for_window(hwnd) }; + if unsafe { adjust_window_rect_ex_for_dpi(&mut rect, style, b_menu.into(), style_ex, dpi) } + .as_bool() + { + Some(rect) + } else { + None + } + } else { + unsafe { AdjustWindowRectEx(&mut rect, style, b_menu, style_ex) } + .ok() + .map(|_| rect) + } +} + +pub fn set_cursor_hidden(hidden: bool) { + static HIDDEN: AtomicBool = AtomicBool::new(false); + let changed = HIDDEN.swap(hidden, Ordering::SeqCst) ^ hidden; + if changed { + unsafe { ShowCursor(!hidden) }; + } +} + +pub fn get_cursor_clip() -> windows::core::Result { + unsafe { + let mut rect = RECT::default(); + GetClipCursor(&mut rect).map(|_| rect) + } +} + +/// Sets the cursor's clip rect. +/// +/// Note that calling this will automatically dispatch a `WM_MOUSEMOVE` event. +pub fn set_cursor_clip(rect: Option) -> windows::core::Result<()> { + unsafe { + let rect_ptr = rect.as_ref().map(|r| r as *const RECT); + ClipCursor(rect_ptr) + } +} + +pub fn get_desktop_rect() -> RECT { + unsafe { + let left = GetSystemMetrics(SM_XVIRTUALSCREEN); + let top = GetSystemMetrics(SM_YVIRTUALSCREEN); + RECT { + left, + top, + right: left + GetSystemMetrics(SM_CXVIRTUALSCREEN), + bottom: top + GetSystemMetrics(SM_CYVIRTUALSCREEN), + } + } +} + +pub fn is_focused(window: HWND) -> bool { + window == unsafe { GetActiveWindow() } +} + +pub fn is_visible(window: HWND) -> bool { + unsafe { IsWindowVisible(window).as_bool() } +} + +pub fn is_maximized(window: HWND) -> windows::core::Result { + let mut placement = WINDOWPLACEMENT { + length: mem::size_of::() as u32, + ..WINDOWPLACEMENT::default() + }; + unsafe { GetWindowPlacement(window, &mut placement)? }; + Ok(placement.showCmd == SW_MAXIMIZE.0 as u32) +} + +pub fn cursor_position() -> windows::core::Result> { + let mut pt = POINT { x: 0, y: 0 }; + unsafe { GetCursorPos(&mut pt)? }; + Ok((pt.x, pt.y).into()) +} + +impl CursorIcon { + pub(crate) fn to_windows_cursor(self) -> PCWSTR { + match self { + CursorIcon::Arrow | CursorIcon::Default => IDC_ARROW, + CursorIcon::Hand => IDC_HAND, + CursorIcon::Crosshair => IDC_CROSS, + CursorIcon::Text | CursorIcon::VerticalText => IDC_IBEAM, + CursorIcon::NotAllowed | CursorIcon::NoDrop => IDC_NO, + CursorIcon::Grab | CursorIcon::Grabbing | CursorIcon::Move | CursorIcon::AllScroll => { + IDC_SIZEALL + } + CursorIcon::EResize | CursorIcon::WResize | CursorIcon::EwResize | CursorIcon::ColResize => { + IDC_SIZEWE + } + CursorIcon::NResize | CursorIcon::SResize | CursorIcon::NsResize | CursorIcon::RowResize => { + IDC_SIZENS + } + CursorIcon::NeResize | CursorIcon::SwResize | CursorIcon::NeswResize => IDC_SIZENESW, + CursorIcon::NwResize | CursorIcon::SeResize | CursorIcon::NwseResize => IDC_SIZENWSE, + CursorIcon::Wait => IDC_WAIT, + CursorIcon::Progress => IDC_APPSTARTING, + CursorIcon::Help => IDC_HELP, + _ => IDC_ARROW, // use arrow for the missing cases. + } + } +} + +// Helper function to dynamically load function pointer. +// `library` and `function` must be zero-terminated. +pub(super) fn get_function_impl(library: &str, function: &str) -> FARPROC { + let library = encode_wide(library); + assert_eq!(function.chars().last(), Some('\0')); + + // Library names we will use are ASCII so we can use the A version to avoid string conversion. + let module = unsafe { LoadLibraryW(PCWSTR::from_raw(library.as_ptr())) }.unwrap_or_default(); + if module.is_invalid() { + return None; + } + + unsafe { GetProcAddress(module, PCSTR::from_raw(function.as_ptr())) } +} + +macro_rules! get_function { + ($lib:expr, $func:ident) => { + crate::platform_impl::platform::util::get_function_impl($lib, concat!(stringify!($func), '\0')) + .map(|f| unsafe { std::mem::transmute::<_, $func>(f) }) + }; +} + +pub type SetProcessDPIAware = unsafe extern "system" fn() -> BOOL; +pub type SetProcessDpiAwareness = + unsafe extern "system" fn(value: PROCESS_DPI_AWARENESS) -> HRESULT; +pub type SetProcessDpiAwarenessContext = + unsafe extern "system" fn(value: DPI_AWARENESS_CONTEXT) -> BOOL; +pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32; +pub type GetDpiForMonitor = unsafe extern "system" fn( + hmonitor: HMONITOR, + dpi_type: MONITOR_DPI_TYPE, + dpi_x: *mut u32, + dpi_y: *mut u32, +) -> HRESULT; +type GetSystemMetricsForDpi = + unsafe extern "system" fn(nindex: SYSTEM_METRICS_INDEX, dpi: u32) -> i32; +pub type EnableNonClientDpiScaling = unsafe extern "system" fn(hwnd: HWND) -> BOOL; +#[allow(non_snake_case)] +pub type AdjustWindowRectExForDpi = unsafe extern "system" fn( + rect: *mut RECT, + dwStyle: WINDOW_STYLE, + bMenu: BOOL, + dwExStyle: WINDOW_EX_STYLE, + dpi: u32, +) -> BOOL; + +lazy_static! { + pub static ref GET_DPI_FOR_WINDOW: Option = + get_function!("user32.dll", GetDpiForWindow); + pub static ref ADJUST_WINDOW_RECT_EX_FOR_DPI: Option = + get_function!("user32.dll", AdjustWindowRectExForDpi); + pub static ref GET_DPI_FOR_MONITOR: Option = + get_function!("shcore.dll", GetDpiForMonitor); + pub static ref GET_SYSTEM_METRICS_FOR_DPI: Option = + get_function!("user32.dll", GetSystemMetricsForDpi); + pub static ref ENABLE_NON_CLIENT_DPI_SCALING: Option = + get_function!("user32.dll", EnableNonClientDpiScaling); + pub static ref SET_PROCESS_DPI_AWARENESS_CONTEXT: Option = + get_function!("user32.dll", SetProcessDpiAwarenessContext); + pub static ref SET_PROCESS_DPI_AWARENESS: Option = + get_function!("shcore.dll", SetProcessDpiAwareness); + pub static ref SET_PROCESS_DPI_AWARE: Option = + get_function!("user32.dll", SetProcessDPIAware); +} + +#[allow(non_snake_case)] +#[cfg(target_pointer_width = "32")] +pub fn SetWindowLongPtrW(window: HWND, index: WINDOW_LONG_PTR_INDEX, value: isize) -> isize { + unsafe { win32wm::SetWindowLongW(window, index, value as _) as _ } +} + +#[allow(non_snake_case)] +#[cfg(target_pointer_width = "64")] +pub fn SetWindowLongPtrW(window: HWND, index: WINDOW_LONG_PTR_INDEX, value: isize) -> isize { + unsafe { win32wm::SetWindowLongPtrW(window, index, value) } +} + +#[allow(non_snake_case)] +#[cfg(target_pointer_width = "32")] +pub fn GetWindowLongPtrW(window: HWND, index: WINDOW_LONG_PTR_INDEX) -> isize { + unsafe { win32wm::GetWindowLongW(window, index) as _ } +} + +#[allow(non_snake_case)] +#[cfg(target_pointer_width = "64")] +pub fn GetWindowLongPtrW(window: HWND, index: WINDOW_LONG_PTR_INDEX) -> isize { + unsafe { win32wm::GetWindowLongPtrW(window, index) } +} + +/// Implementation of the `LOWORD` macro. +#[allow(non_snake_case)] +#[inline] +pub fn LOWORD(dword: u32) -> u16 { + (dword & 0xFFFF) as u16 +} + +/// Implementation of the `HIWORD` macro. +#[allow(non_snake_case)] +#[inline] +pub fn HIWORD(dword: u32) -> u16 { + ((dword & 0xFFFF_0000) >> 16) as u16 +} + +/// Implementation of the `GET_X_LPARAM` macro. +#[allow(non_snake_case)] +#[inline] +pub fn GET_X_LPARAM(lparam: LPARAM) -> i16 { + ((lparam.0 as usize) & 0xFFFF) as u16 as i16 +} + +/// Implementation of the `GET_Y_LPARAM` macro. +#[allow(non_snake_case)] +#[inline] +pub fn GET_Y_LPARAM(lparam: LPARAM) -> i16 { + (((lparam.0 as usize) & 0xFFFF_0000) >> 16) as u16 as i16 +} + +/// Implementation of the `MAKELPARAM` macro. +/// Inverse of [GET_X_LPARAM] and [GET_Y_LPARAM] to put the (`x`, `y`) signed +/// coordinates/values back into an [LPARAM]. +#[allow(non_snake_case)] +#[inline] +pub fn MAKELPARAM(x: i16, y: i16) -> LPARAM { + LPARAM(((x as u16 as u32) | ((y as u16 as u32) << 16)) as usize as _) +} + +/// Implementation of the `GET_WHEEL_DELTA_WPARAM` macro. +#[allow(non_snake_case)] +#[inline] +pub fn GET_WHEEL_DELTA_WPARAM(wparam: WPARAM) -> i16 { + ((wparam.0 & 0xFFFF_0000) >> 16) as u16 as i16 +} + +/// Implementation of the `GET_XBUTTON_WPARAM` macro. +#[allow(non_snake_case)] +#[inline] +pub fn GET_XBUTTON_WPARAM(wparam: WPARAM) -> u16 { + ((wparam.0 & 0xFFFF_0000) >> 16) as u16 +} + +/// Implementation of the `PRIMARYLANGID` macro. +#[allow(non_snake_case)] +#[inline] +pub fn PRIMARYLANGID(hkl: HKL) -> u32 { + ((hkl.0 as usize) & 0x3FF) as u32 +} + +/// Implementation of the `RGB` macro. +#[allow(non_snake_case)] +#[inline] +pub fn RGB>(r: T, g: T, b: T) -> COLORREF { + COLORREF(r.into() | (g.into() << 8) | (b.into() << 16)) +} + +pub unsafe extern "system" fn call_default_window_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + DefWindowProcW(hwnd, msg, wparam, lparam) +} + +pub fn get_instance_handle() -> windows::Win32::Foundation::HMODULE { + // Gets the instance handle by taking the address of the + // pseudo-variable created by the microsoft linker: + // https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483 + + // This is preferred over GetModuleHandle(NULL) because it also works in DLLs: + // https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance + + extern "C" { + static __ImageBase: windows::Win32::System::SystemServices::IMAGE_DOS_HEADER; + } + + windows::Win32::Foundation::HMODULE(unsafe { &__ImageBase as *const _ as _ }) +} + +pub static WIN_VERSION: Lazy = + Lazy::new(windows_version::OsVersion::current); + +pub fn get_frame_thickness(dpi: u32) -> i32 { + let resize_frame_thickness = unsafe { get_system_metrics_for_dpi(SM_CXSIZEFRAME, dpi) }; + let padding_thickness = unsafe { get_system_metrics_for_dpi(SM_CXPADDEDBORDER, dpi) }; + resize_frame_thickness + padding_thickness +} + +pub fn calculate_insets_for_dpi(dpi: u32) -> RECT { + // - On Windows 10 + // The top inset must be zero, since if there is any nonclient area, Windows will draw + // a full native titlebar outside the client area. (This doesn't occur in the maximized + // case.) + // + // - On Windows 11 + // The top inset is calculated using an empirical formula that I derived through various + // tests. Without this, the top 1-2 rows of pixels in our window would be obscured. + + let frame_thickness = get_frame_thickness(dpi); + + let top_inset = match WIN_VERSION.build { + v if v >= 22000 => (dpi as f32 / USER_DEFAULT_SCREEN_DPI as f32).round() as i32, + _ => 0, + }; + + RECT { + left: frame_thickness, + top: top_inset, + right: frame_thickness, + bottom: frame_thickness, + } +} + +/// Calcuclate window insets, used in WM_NCCALCSIZE +/// +/// Derived of GPUI implementation +/// see +pub fn calculate_window_insets(window: HWND) -> RECT { + let dpi = unsafe { super::dpi::hwnd_dpi(window) }; + calculate_insets_for_dpi(dpi) +} + +pub fn window_rect(hwnd: HWND) -> RECT { + unsafe { + let mut rect = RECT::default(); + if GetWindowRect(hwnd, &mut rect).is_err() { + panic!( + "Unexpected GetWindowRect failure: please report this error to \ + tauri-apps/tao" + ) + } + rect + } +} + +pub fn client_rect(hwnd: HWND) -> RECT { + unsafe { + let mut rect = RECT::default(); + if GetClientRect(hwnd, &mut rect).is_err() { + panic!( + "Unexpected GetClientRect failure: please report this error to \ + tauri-apps/tao" + ) + } + rect + } +} + +pub unsafe fn get_system_metrics_for_dpi(nindex: SYSTEM_METRICS_INDEX, dpi: u32) -> i32 { + #[allow(non_snake_case)] + if let Some(GetSystemMetricsForDpi) = *GET_SYSTEM_METRICS_FOR_DPI { + GetSystemMetricsForDpi(nindex, dpi) + } else { + GetSystemMetrics(nindex) + } +} diff --git a/vendor/tao/src/platform_impl/windows/window.rs b/vendor/tao/src/platform_impl/windows/window.rs new file mode 100644 index 0000000000..170de36681 --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/window.rs @@ -0,0 +1,1554 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(target_os = "windows")] + +use mem::MaybeUninit; +use parking_lot::Mutex; +use std::{ + cell::{Cell, RefCell}, + ffi::OsStr, + io, mem, + os::windows::ffi::OsStrExt, + sync::Arc, +}; + +use crossbeam_channel as channel; +use windows::{ + core::PCWSTR, + Win32::{ + Foundation::{ + self as win32f, HINSTANCE, HMODULE, HWND, LPARAM, LRESULT, POINT, POINTS, RECT, WPARAM, + }, + Graphics::{ + Dwm::{DwmEnableBlurBehindWindow, DWM_BB_BLURREGION, DWM_BB_ENABLE, DWM_BLURBEHIND}, + Gdi::*, + }, + System::{Com::*, LibraryLoader::*, Ole::*}, + UI::{ + Input::{Ime::*, KeyboardAndMouse::*, Touch::*}, + Shell::{ITaskbarList4 as ITaskbarList, TaskbarList, *}, + WindowsAndMessaging::{self as win32wm, *}, + }, + }, +}; + +use crate::{ + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, + error::{ExternalError, NotSupportedError, OsError as RootOsError}, + icon::Icon, + monitor::MonitorHandle as RootMonitorHandle, + platform_impl::platform::{ + dark_mode::try_window_theme, + dpi::{dpi_to_scale_factor, hwnd_dpi}, + drop_handler::FileDropHandler, + event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID}, + icon::{self, IconType}, + monitor::{self}, + util, + window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState}, + OsError, Parent, PlatformSpecificWindowBuilderAttributes, WindowId, + }, + window::{ + CursorIcon, Fullscreen, ProgressBarState, ProgressState, ResizeDirection, Theme, + UserAttentionType, WindowAttributes, WindowSizeConstraints, RGBA, + }, +}; + +use super::{ + event_loop::CHANGE_THEME_MSG_ID, + keyboard::{KeyEventBuilder, KEY_EVENT_BUILDERS}, + util::calculate_insets_for_dpi, +}; + +/// A simple non-owning wrapper around a window. +#[derive(Clone, Copy)] +pub struct WindowWrapper(pub HWND); + +// Send and Sync are not implemented for HWND and HDC, we have to wrap it and implement them manually. +// For more info see: +// https://github.com/retep998/winapi-rs/issues/360 +// https://github.com/retep998/winapi-rs/issues/396 +unsafe impl Sync for WindowWrapper {} +unsafe impl Send for WindowWrapper {} + +/// The Win32 implementation of the main `Window` object. +pub struct Window { + /// Main handle for the window. + window: WindowWrapper, + + /// The current window state. + window_state: Arc>, + + // The events loop proxy. + thread_executor: event_loop::EventLoopThreadExecutor, +} + +impl Window { + pub fn new( + event_loop: &EventLoopWindowTarget, + w_attr: WindowAttributes, + pl_attr: PlatformSpecificWindowBuilderAttributes, + ) -> Result { + // We dispatch an `init` function because of code style. + // First person to remove the need for cloning here gets a cookie! + // + // done. you owe me -- ossi + unsafe { + let drag_and_drop = pl_attr.drag_and_drop; + init(w_attr, pl_attr, event_loop).map(|win| { + let file_drop_handler = if drag_and_drop { + // It is ok if the initialize result is `S_FALSE` because it might happen that + // multiple windows are created on the same thread. + if let Err(error) = OleInitialize(None) { + match error.code() { + win32f::OLE_E_WRONGCOMPOBJ => { + panic!("OleInitialize failed! Result was: `OLE_E_WRONGCOMPOBJ`") + } + win32f::RPC_E_CHANGED_MODE => panic!( + "OleInitialize failed! Result was: `RPC_E_CHANGED_MODE`. \ + Make sure other crates are not using multithreaded COM library \ + on the same thread or disable drag and drop support." + ), + _ => (), + }; + } + + let file_drop_runner = event_loop.runner_shared.clone(); + let file_drop_handler: IDropTarget = FileDropHandler::new( + win.window.0, + Box::new(move |event| { + if let Ok(e) = event.map_nonuser_event() { + file_drop_runner.send_event(e) + } + }), + ) + .into(); + + assert!(RegisterDragDrop(win.window.0, &file_drop_handler).is_ok()); + Some(file_drop_handler) + } else { + None + }; + + let subclass_input = event_loop::SubclassInput { + window_state: win.window_state.clone(), + event_loop_runner: event_loop.runner_shared.clone(), + _file_drop_handler: file_drop_handler, + subclass_removed: Cell::new(false), + recurse_depth: Cell::new(0), + event_loop_preferred_theme: event_loop.preferred_theme.clone(), + }; + + event_loop::subclass_window(win.window.0, subclass_input); + win + }) + } + } + + pub fn set_title(&self, text: &str) { + let text = util::encode_wide(text); + unsafe { + let _ = SetWindowTextW(self.window.0, PCWSTR::from_raw(text.as_ptr())); + } + } + + pub fn title(&self) -> String { + let len = unsafe { GetWindowTextLengthW(self.window.0) }; + let mut buf = vec![0; (len + 1) as usize]; + unsafe { GetWindowTextW(self.window.0, &mut buf) }; + String::from_utf16_lossy(&buf[..len as _]) + } + #[inline] + pub fn set_visible(&self, visible: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::VISIBLE, visible) + }); + }); + } + + #[inline] + pub fn set_focus(&self) { + let window = self.window; + let window_flags = self.window_state.lock().window_flags(); + + let is_visible = window_flags.contains(WindowFlags::VISIBLE); + let is_minimized = window_flags.contains(WindowFlags::MINIMIZED); + let is_foreground = window.0 == unsafe { GetForegroundWindow() }; + + if is_visible && !is_minimized && !is_foreground { + unsafe { force_window_active(window.0) }; + } + } + + #[inline] + pub fn set_focusable(&self, focusable: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::FOCUSABLE, focusable) + }); + }); + } + + #[inline] + pub fn is_focused(&self) -> bool { + let window_state = self.window_state.lock(); + window_state.has_active_focus() + } + + #[inline] + pub fn request_redraw(&self) { + unsafe { + let _ = RedrawWindow(Some(self.window.0), None, None, RDW_INTERNALPAINT); + } + } + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + unsafe { util::get_window_rect(self.window.0) } + .map(|rect| Ok(PhysicalPosition::new(rect.left, rect.top))) + .expect("Unexpected GetWindowRect failure") + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + let mut position = POINT::default(); + if !unsafe { ClientToScreen(self.window.0, &mut position) }.as_bool() { + panic!("Unexpected ClientToScreen failure") + } + Ok(PhysicalPosition::new(position.x, position.y)) + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + let (x, y): (i32, i32) = position.to_physical::(self.scale_factor()).into(); + + let window_state = Arc::clone(&self.window_state); + let window = self.window.0 .0 as isize; + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + }); + + unsafe { + let _ = SetWindowPos( + self.window.0, + None, + x, + y, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE, + ); + let _ = InvalidateRgn(self.window.0, None, false); + } + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let client_rect = util::client_rect(self.hwnd()); + PhysicalSize::new( + (client_rect.right - client_rect.left) as u32, + (client_rect.bottom - client_rect.top) as u32, + ) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let window_rect = util::window_rect(self.hwnd()); + PhysicalSize::new( + (window_rect.right - window_rect.left) as u32, + (window_rect.bottom - window_rect.top) as u32, + ) + } + + #[inline] + pub fn set_inner_size(&self, size: Size) { + let scale_factor = self.scale_factor(); + + let (mut desired_width, mut desired_height) = size.to_physical::(scale_factor).into(); + + let window_flags = self.window_state.lock().window_flags; + + // undecorated windows with shadows have hidden offsets + // we need to calculate them and account for them in new size + // + // implementation derived from GPUI + // see + if window_flags.undecorated_with_shadows() { + let hwnd = self.hwnd(); + + let client_rect = util::client_rect(hwnd); + let window_rect = util::window_rect(hwnd); + + let width_offset = + (window_rect.right - window_rect.left) - (client_rect.right - client_rect.left); + let height_offset = + (window_rect.bottom - window_rect.top) - (client_rect.bottom - client_rect.top); + + desired_width += width_offset; + desired_height += height_offset; + } + + let window_state = Arc::clone(&self.window_state); + let window = self.window.0 .0 as isize; + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + }); + + util::set_inner_size_physical( + self.window.0, + desired_width, + desired_height, + window_flags.contains(WindowFlags::MARKER_DECORATIONS), + ); + } + + #[inline] + pub fn set_min_inner_size(&self, size: Option) { + let (width, height) = size.map(crate::extract_width_height).unzip(); + + { + let mut window_state = self.window_state.lock(); + window_state.size_constraints.min_width = width; + window_state.size_constraints.min_height = height; + } + + // Make windows re-check the window size bounds. + let size = self.inner_size(); + self.set_inner_size(size.into()); + } + + #[inline] + pub fn set_max_inner_size(&self, size: Option) { + let (width, height) = size.map(crate::extract_width_height).unzip(); + + { + let mut window_state = self.window_state.lock(); + window_state.size_constraints.max_width = width; + window_state.size_constraints.max_height = height; + } + + // Make windows re-check the window size bounds. + let size = self.inner_size(); + self.set_inner_size(size.into()); + } + + #[inline] + pub fn set_inner_size_constraints(&self, constraints: WindowSizeConstraints) { + self.window_state.lock().size_constraints = constraints; + // Make windows re-check the window size bounds. + let size = self.inner_size(); + self.set_inner_size(size.into()); + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::RESIZABLE, resizable) + }); + }); + } + + #[inline] + pub fn set_minimizable(&self, minimizable: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::MINIMIZABLE, minimizable) + }); + }); + } + + #[inline] + pub fn set_maximizable(&self, maximizable: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::MAXIMIZABLE, maximizable) + }); + }); + } + + #[inline] + pub fn set_closable(&self, closable: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::CLOSABLE, closable) + }); + }); + } + + /// Returns the `hwnd` of this window. + #[inline] + pub fn hwnd(&self) -> HWND { + self.window.0 + } + + #[inline] + pub fn hinstance(&self) -> HMODULE { + util::get_instance_handle() + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::Win32Handle::empty(); + window_handle.hwnd = self.window.0 .0 as *mut _; + let hinstance = util::GetWindowLongPtrW(self.hwnd(), GWLP_HINSTANCE); + window_handle.hinstance = hinstance as *mut _; + rwh_04::RawWindowHandle::Win32(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::Win32WindowHandle::empty(); + window_handle.hwnd = self.window.0 .0 as *mut _; + let hinstance = util::GetWindowLongPtrW(self.hwnd(), GWLP_HINSTANCE); + window_handle.hinstance = hinstance as *mut _; + rwh_05::RawWindowHandle::Win32(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Windows(rwh_05::WindowsDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let mut window_handle = rwh_06::Win32WindowHandle::new(unsafe { + // SAFETY: Handle will never be zero. + let window = self.window.0 .0; + std::num::NonZeroIsize::new_unchecked(window as _) + }); + let hinstance = util::GetWindowLongPtrW(self.hwnd(), GWLP_HINSTANCE); + window_handle.hinstance = std::num::NonZeroIsize::new(hinstance); + Ok(rwh_06::RawWindowHandle::Win32(window_handle)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06(&self) -> Result { + Ok(rwh_06::RawDisplayHandle::Windows( + rwh_06::WindowsDisplayHandle::new(), + )) + } + + #[inline] + pub fn set_cursor_icon(&self, cursor: CursorIcon) { + self.window_state.lock().mouse.cursor = cursor; + self.thread_executor.execute_in_thread(move || unsafe { + let cursor = LoadCursorW(None, cursor.to_windows_cursor()).ok(); + SetCursor(cursor); + }); + } + + #[inline] + pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + let (tx, rx) = channel::unbounded(); + + self.thread_executor.execute_in_thread(move || { + let result = window_state + .lock() + .mouse + .set_cursor_flags(HWND(window as _), |f| f.set(CursorFlags::GRABBED, grab)) + .map_err(|e| ExternalError::Os(os_error!(OsError::IoError(e)))); + let _ = tx.send(result); + }); + rx.recv().unwrap() + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + let (tx, rx) = channel::unbounded(); + + self.thread_executor.execute_in_thread(move || { + let result = window_state + .lock() + .mouse + .set_cursor_flags(HWND(window as _), |f| f.set(CursorFlags::HIDDEN, !visible)) + .map_err(|e| e.to_string()); + let _ = tx.send(result); + }); + rx.recv().unwrap().ok(); + } + + #[inline] + pub fn cursor_position(&self) -> Result, ExternalError> { + util::cursor_position().map_err(Into::into) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.window_state.lock().scale_factor + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + let scale_factor = self.scale_factor(); + let (x, y) = position.to_physical::(scale_factor).into(); + + let mut point = POINT { x, y }; + unsafe { + if !ClientToScreen(self.window.0, &mut point).as_bool() { + return Err(ExternalError::Os(os_error!(OsError::IoError( + io::Error::last_os_error() + )))); + } + SetCursorPos(point.x, point.y) + .map_err(|e| ExternalError::Os(os_error!(OsError::IoError(e.into())))) + } + } + + fn handle_os_dragging(&self, wparam: WPARAM) -> Result<(), ExternalError> { + let points = { + let mut pos = unsafe { mem::zeroed() }; + unsafe { GetCursorPos(&mut pos)? }; + pos + }; + let points = POINTS { + x: points.x as i16, + y: points.y as i16, + }; + unsafe { ReleaseCapture()? }; + + self.window_state.lock().dragging = true; + + unsafe { + PostMessageW( + Some(self.hwnd()), + WM_NCLBUTTONDOWN, + wparam, + LPARAM(&points as *const _ as _), + )? + }; + + Ok(()) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + self.handle_os_dragging(WPARAM(HTCAPTION as _)) + } + + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + self.handle_os_dragging(WPARAM(direction.to_win32() as _)) + } + + #[inline] + pub fn set_ignore_cursor_events(&self, ignore: bool) -> Result<(), ExternalError> { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::IGNORE_CURSOR_EVENT, ignore) + }); + }); + + Ok(()) + } + + #[inline] + pub fn id(&self) -> WindowId { + WindowId(self.window.0 .0 as _) + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + + let is_minimized = self.is_minimized(); + + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags_in_place(&mut window_state.lock(), |f| { + f.set(WindowFlags::MINIMIZED, is_minimized) + }); + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::MINIMIZED, minimized) + }); + }); + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::MAXIMIZED, maximized) + }); + }); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + let window_state = self.window_state.lock(); + window_state.window_flags.contains(WindowFlags::MAXIMIZED) + } + + #[inline] + pub fn is_always_on_top(&self) -> bool { + let window_state = self.window_state.lock(); + window_state + .window_flags + .contains(WindowFlags::ALWAYS_ON_TOP) + } + + #[inline] + pub fn is_minimized(&self) -> bool { + unsafe { IsIconic(self.hwnd()) }.as_bool() + } + + #[inline] + pub fn is_resizable(&self) -> bool { + let window_state = self.window_state.lock(); + window_state.window_flags.contains(WindowFlags::RESIZABLE) + } + + #[inline] + pub fn is_minimizable(&self) -> bool { + let window_state = self.window_state.lock(); + window_state.window_flags.contains(WindowFlags::MINIMIZABLE) + } + + #[inline] + pub fn is_maximizable(&self) -> bool { + let window_state = self.window_state.lock(); + window_state.window_flags.contains(WindowFlags::MAXIMIZABLE) + } + + #[inline] + pub fn is_closable(&self) -> bool { + let window_state = self.window_state.lock(); + window_state.window_flags.contains(WindowFlags::CLOSABLE) + } + + #[inline] + pub fn is_decorated(&self) -> bool { + let window_state = self.window_state.lock(); + window_state + .window_flags + .contains(WindowFlags::MARKER_DECORATIONS) + } + + #[inline] + pub fn is_visible(&self) -> bool { + util::is_visible(self.window.0) + } + + #[inline] + pub fn fullscreen(&self) -> Option { + let window_state = self.window_state.lock(); + window_state.fullscreen.clone() + } + + #[inline] + pub fn set_fullscreen(&self, fullscreen: Option) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + let mut window_state_lock = window_state.lock(); + let old_fullscreen = window_state_lock.fullscreen.clone(); + + match (&old_fullscreen, &fullscreen) { + // Return if we already in the same fullscreen mode + _ if old_fullscreen == fullscreen => return, + // Return if saved Borderless(monitor) is the same as current monitor when requested fullscreen is Borderless(None) + (Some(Fullscreen::Borderless(Some(monitor))), Some(Fullscreen::Borderless(None))) + if monitor.inner == monitor::current_monitor(window.0) => + { + return + } + _ => {} + } + + window_state_lock.fullscreen = fullscreen.clone(); + drop(window_state_lock); + + let window_isize = window.0 .0 as isize; + self.thread_executor.execute_in_thread(move || { + let hwnd = HWND(window_isize as _); + // Change video mode if we're transitioning to or from exclusive + // fullscreen + match (&old_fullscreen, &fullscreen) { + (&None, &Some(Fullscreen::Exclusive(ref video_mode))) + | (&Some(Fullscreen::Borderless(_)), &Some(Fullscreen::Exclusive(ref video_mode))) + | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Exclusive(ref video_mode))) => { + let monitor = video_mode.monitor(); + + let mut display_name = OsStr::new(&monitor.inner.native_identifier()) + .encode_wide() + .collect::>(); + // `encode_wide` does not add a null-terminator but + // `ChangeDisplaySettingsExW` requires a null-terminated + // string, so add it + display_name.push(0); + + let native_video_mode = video_mode.video_mode.native_video_mode; + + let res = unsafe { + ChangeDisplaySettingsExW( + PCWSTR::from_raw(display_name.as_ptr()), + Some(&native_video_mode), + None, + CDS_FULLSCREEN, + None, + ) + }; + + debug_assert!(res != DISP_CHANGE_BADFLAGS); + debug_assert!(res != DISP_CHANGE_BADMODE); + debug_assert!(res != DISP_CHANGE_BADPARAM); + debug_assert!(res != DISP_CHANGE_FAILED); + assert_eq!(res, DISP_CHANGE_SUCCESSFUL); + } + (&Some(Fullscreen::Exclusive(_)), &None) + | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Borderless(_))) => { + let res = + unsafe { ChangeDisplaySettingsExW(PCWSTR::null(), None, None, CDS_FULLSCREEN, None) }; + + debug_assert!(res != DISP_CHANGE_BADFLAGS); + debug_assert!(res != DISP_CHANGE_BADMODE); + debug_assert!(res != DISP_CHANGE_BADPARAM); + debug_assert!(res != DISP_CHANGE_FAILED); + assert_eq!(res, DISP_CHANGE_SUCCESSFUL); + } + _ => (), + } + + unsafe { + // There are some scenarios where calling `ChangeDisplaySettingsExW` takes long + // enough to execute that the DWM thinks our program has frozen and takes over + // our program's window. When that happens, the `SetWindowPos` call below gets + // eaten and the window doesn't get set to the proper fullscreen position. + // + // Calling `PeekMessageW` here notifies Windows that our process is still running + // fine, taking control back from the DWM and ensuring that the `SetWindowPos` call + // below goes through. + let mut msg = MSG::default(); + let _ = PeekMessageW(&mut msg, None, 0, 0, PM_NOREMOVE); + } + + // Update window style + WindowState::set_window_flags(window_state.lock(), HWND(window_isize as _), |f| { + f.set( + WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN, + matches!(fullscreen, Some(Fullscreen::Exclusive(_))), + ); + f.set( + WindowFlags::MARKER_BORDERLESS_FULLSCREEN, + matches!(fullscreen, Some(Fullscreen::Borderless(_))), + ); + }); + + // Update window bounds + match &fullscreen { + Some(fullscreen) => { + // Save window bounds before entering fullscreen + let placement = unsafe { + let mut placement = WINDOWPLACEMENT::default(); + let _ = GetWindowPlacement(hwnd, &mut placement); + placement + }; + + window_state.lock().saved_window = Some(SavedWindow { placement }); + + let monitor = match &fullscreen { + Fullscreen::Exclusive(video_mode) => video_mode.monitor(), + Fullscreen::Borderless(Some(monitor)) => monitor.clone(), + Fullscreen::Borderless(None) => RootMonitorHandle { + inner: monitor::current_monitor(hwnd), + }, + }; + + let position: (i32, i32) = monitor.position().into(); + let size: (u32, u32) = monitor.size().into(); + + unsafe { + let _ = SetWindowPos( + hwnd, + None, + position.0, + position.1, + size.0 as i32, + size.1 as i32, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER, + ); + let _ = InvalidateRgn(hwnd, None, false); + } + } + None => { + let mut window_state_lock = window_state.lock(); + if let Some(SavedWindow { placement }) = window_state_lock.saved_window.take() { + drop(window_state_lock); + unsafe { + let _ = SetWindowPlacement(hwnd, &placement); + let _ = InvalidateRgn(hwnd, None, false); + } + } + } + } + + unsafe { + taskbar_mark_fullscreen(hwnd, fullscreen.is_some()); + } + }); + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::MARKER_DECORATIONS, decorations) + }); + }); + } + + #[inline] + pub fn set_always_on_bottom(&self, always_on_bottom: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::ALWAYS_ON_BOTTOM, always_on_bottom) + }); + }); + } + + #[inline] + pub fn set_always_on_top(&self, always_on_top: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::ALWAYS_ON_TOP, always_on_top) + }); + }); + } + + pub fn set_rtl(&self, rtl: bool) { + let window = self.window.0 .0 as isize; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), HWND(window as _), |f| { + f.set(WindowFlags::RIGHT_TO_LEFT_LAYOUT, rtl) + }); + }); + } + + #[inline] + pub fn current_monitor(&self) -> Option { + Some(RootMonitorHandle { + inner: monitor::current_monitor(self.window.0), + }) + } + + #[inline] + pub fn set_window_icon(&self, window_icon: Option) { + if let Some(ref window_icon) = window_icon { + window_icon + .inner + .set_for_window(self.window.0, IconType::Small); + } else { + icon::unset_for_window(self.window.0, IconType::Small); + } + self.window_state.lock().window_icon = window_icon; + } + + #[inline] + pub fn set_taskbar_icon(&self, taskbar_icon: Option) { + if let Some(ref taskbar_icon) = taskbar_icon { + taskbar_icon + .inner + .set_for_window(self.window.0, IconType::Big); + } else { + icon::unset_for_window(self.window.0, IconType::Big); + } + self.window_state.lock().taskbar_icon = taskbar_icon; + } + + pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) { + if unsafe { GetSystemMetrics(SM_IMMENABLED) } != 0 { + let composition_form = COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: POINT { x, y }, + rcArea: RECT::default(), + }; + unsafe { + let himc = ImmGetContext(self.window.0); + let _ = ImmSetCompositionWindow(himc, &composition_form); + let _ = ImmReleaseContext(self.window.0, himc); + } + } + } + + #[inline] + pub fn set_ime_position(&self, spot: Position) { + let (x, y) = spot.to_physical::(self.scale_factor()).into(); + self.set_ime_position_physical(x, y); + } + + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let window = self.window; + let active_window_handle = unsafe { GetActiveWindow() }; + if window.0 == active_window_handle { + // active window could be minimized, so we skip requesting attention + // if it is not minimized + let window_flags = self.window_state.lock().window_flags(); + let is_minimized = window_flags.contains(WindowFlags::MINIMIZED); + if !is_minimized { + return; + } + } + + let window_isize = window.0 .0 as isize; + + self.thread_executor.execute_in_thread(move || unsafe { + let (flags, count) = request_type + .map(|ty| match ty { + UserAttentionType::Critical => (FLASHW_ALL | FLASHW_TIMERNOFG, u32::MAX), + UserAttentionType::Informational => (FLASHW_TRAY, 4), + }) + .unwrap_or((FLASHW_STOP, 0)); + + let flash_info = FLASHWINFO { + cbSize: mem::size_of::() as u32, + hwnd: HWND(window_isize as _), + dwFlags: flags, + uCount: count, + dwTimeout: 0, + }; + let _ = FlashWindowEx(&flash_info); + }); + } + + #[inline] + pub fn theme(&self) -> Theme { + self.window_state.lock().current_theme + } + + pub fn set_theme(&self, theme: Option) { + { + let mut window_state = self.window_state.lock(); + if window_state.preferred_theme == theme { + return; + } + window_state.preferred_theme = theme; + } + unsafe { SendMessageW(self.hwnd(), *CHANGE_THEME_MSG_ID, None, None) }; + } + + #[inline] + pub fn reset_dead_keys(&self) { + // `ToUnicode` consumes the dead-key by default, so we are constructing a fake (but valid) + // key input which we can call `ToUnicode` with. + unsafe { + let vk = u32::from(VK_SPACE.0); + let scancode = MapVirtualKeyW(vk, MAPVK_VK_TO_VSC); + let kbd_state = [0; 256]; + let mut char_buff: [MaybeUninit; 8] = [MaybeUninit::uninit(); 8]; + ToUnicode( + vk, + scancode, + Some(&kbd_state), + mem::transmute::<&mut [std::mem::MaybeUninit], &mut [u16]>(char_buff.as_mut()), + 0, + ); + } + } + + #[inline] + pub fn begin_resize_drag(&self, edge: isize, button: u32, x: i32, y: i32) { + unsafe { + let w_param = WPARAM(edge as _); + let l_param = util::MAKELPARAM(x as i16, y as i16); + + let _ = ReleaseCapture(); + let _ = PostMessageW(Some(self.hwnd()), button, w_param, l_param); + } + } + + #[inline] + pub(crate) fn set_skip_taskbar(&self, skip: bool) -> Result<(), ExternalError> { + self.window_state.lock().skip_taskbar = skip; + unsafe { set_skip_taskbar(self.hwnd(), skip) } + } + + #[inline] + pub fn set_background_color(&self, color: Option) { + self.window_state.lock().background_color = color; + + unsafe { + let _ = InvalidateRect(Some(self.hwnd()), None, true); + let _ = UpdateWindow(self.hwnd()); + } + } + + #[inline] + pub fn set_progress_bar(&self, progress: ProgressBarState) { + unsafe { + let taskbar_list: ITaskbarList = CoCreateInstance(&TaskbarList, None, CLSCTX_SERVER).unwrap(); + let handle = self.window.0; + + if let Some(state) = progress.state { + let taskbar_state = { + match state { + ProgressState::None => TBPF_NOPROGRESS, + ProgressState::Indeterminate => TBPF_INDETERMINATE, + ProgressState::Normal => TBPF_NORMAL, + ProgressState::Error => TBPF_ERROR, + ProgressState::Paused => TBPF_PAUSED, + } + }; + + taskbar_list + .SetProgressState(handle, taskbar_state) + .unwrap_or(()); + } + if let Some(value) = progress.progress { + let value = if value > 100 { 100 } else { value }; + + taskbar_list + .SetProgressValue(handle, value, 100) + .unwrap_or(()); + } + } + } + + #[inline] + pub fn set_overlay_icon(&self, icon: Option<&Icon>) { + let taskbar: ITaskbarList = + unsafe { CoCreateInstance(&TaskbarList, None, CLSCTX_SERVER).unwrap() }; + + let icon = icon.map(|i| i.inner.as_raw_handle()).unwrap_or_default(); + + unsafe { + taskbar + .SetOverlayIcon(self.window.0, icon, None) + .unwrap_or(()); + } + } + + #[inline] + pub fn set_undecorated_shadow(&self, shadow: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock(), window.0, |f| { + f.set(WindowFlags::MARKER_UNDECORATED_SHADOW, shadow) + }); + }); + } + + #[inline] + pub fn has_undecorated_shadow(&self) -> bool { + self + .window_state + .lock() + .window_flags + .contains(WindowFlags::MARKER_UNDECORATED_SHADOW) + } + + pub fn set_content_protection(&self, enabled: bool) { + unsafe { + let _ = SetWindowDisplayAffinity( + self.hwnd(), + if enabled { + WDA_EXCLUDEFROMCAPTURE + } else { + WDA_NONE + }, + ); + } + } +} + +impl Drop for Window { + #[inline] + fn drop(&mut self) { + KEY_EVENT_BUILDERS.lock().remove(&self.id()); + unsafe { + // The window must be destroyed from the same thread that created it, so we send a + // custom message to be handled by our callback to do the actual work. + let _ = PostMessageW(Some(self.window.0), *DESTROY_MSG_ID, WPARAM(0), LPARAM(0)); + } + } +} + +unsafe fn init( + attributes: WindowAttributes, + pl_attribs: PlatformSpecificWindowBuilderAttributes, + event_loop: &EventLoopWindowTarget, +) -> Result { + // registering the window class + let class_name = register_window_class(&pl_attribs.window_classname); + + let mut window_flags = WindowFlags::empty(); + window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations); + window_flags.set( + WindowFlags::MARKER_UNDECORATED_SHADOW, + pl_attribs.decoration_shadow, + ); + window_flags.set(WindowFlags::ALWAYS_ON_BOTTOM, attributes.always_on_bottom); + window_flags.set(WindowFlags::ALWAYS_ON_TOP, attributes.always_on_top); + window_flags.set( + WindowFlags::NO_BACK_BUFFER, + pl_attribs.no_redirection_bitmap, + ); + window_flags.set(WindowFlags::TRANSPARENT, attributes.transparent); + // WindowFlags::VISIBLE and MAXIMIZED are set down below after the window has been configured. + window_flags.set(WindowFlags::RESIZABLE, attributes.resizable); + window_flags.set(WindowFlags::MINIMIZABLE, attributes.minimizable); + window_flags.set(WindowFlags::MAXIMIZABLE, attributes.maximizable); + // will be changed later using `window.set_closable` + // but we need to have a default for the diffing to work + window_flags.set(WindowFlags::CLOSABLE, true); + + window_flags.set(WindowFlags::FOCUSABLE, attributes.focusable); + + window_flags.set(WindowFlags::MARKER_DONT_FOCUS, !attributes.focused); + + window_flags.set(WindowFlags::RIGHT_TO_LEFT_LAYOUT, pl_attribs.rtl); + + let parent = match pl_attribs.parent { + Parent::ChildOf(parent) => { + window_flags.set(WindowFlags::CHILD, true); + if pl_attribs.menu.is_some() { + warn!("Setting a menu on a child window is unsupported"); + } + Some(parent) + } + Parent::OwnedBy(parent) => { + window_flags.set(WindowFlags::POPUP, true); + Some(parent) + } + Parent::None => { + window_flags.set(WindowFlags::ON_TASKBAR, true); + None + } + }; + + // creating the real window this time, by using the functions in `extra_functions` + let real_window = { + let (style, ex_style) = window_flags.to_window_styles(); + let title = util::encode_wide(&attributes.title); + + let (target_monitor, position) = attributes + .position + .and_then(|p| { + monitor::available_monitors() + .into_iter() + .find_map(|monitor| { + let dpi = monitor.dpi(); + let scale_factor = dpi_to_scale_factor(dpi); + let position = p.to_physical::(scale_factor); + let (x, y): (i32, i32) = monitor.position().into(); + let (width, height): (i32, i32) = monitor.size().into(); + + let frame_thickness = if window_flags.contains_shadow() { + util::get_frame_thickness(dpi) + } else { + 0 + }; + + // Only the starting position x needs to be accounted + if x <= position.x + frame_thickness + && position.x <= x + width + && y <= position.y + && position.y <= y + height + { + Some((monitor, position.into())) + } else { + None + } + }) + }) + .unwrap_or_else(|| (monitor::primary_monitor(), (CW_USEDEFAULT, CW_USEDEFAULT))); + + let desired_size = attributes + .inner_size + .unwrap_or_else(|| PhysicalSize::new(800, 600).into()); + let clamped_size = attributes + .inner_size_constraints + .clamp(desired_size, target_monitor.scale_factor()); + + // Best effort: try to create the window with the requested inner size + let adjusted_size = { + let (mut w, mut h): (i32, i32) = clamped_size + .to_physical::(target_monitor.scale_factor()) + .into(); + + if window_flags.contains(WindowFlags::MARKER_DECORATIONS) { + let mut rect = RECT { + left: 0, + top: 0, + right: w, + bottom: h, + }; + + unsafe { + AdjustWindowRectEx( + &mut rect, + window_flags.to_adjusted_window_styles().0, + pl_attribs.menu.is_some(), + ex_style, + )?; + } + + w = rect.right - rect.left; + h = rect.bottom - rect.top; + } else if window_flags.undecorated_with_shadows() { + let dpi = target_monitor.dpi(); + let insets = calculate_insets_for_dpi(dpi); + w += insets.left + insets.right; + h += insets.top + insets.bottom; + } + + (w, h) + }; + + let handle = CreateWindowExW( + ex_style, + PCWSTR::from_raw(class_name.as_ptr()), + PCWSTR::from_raw(title.as_ptr()), + style, + position.0, + position.1, + adjusted_size.0, + adjusted_size.1, + parent, + pl_attribs.menu, + GetModuleHandleW(PCWSTR::null()).map(Into::into).ok(), + Some(Box::into_raw(Box::new(window_flags)) as _), + )?; + + if !IsWindow(Some(handle)).as_bool() { + return Err(os_error!(OsError::IoError(io::Error::last_os_error()))); + } + + super::dark_mode::allow_dark_mode_for_window(handle, true); + + WindowWrapper(handle) + }; + + // Register for touch events if applicable + { + let digitizer = GetSystemMetrics(SM_DIGITIZER) as u32; + if digitizer & NID_READY != 0 { + RegisterTouchWindow(real_window.0, TWF_WANTPALM)?; + } + } + + let dpi = hwnd_dpi(real_window.0); + let scale_factor = dpi_to_scale_factor(dpi); + + // making the window transparent + if attributes.transparent && !pl_attribs.no_redirection_bitmap { + // Empty region for the blur effect, so the window is fully transparent + let region = CreateRectRgn(0, 0, -1, -1); + + let bb = DWM_BLURBEHIND { + dwFlags: DWM_BB_ENABLE | DWM_BB_BLURREGION, + fEnable: true.into(), + hRgnBlur: region, + fTransitionOnMaximized: false.into(), + }; + + let _ = DwmEnableBlurBehindWindow(real_window.0, &bb); + let _ = DeleteObject(region.into()); + } + + // If the system theme is dark, we need to set the window theme now + // before we update the window flags (and possibly show the + // window for the first time). + let current_theme = try_window_theme( + real_window.0, + attributes + .preferred_theme + .or(*event_loop.preferred_theme.lock()), + false, + ); + + let window_state = { + let window_state = WindowState::new( + &attributes, + None, + scale_factor, + current_theme, + attributes.preferred_theme, + attributes.background_color, + ); + let window_state = Arc::new(Mutex::new(window_state)); + WindowState::set_window_flags(window_state.lock(), real_window.0, |f| *f = window_flags); + window_state + }; + + let win = Window { + window: real_window, + window_state, + thread_executor: event_loop.create_thread_executor(), + }; + + KEY_EVENT_BUILDERS + .lock() + .insert(win.id(), KeyEventBuilder::default()); + + let _ = win.set_skip_taskbar(pl_attribs.skip_taskbar); + win.set_window_icon(attributes.window_icon); + win.set_taskbar_icon(pl_attribs.taskbar_icon); + + if attributes.fullscreen.is_some() { + win.set_fullscreen(attributes.fullscreen); + force_window_active(win.window.0); + } else if attributes.maximized { + win.set_maximized(true); + } + + if attributes.content_protection { + win.set_content_protection(true); + } + + win.set_visible(attributes.visible); + win.set_closable(attributes.closable); + + Ok(win) +} + +unsafe fn register_window_class(window_classname: &str) -> Vec { + let class_name = util::encode_wide(window_classname); + + let class = WNDCLASSEXW { + cbSize: mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW | CS_OWNDC, + lpfnWndProc: Some(window_proc), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: HINSTANCE(GetModuleHandleW(PCWSTR::null()).unwrap_or_default().0), + hIcon: HICON::default(), + hCursor: HCURSOR::default(), // must be null in order for cursor state to work properly + hbrBackground: HBRUSH::default(), + lpszMenuName: PCWSTR::null(), + lpszClassName: PCWSTR::from_raw(class_name.as_ptr()), + hIconSm: HICON::default(), + }; + + // We ignore errors because registering the same window class twice would trigger + // an error, and because errors here are detected during CreateWindowEx anyway. + // Also since there is no weird element in the struct, there is no reason for this + // call to fail. + RegisterClassExW(&class); + + class_name +} + +unsafe extern "system" fn window_proc( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + // This window procedure is only needed until the subclass procedure is attached. + // we need this because we need to respond to WM_NCCALCSIZE as soon as possible + // in order to make the window borderless if needed. + match msg { + win32wm::WM_NCCALCSIZE => { + let userdata = util::GetWindowLongPtrW(window, GWL_USERDATA); + if userdata != 0 { + let window_flags = WindowFlags::from_bits_truncate(userdata as _); + + if wparam == WPARAM(0) || window_flags.contains(WindowFlags::MARKER_DECORATIONS) { + return DefWindowProcW(window, msg, wparam, lparam); + } + + // adjust the maximized borderless window so it doesn't cover the taskbar + if util::is_maximized(window).unwrap_or(false) { + let params = &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS); + if let Ok(monitor_info) = + monitor::get_monitor_info(MonitorFromRect(¶ms.rgrc[0], MONITOR_DEFAULTTONULL)) + { + params.rgrc[0] = monitor_info.monitorInfo.rcWork; + } + } else if window_flags.contains(WindowFlags::MARKER_UNDECORATED_SHADOW) { + let params = &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS); + + let insets = util::calculate_window_insets(window); + + params.rgrc[0].left += insets.left; + params.rgrc[0].top += insets.top; + params.rgrc[0].right -= insets.right; + params.rgrc[0].bottom -= insets.bottom; + } + return LRESULT(0); // return 0 here to make the window borderless + } + + DefWindowProcW(window, msg, wparam, lparam) + } + win32wm::WM_NCCREATE => { + let userdata = util::GetWindowLongPtrW(window, GWL_USERDATA); + if userdata == 0 { + let createstruct = &*(lparam.0 as *const CREATESTRUCTW); + let userdata = createstruct.lpCreateParams; + let window_flags = Box::from_raw(userdata as *mut WindowFlags); + util::SetWindowLongPtrW(window, GWL_USERDATA, window_flags.bits() as _); + } + DefWindowProcW(window, msg, wparam, lparam) + } + _ => DefWindowProcW(window, msg, wparam, lparam), + } +} + +struct ComInitialized(Option<()>); +impl Drop for ComInitialized { + fn drop(&mut self) { + if let Some(()) = self.0.take() { + unsafe { CoUninitialize() }; + } + } +} + +thread_local! { + static COM_INITIALIZED: ComInitialized = { + unsafe { + ComInitialized(match CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok() { + Ok(()) => Some(()), + Err(_) => None, + }) + } + }; + + static TASKBAR_LIST: RefCell> = const { RefCell::new(None) }; +} + +pub fn com_initialized() { + COM_INITIALIZED.with(|_| {}); +} + +// Reference Implementation: +// https://github.com/chromium/chromium/blob/f18e79d901f56154f80eea1e2218544285e62623/ui/views/win/fullscreen_handler.cc +// +// As per MSDN marking the window as fullscreen should ensure that the +// taskbar is moved to the bottom of the Z-order when the fullscreen window +// is activated. If the window is not fullscreen, the Shell falls back to +// heuristics to determine how the window should be treated, which means +// that it could still consider the window as fullscreen. :( +unsafe fn taskbar_mark_fullscreen(handle: HWND, fullscreen: bool) { + com_initialized(); + + TASKBAR_LIST.with(|task_bar_list_ptr| { + let mut task_bar_list = task_bar_list_ptr.borrow().clone(); + + if task_bar_list.is_none() { + let result: windows::core::Result = + CoCreateInstance(&TaskbarList, None, CLSCTX_ALL); + if let Ok(created) = result { + if let Ok(()) = created.HrInit() { + task_bar_list = Some(created); + } + } + + if task_bar_list.is_none() { + return; + } + + *task_bar_list_ptr.borrow_mut() = task_bar_list.clone(); + } + + let _ = task_bar_list + .unwrap() + .MarkFullscreenWindow(handle, fullscreen); + }) +} + +unsafe fn force_window_active(handle: HWND) { + // Try to focus the window without the hack first. + if SetForegroundWindow(handle).as_bool() { + return; + } + + // In some situations, calling SetForegroundWindow could not bring up the window, + // This is a little hack which can "steal" the foreground window permission. + // We only call this function in the window creation, so it should be fine. + // See: https://stackoverflow.com/questions/10740346/setforegroundwindow-only-working-while-visual-studio-is-open + let alt_sc = MapVirtualKeyW(u32::from(VK_MENU.0), MAPVK_VK_TO_VSC); + + let mut inputs: [INPUT; 2] = mem::zeroed(); + inputs[0].r#type = INPUT_KEYBOARD; + inputs[0].Anonymous.ki.wVk = VK_LMENU as _; + inputs[0].Anonymous.ki.wScan = alt_sc as _; + inputs[0].Anonymous.ki.dwFlags = KEYEVENTF_EXTENDEDKEY; + + inputs[1].r#type = INPUT_KEYBOARD; + inputs[1].Anonymous.ki.wVk = VK_LMENU as _; + inputs[1].Anonymous.ki.wScan = alt_sc as _; + inputs[1].Anonymous.ki.dwFlags = KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP; + + // Simulate a key press and release + SendInput(&inputs, mem::size_of::() as _); + + let _ = SetForegroundWindow(handle); +} + +pub(crate) unsafe fn set_skip_taskbar(hwnd: HWND, skip: bool) -> Result<(), ExternalError> { + com_initialized(); + let taskbar_list: ITaskbarList = CoCreateInstance(&TaskbarList, None, CLSCTX_SERVER)?; + if skip { + taskbar_list.DeleteTab(hwnd)?; + } else { + taskbar_list.AddTab(hwnd)?; + } + + Ok(()) +} + +impl ResizeDirection { + pub(crate) fn to_win32(&self) -> u32 { + match self { + ResizeDirection::East => HTRIGHT, + ResizeDirection::North => HTTOP, + ResizeDirection::NorthEast => HTTOPRIGHT, + ResizeDirection::NorthWest => HTTOPLEFT, + ResizeDirection::South => HTBOTTOM, + ResizeDirection::SouthEast => HTBOTTOMRIGHT, + ResizeDirection::SouthWest => HTBOTTOMLEFT, + ResizeDirection::West => HTLEFT, + } + } +} diff --git a/vendor/tao/src/platform_impl/windows/window_state.rs b/vendor/tao/src/platform_impl/windows/window_state.rs new file mode 100644 index 0000000000..6437c80df2 --- /dev/null +++ b/vendor/tao/src/platform_impl/windows/window_state.rs @@ -0,0 +1,513 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + dpi::PhysicalPosition, + icon::Icon, + keyboard::ModifiersState, + platform_impl::platform::{event_loop, minimal_ime::MinimalIme, util}, + window::{CursorIcon, Fullscreen, Theme, WindowAttributes, WindowSizeConstraints, RGBA}, +}; +use parking_lot::MutexGuard; +use std::io; +use windows::Win32::{ + Foundation::{HWND, LPARAM, RECT, WPARAM}, + Graphics::Gdi::InvalidateRgn, + UI::WindowsAndMessaging::*, +}; + +/// Contains information about states and the window that the callback is going to use. +pub struct WindowState { + pub mouse: MouseProperties, + + /// Used by `WM_GETMINMAXINFO`. + pub size_constraints: WindowSizeConstraints, + + pub window_icon: Option, + pub taskbar_icon: Option, + + pub saved_window: Option, + pub scale_factor: f64, + + pub dragging: bool, + + pub skip_taskbar: bool, + + pub modifiers_state: ModifiersState, + pub fullscreen: Option, + pub current_theme: Theme, + pub preferred_theme: Option, + + pub ime_handler: MinimalIme, + + pub window_flags: WindowFlags, + + // Used by WM_NCACTIVATE, WM_SETFOCUS and WM_KILLFOCUS + pub is_active: bool, + pub is_focused: bool, + + pub background_color: Option, +} + +unsafe impl Send for WindowState {} +unsafe impl Sync for WindowState {} + +#[derive(Clone)] +pub struct SavedWindow { + pub placement: WINDOWPLACEMENT, +} + +#[derive(Clone)] +pub struct MouseProperties { + pub cursor: CursorIcon, + pub capture_count: u32, + cursor_flags: CursorFlags, + pub last_position: Option>, +} + +bitflags! { + #[derive(Clone, Copy)] + pub struct CursorFlags: u8 { + const GRABBED = 1 << 0; + const HIDDEN = 1 << 1; + const IN_WINDOW = 1 << 2; + } +} +bitflags! { + #[derive(Clone, Copy, PartialEq)] + pub struct WindowFlags: u32 { + const RESIZABLE = 1 << 0; + const VISIBLE = 1 << 1; + const ON_TASKBAR = 1 << 2; + const ALWAYS_ON_TOP = 1 << 3; + const NO_BACK_BUFFER = 1 << 4; + const TRANSPARENT = 1 << 5; + const CHILD = 1 << 6; + const MAXIMIZED = 1 << 7; + const POPUP = 1 << 8; + const ALWAYS_ON_BOTTOM = 1 << 9; + const MINIMIZABLE = 1 << 10; + const MAXIMIZABLE = 1 << 11; + const CLOSABLE = 1 << 12; + const MINIMIZED = 1 << 13; + + const IGNORE_CURSOR_EVENT = 1 << 14; + + /// Marker flag for fullscreen. Should always match `WindowState::fullscreen`, but is + /// included here to make masking easier. + const MARKER_EXCLUSIVE_FULLSCREEN = 1 << 15; + const MARKER_BORDERLESS_FULLSCREEN = 1 << 16; + + /// The `WM_SIZE` event contains some parameters that can effect the state of `WindowFlags`. + /// In most cases, it's okay to let those parameters change the state. However, when we're + /// running the `WindowFlags::apply_diff` function, we *don't* want those parameters to + /// effect our stored state, because the purpose of `apply_diff` is to update the actual + /// window's state to match our stored state. This controls whether to accept those changes. + const MARKER_RETAIN_STATE_ON_SIZE = 1 << 17; + + const MARKER_IN_SIZE_MOVE = 1 << 18; + + const MARKER_DONT_FOCUS = 1 << 19; + + /// Fully decorated window (incl. caption, border and drop shadow). + const MARKER_DECORATIONS = 1 << 20; + /// Drop shadow for undecorated windows. + const MARKER_UNDECORATED_SHADOW = 1 << 21; + + const RIGHT_TO_LEFT_LAYOUT = 1 << 22; + + const FOCUSABLE = 1 << 23; + + const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits(); + } +} + +impl WindowState { + pub fn new( + attributes: &WindowAttributes, + taskbar_icon: Option, + scale_factor: f64, + current_theme: Theme, + preferred_theme: Option, + background_color: Option, + ) -> WindowState { + WindowState { + mouse: MouseProperties { + cursor: CursorIcon::default(), + capture_count: 0, + cursor_flags: CursorFlags::empty(), + last_position: None, + }, + + size_constraints: attributes.inner_size_constraints, + + window_icon: attributes.window_icon.clone(), + taskbar_icon, + + saved_window: None, + scale_factor, + + dragging: false, + + skip_taskbar: false, + + modifiers_state: ModifiersState::default(), + fullscreen: None, + current_theme, + preferred_theme, + ime_handler: MinimalIme::default(), + window_flags: WindowFlags::empty(), + is_active: false, + is_focused: false, + + background_color, + } + } + + pub fn window_flags(&self) -> WindowFlags { + self.window_flags + } + + pub fn set_window_flags(mut this: MutexGuard<'_, Self>, window: HWND, f: F) + where + F: FnOnce(&mut WindowFlags), + { + let old_flags = this.window_flags; + f(&mut this.window_flags); + let new_flags = this.window_flags; + + drop(this); + old_flags.apply_diff(window, new_flags); + } + + pub fn set_window_flags_in_place(&mut self, f: F) + where + F: FnOnce(&mut WindowFlags), + { + f(&mut self.window_flags); + } + + pub fn has_active_focus(&self) -> bool { + self.is_active && self.is_focused + } + + // Updates is_active and returns whether active-focus state has changed + pub fn set_active(&mut self, is_active: bool) -> bool { + let old = self.has_active_focus(); + self.is_active = is_active; + old != self.has_active_focus() + } + + // Updates is_focused and returns whether active-focus state has changed + pub fn set_focused(&mut self, is_focused: bool) -> bool { + let old = self.has_active_focus(); + self.is_focused = is_focused; + old != self.has_active_focus() + } +} + +impl MouseProperties { + pub fn cursor_flags(&self) -> CursorFlags { + self.cursor_flags + } + + pub fn set_cursor_flags(&mut self, window: HWND, f: F) -> Result<(), io::Error> + where + F: FnOnce(&mut CursorFlags), + { + let old_flags = self.cursor_flags; + f(&mut self.cursor_flags); + match self.cursor_flags.refresh_os_cursor(window) { + Ok(()) => (), + Err(e) => { + self.cursor_flags = old_flags; + return Err(e); + } + } + + Ok(()) + } +} + +impl WindowFlags { + fn mask(mut self) -> WindowFlags { + if self.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN) { + self |= WindowFlags::EXCLUSIVE_FULLSCREEN_OR_MASK; + } + + self + } + + pub fn to_window_styles(self) -> (WINDOW_STYLE, WINDOW_EX_STYLE) { + let (mut style, mut style_ex) = (Default::default(), Default::default()); + style |= WS_CAPTION | WS_CLIPSIBLINGS | WS_SYSMENU; + style_ex |= WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES; + if self.contains(WindowFlags::RESIZABLE) { + style |= WS_SIZEBOX; + } + if self.contains(WindowFlags::MAXIMIZABLE) { + style |= WS_MAXIMIZEBOX; + } + if self.contains(WindowFlags::MINIMIZABLE) { + style |= WS_MINIMIZEBOX; + } + if self.contains(WindowFlags::VISIBLE) { + style |= WS_VISIBLE; + } + if self.contains(WindowFlags::ON_TASKBAR) { + style_ex |= WS_EX_APPWINDOW; + } + if self.contains(WindowFlags::ALWAYS_ON_TOP) { + style_ex |= WS_EX_TOPMOST; + } + if self.contains(WindowFlags::NO_BACK_BUFFER) { + style_ex |= WS_EX_NOREDIRECTIONBITMAP; + } + if self.contains(WindowFlags::CHILD) { + style |= WS_CHILD; // This is incompatible with WS_POPUP if that gets added eventually. + + // Remove decorations window styles for child + if !self.contains(WindowFlags::MARKER_DECORATIONS) { + style &= !WS_CAPTION; + style_ex &= !WS_EX_WINDOWEDGE; + } + } + if self.contains(WindowFlags::POPUP) { + style |= WS_POPUP; + } + if self.contains(WindowFlags::MINIMIZED) { + style |= WS_MINIMIZE; + } + if self.contains(WindowFlags::MAXIMIZED) { + style |= WS_MAXIMIZE; + } + if self.contains(WindowFlags::IGNORE_CURSOR_EVENT) { + style_ex |= WS_EX_TRANSPARENT | WS_EX_LAYERED; + } + if self.intersects( + WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN | WindowFlags::MARKER_BORDERLESS_FULLSCREEN, + ) { + style &= !WS_OVERLAPPEDWINDOW; + } + if self.contains(WindowFlags::RIGHT_TO_LEFT_LAYOUT) { + style_ex |= WS_EX_LAYOUTRTL | WS_EX_RTLREADING | WS_EX_RIGHT; + } + if !self.contains(WindowFlags::FOCUSABLE) { + style_ex |= WS_EX_NOACTIVATE; + } + + (style, style_ex) + } + + /// Returns the appropriate window styles for `AdjustWindowRectEx` + pub fn to_adjusted_window_styles(self) -> (WINDOW_STYLE, WINDOW_EX_STYLE) { + let (mut style, style_ex) = self.to_window_styles(); + + if !self.contains(WindowFlags::MARKER_DECORATIONS) { + style &= !(WS_CAPTION | WS_THICKFRAME) + } + + (style, style_ex) + } + + /// Adjust the window client rectangle to the return value, if present. + fn apply_diff(mut self, window: HWND, mut new: WindowFlags) { + self = self.mask(); + new = new.mask(); + + let mut diff = self ^ new; + + if diff == WindowFlags::empty() { + return; + } + + if new.contains(WindowFlags::VISIBLE) { + unsafe { + let _ = ShowWindow( + window, + if self.contains(WindowFlags::MARKER_DONT_FOCUS) { + self.set(WindowFlags::MARKER_DONT_FOCUS, false); + SW_SHOWNOACTIVATE + } else { + SW_SHOW + }, + ); + } + } + + if diff.contains(WindowFlags::ALWAYS_ON_TOP) { + unsafe { + let _ = SetWindowPos( + window, + Some(if new.contains(WindowFlags::ALWAYS_ON_TOP) { + HWND_TOPMOST + } else { + HWND_NOTOPMOST + }), + 0, + 0, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, + ); + let _ = InvalidateRgn(window, None, false); + } + } + + if diff.contains(WindowFlags::ALWAYS_ON_BOTTOM) { + unsafe { + let _ = SetWindowPos( + window, + Some(if new.contains(WindowFlags::ALWAYS_ON_BOTTOM) { + HWND_BOTTOM + } else { + HWND_NOTOPMOST + }), + 0, + 0, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, + ); + let _ = InvalidateRgn(window, None, false); + } + } + + if diff.contains(WindowFlags::MAXIMIZED) || new.contains(WindowFlags::MAXIMIZED) { + unsafe { + let _ = ShowWindow( + window, + match new.contains(WindowFlags::MAXIMIZED) { + true => SW_MAXIMIZE, + false => SW_RESTORE, + }, + ); + } + } + + // Minimize operations should execute after maximize for proper window animations + if diff.contains(WindowFlags::MINIMIZED) { + unsafe { + let _ = ShowWindow( + window, + match new.contains(WindowFlags::MINIMIZED) { + true => SW_MINIMIZE, + false => SW_RESTORE, + }, + ); + } + + diff.remove(WindowFlags::MINIMIZED); + } + + if diff.contains(WindowFlags::CLOSABLE) || new.contains(WindowFlags::CLOSABLE) { + unsafe { + let system_menu = GetSystemMenu(window, false); + let _ = EnableMenuItem( + system_menu, + SC_CLOSE, + MF_BYCOMMAND + | if new.contains(WindowFlags::CLOSABLE) { + MF_ENABLED + } else { + MF_GRAYED + }, + ); + } + } + + if !new.contains(WindowFlags::VISIBLE) { + unsafe { + let _ = ShowWindow(window, SW_HIDE); + } + } + + if diff != WindowFlags::empty() { + let (style, style_ex) = new.to_window_styles(); + + unsafe { + SendMessageW( + window, + *event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID, + Some(WPARAM(1)), + Some(LPARAM(0)), + ); + + // This condition is necessary to avoid having an unrestorable window + if !new.contains(WindowFlags::MINIMIZED) { + SetWindowLongW(window, GWL_STYLE, style.0 as i32); + SetWindowLongW(window, GWL_EXSTYLE, style_ex.0 as i32); + } + + let mut flags = SWP_NOZORDER | SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED; + + // We generally don't want style changes here to affect window + // focus, but for fullscreen windows they must be activated + // (i.e. focused) so that they appear on top of the taskbar + if !new.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN) + && !new.contains(WindowFlags::MARKER_BORDERLESS_FULLSCREEN) + { + flags |= SWP_NOACTIVATE; + } + + // Refresh the window frame + let _ = SetWindowPos(window, None, 0, 0, 0, 0, flags); + SendMessageW( + window, + *event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID, + Some(WPARAM(0)), + Some(LPARAM(0)), + ); + } + } + } + + pub fn undecorated_with_shadows(&self) -> bool { + self.contains(WindowFlags::MARKER_UNDECORATED_SHADOW) + && !self.contains(WindowFlags::MARKER_DECORATIONS) + } + + pub fn contains_shadow(&self) -> bool { + self.contains(WindowFlags::MARKER_UNDECORATED_SHADOW) + || self.contains(WindowFlags::MARKER_DECORATIONS) + } +} + +impl CursorFlags { + fn refresh_os_cursor(self, window: HWND) -> Result<(), io::Error> { + let client_rect = util::get_client_rect(window)?; + + if util::is_focused(window) { + let cursor_clip = match self.contains(CursorFlags::GRABBED) { + true => Some(client_rect), + false => None, + }; + + let rect_to_tuple = |rect: RECT| (rect.left, rect.top, rect.right, rect.bottom); + let active_cursor_clip = rect_to_tuple(util::get_cursor_clip()?); + let desktop_rect = rect_to_tuple(util::get_desktop_rect()); + + let active_cursor_clip = match desktop_rect == active_cursor_clip { + true => None, + false => Some(active_cursor_clip), + }; + + // We do this check because calling `set_cursor_clip` incessantly will flood the event + // loop with `WM_MOUSEMOVE` events, and `refresh_os_cursor` is called by `set_cursor_flags` + // which at times gets called once every iteration of the eventloop. + if active_cursor_clip != cursor_clip.map(rect_to_tuple) { + util::set_cursor_clip(cursor_clip)?; + } + } + + let cursor_in_client = self.contains(CursorFlags::IN_WINDOW); + if cursor_in_client { + util::set_cursor_hidden(self.contains(CursorFlags::HIDDEN)); + } else { + util::set_cursor_hidden(false); + } + + Ok(()) + } +} diff --git a/vendor/tao/src/window.rs b/vendor/tao/src/window.rs new file mode 100644 index 0000000000..9ec5ccc497 --- /dev/null +++ b/vendor/tao/src/window.rs @@ -0,0 +1,1698 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +//! The `Window` struct and associated types. +use std::fmt; + +use crate::{ + dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Pixel, PixelUnit, Position, Size}, + error::{ExternalError, NotSupportedError, OsError}, + event_loop::EventLoopWindowTarget, + monitor::{MonitorHandle, VideoMode}, + platform_impl, +}; + +pub use crate::icon::{BadIcon, Icon}; + +/// Progress State +#[derive(Debug, Clone, Copy)] +pub enum ProgressState { + None, + Normal, + /// **Treated as Normal in linux and macOS** + Indeterminate, + /// **Treated as Normal in linux** + Paused, + /// **Treated as Normal in linux** + Error, +} + +pub struct ProgressBarState { + /// The progress bar state. + pub state: Option, + /// The progress bar progress. This can be a value ranging from `0` to `100` + pub progress: Option, + /// The `.desktop` filename with the Unity desktop window manager, for example `myapp.desktop` **Linux Only** + pub desktop_filename: Option, +} + +/// Represents a window. +/// +/// # Example +/// +/// ```no_run +/// use tao::{ +/// event::{Event, WindowEvent}, +/// event_loop::{ControlFlow, EventLoop}, +/// window::Window, +/// }; +/// +/// let mut event_loop = EventLoop::new(); +/// let window = Window::new(&event_loop).unwrap(); +/// +/// event_loop.run(move |event, _, control_flow| { +/// *control_flow = ControlFlow::Wait; +/// +/// match event { +/// Event::WindowEvent { +/// event: WindowEvent::CloseRequested, +/// .. +/// } => *control_flow = ControlFlow::Exit, +/// _ => (), +/// } +/// }); +/// ``` +pub struct Window { + pub(crate) window: platform_impl::Window, +} + +impl fmt::Debug for Window { + fn fmt(&self, fmtr: &mut fmt::Formatter<'_>) -> fmt::Result { + fmtr.pad("Window { .. }") + } +} + +impl Drop for Window { + fn drop(&mut self) { + // If the window is in exclusive fullscreen, we must restore the desktop + // video mode (generally this would be done on application exit, but + // closing the window doesn't necessarily always mean application exit, + // such as when there are multiple windows) + if let Some(Fullscreen::Exclusive(_)) = self.fullscreen() { + self.set_fullscreen(None); + } + } +} + +/// Type alias for a color in the RGBA format. +/// +/// Each value can be 0..255 inclusive. +pub type RGBA = (u8, u8, u8, u8); + +/// Identifier of a window. Unique for each window. +/// +/// Can be obtained with `window.id()`. +/// +/// Whenever you receive an event specific to a window, this event contains a `WindowId` which you +/// can then compare to the ids of your windows. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(pub(crate) platform_impl::WindowId); + +impl WindowId { + /// # Safety + /// Returns a dummy `WindowId`, useful for unit testing. The only guarantee made about the return + /// value of this function is that it will always be equal to itself and to future values returned + /// by this function. No other guarantees are made. This may be equal to a real `WindowId`. + /// + /// **Passing this into a tao function will result in undefined behavior.** + pub unsafe fn dummy() -> Self { + WindowId(platform_impl::WindowId::dummy()) + } +} + +/// Object that allows you to build windows. +#[derive(Clone, Default)] +pub struct WindowBuilder { + /// The attributes to use to create the window. + pub window: WindowAttributes, + + // Platform-specific configuration. + pub(crate) platform_specific: platform_impl::PlatformSpecificWindowBuilderAttributes, +} + +impl fmt::Debug for WindowBuilder { + fn fmt(&self, fmtr: &mut fmt::Formatter<'_>) -> fmt::Result { + fmtr + .debug_struct("WindowBuilder") + .field("window", &self.window) + .finish() + } +} + +/// Attributes to use when creating a window. +#[derive(Debug, Clone)] +pub struct WindowAttributes { + /// The dimensions of the window. If this is `None`, some platform-specific dimensions will be + /// used. + /// + /// The default is `None`. + pub inner_size: Option, + + /// The window size constraints + pub inner_size_constraints: WindowSizeConstraints, + + /// The desired position of the window. If this is `None`, some platform-specific position + /// will be chosen. + /// + /// The default is `None`. + /// + /// ## Platform-specific + /// + /// - **macOS**: The top left corner position of the window content, the window's "inner" + /// position. The window title bar will be placed above it. + /// The window will be positioned such that it fits on screen, maintaining + /// set `inner_size` if any. + /// If you need to precisely position the top left corner of the whole window you have to + /// use [`Window::set_outer_position`] after creating the window. + /// - **Windows**: The top left corner position of the window title bar, the window's "outer" + /// position. + /// There may be a small gap between this position and the window due to the specifics of the + /// Window Manager. + /// - **Linux**: The top left corner of the window, the window's "outer" position. + /// - **Linux(Wayland)**: Unsupported. + /// - **Others**: Ignored. + /// + /// See [`Window::set_outer_position`]. + /// + /// [`Window::set_outer_position`]: crate::window::Window::set_outer_position + pub position: Option, + + /// Whether the window is resizable or not. + /// + /// The default is `true`. + pub resizable: bool, + + /// Whether the window is minimizable or not. + /// + /// The default is `true`. + /// + /// See [`Window::set_minimizable`] for details. + pub minimizable: bool, + + /// Whether the window is maximizable or not. + /// + /// The default is `true`. + /// + /// See [`Window::set_maximizable`] for details. + pub maximizable: bool, + + /// Whether the window is closable or not. + /// + /// The default is `true`. + /// + /// See [`Window::set_closable`] for details. + pub closable: bool, + + /// Whether the window should be set as fullscreen upon creation. + /// + /// The default is `None`. + pub fullscreen: Option, + + /// The title of the window in the title bar. + /// + /// The default is `"tao window"`. + pub title: String, + + /// Whether the window should be maximized upon creation. + /// + /// The default is `false`. + pub maximized: bool, + + /// Whether the window should be immediately visible upon creation. + /// + /// The default is `true`. + pub visible: bool, + + /// Whether the the window should be transparent. If this is true, writing colors + /// with alpha values different than `1.0` will produce a transparent window. + /// + /// The default is `false`. + pub transparent: bool, + + /// Whether the window should have borders and bars. + /// + /// The default is `true`. + pub decorations: bool, + + /// Whether the window should always be on top of other windows. + /// + /// The default is `false`. + /// + /// ## Platform-specific: + /// + /// - **Linux(x11):** Result depends on the system's window manager. Consider this setting a suggestion. + /// - **Linux(Wayland):** Unsupported. + // TODO: Unsupported in gtk4 + pub always_on_top: bool, + + /// Whether the window should always be on bottom of other windows. + /// + /// The default is `false`. + /// + /// ## Platform-specific: + /// + /// - **Linux(x11):** Result depends on the system's window manager. Consider this setting a suggestion. + /// - **Linux(Wayland):** Unsupported. + // TODO: Unsupported in gtk4 + pub always_on_bottom: bool, + + /// The window icon. + /// + /// The default is `None`. + pub window_icon: Option, + + pub preferred_theme: Option, + + /// Whether the window should be initially focused or not. + /// + /// ## Platform-specific: + /// + /// **Android / iOS:** Unsupported. + pub focused: bool, + + /// Whether the window should be focusable or not. + /// + /// ## Platform-specific: + /// + /// **Android / iOS:** Unsupported. + pub focusable: bool, + + /// Prevents the window contents from being captured by other apps. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Linux:** Unsupported. + pub content_protection: bool, + + /// Sets whether the window should be visible on all workspaces. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Windows:** Unsupported. + pub visible_on_all_workspaces: bool, + + /// Sets the window background color. + /// + /// ## Platform-specific: + /// + /// - **Windows:** alpha channel is ignored. Instead manually draw the window, for example using `softbuffer` crate, see + /// - **iOS / Android:** Unsupported. + pub background_color: Option, +} + +impl Default for WindowAttributes { + #[inline] + fn default() -> WindowAttributes { + WindowAttributes { + inner_size: None, + inner_size_constraints: Default::default(), + position: None, + resizable: true, + minimizable: true, + maximizable: true, + closable: true, + title: "tao window".to_owned(), + maximized: false, + fullscreen: None, + visible: true, + transparent: false, + decorations: true, + always_on_top: false, + always_on_bottom: false, + window_icon: None, + preferred_theme: None, + focused: true, + focusable: true, + content_protection: false, + visible_on_all_workspaces: false, + background_color: None, + } + } +} + +impl WindowBuilder { + /// Initializes a new `WindowBuilder` with default values. + #[inline] + pub fn new() -> Self { + Default::default() + } + + /// Requests the window to be of specific dimensions. + /// + /// See [`Window::set_inner_size`] for details. + /// + /// [`Window::set_inner_size`]: crate::window::Window::set_inner_size + #[inline] + pub fn with_inner_size>(mut self, size: S) -> Self { + self.window.inner_size = Some(size.into()); + self + } + + /// Sets a minimum dimension size for the window. + /// + /// See [`Window::set_min_inner_size`] for details. + /// + /// [`Window::set_min_inner_size`]: crate::window::Window::set_min_inner_size + #[inline] + pub fn with_min_inner_size>(mut self, min_size: S) -> Self { + let size: Size = min_size.into(); + let (width, height) = crate::extract_width_height(size); + self.window.inner_size_constraints.min_width = Some(width); + self.window.inner_size_constraints.min_height = Some(height); + self + } + + /// Sets a maximum dimension size for the window. + /// + /// See [`Window::set_max_inner_size`] for details. + /// + /// [`Window::set_max_inner_size`]: crate::window::Window::set_max_inner_size + #[inline] + pub fn with_max_inner_size>(mut self, max_size: S) -> Self { + let size: Size = max_size.into(); + let (width, height) = crate::extract_width_height(size); + self.window.inner_size_constraints.max_width = Some(width); + self.window.inner_size_constraints.max_height = Some(height); + self + } + + /// Sets inner size constraints for the window. + /// + /// See [`Window::set_inner_size_constraints`] for details. + /// + /// [`Window::set_inner_size_constraints`]: crate::window::Window::set_inner_size_constraints + #[inline] + pub fn with_inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self { + self.window.inner_size_constraints = constraints; + self + } + + /// Sets a desired initial position for the window. + /// + /// See [`WindowAttributes::position`] for details. + /// + /// [`WindowAttributes::position`]: crate::window::WindowAttributes::position + #[inline] + pub fn with_position>(mut self, position: P) -> Self { + self.window.position = Some(position.into()); + self + } + + /// Sets whether the window is resizable or not. + /// + /// See [`Window::set_resizable`] for details. + /// + /// [`Window::set_resizable`]: crate::window::Window::set_resizable + #[inline] + pub fn with_resizable(mut self, resizable: bool) -> Self { + self.window.resizable = resizable; + self + } + + /// Sets whether the window is minimizable or not. + /// + /// See [`Window::set_minimizable`] for details. + /// + /// [`Window::set_minimizable`]: crate::window::Window::set_minimizable + #[inline] + pub fn with_minimizable(mut self, minimizable: bool) -> Self { + self.window.minimizable = minimizable; + self + } + + /// Sets whether the window is maximizable or not. + /// + /// See [`Window::set_maximizable`] for details. + /// + /// [`Window::set_maximizable`]: crate::window::Window::set_maximizable + #[inline] + pub fn with_maximizable(mut self, maximizable: bool) -> Self { + self.window.maximizable = maximizable; + self + } + + /// Sets whether the window is closable or not. + /// + /// See [`Window::set_closable`] for details. + /// + /// [`Window::set_closable`]: crate::window::Window::set_closable + #[inline] + pub fn with_closable(mut self, closable: bool) -> Self { + self.window.closable = closable; + self + } + + /// Requests a specific title for the window. + /// + /// See [`Window::set_title`] for details. + /// + /// [`Window::set_title`]: crate::window::Window::set_title + #[inline] + pub fn with_title>(mut self, title: T) -> Self { + self.window.title = title.into(); + self + } + + /// Sets the window fullscreen state. + /// + /// See [`Window::set_fullscreen`] for details. + /// + /// [`Window::set_fullscreen`]: crate::window::Window::set_fullscreen + #[inline] + pub fn with_fullscreen(mut self, fullscreen: Option) -> Self { + self.window.fullscreen = fullscreen; + self + } + + /// Requests maximized mode. + /// + /// See [`Window::set_maximized`] for details. + /// + /// [`Window::set_maximized`]: crate::window::Window::set_maximized + #[inline] + pub fn with_maximized(mut self, maximized: bool) -> Self { + self.window.maximized = maximized; + self + } + + /// Sets whether the window will be initially hidden or visible. + /// + /// See [`Window::set_visible`] for details. + /// + /// [`Window::set_visible`]: crate::window::Window::set_visible + #[inline] + pub fn with_visible(mut self, visible: bool) -> Self { + self.window.visible = visible; + self + } + + /// Sets whether the background of the window should be transparent. + #[inline] + pub fn with_transparent(mut self, transparent: bool) -> Self { + self.window.transparent = transparent; + self + } + + /// Sets whether the window should have a border, a title bar, etc. + /// + /// See [`Window::set_decorations`] for details. + /// + /// [`Window::set_decorations`]: crate::window::Window::set_decorations + #[inline] + pub fn with_decorations(mut self, decorations: bool) -> Self { + self.window.decorations = decorations; + self + } + + /// Sets whether or not the window will always be below other windows. + /// + /// See [`Window::set_always_on_bottom`] for details. + /// + /// [`Window::set_always_on_bottom`]: crate::window::Window::set_always_on_bottom + #[inline] + pub fn with_always_on_bottom(mut self, always_on_bottom: bool) -> Self { + self.window.always_on_top = false; + self.window.always_on_bottom = always_on_bottom; + self + } + + /// Sets whether or not the window will always be on top of other windows. + /// + /// See [`Window::set_always_on_top`] for details. + /// + /// [`Window::set_always_on_top`]: crate::window::Window::set_always_on_top + #[inline] + pub fn with_always_on_top(mut self, always_on_top: bool) -> Self { + self.window.always_on_bottom = false; + self.window.always_on_top = always_on_top; + self + } + + /// Sets the window icon. + /// + /// See [`Window::set_window_icon`] for details. + /// + /// [`Window::set_window_icon`]: crate::window::Window::set_window_icon + #[inline] + pub fn with_window_icon(mut self, window_icon: Option) -> Self { + self.window.window_icon = window_icon; + self + } + + /// Forces a theme or uses the system settings if `None` was provided. + /// + /// ## Platform-specific: + /// + /// - **Windows**: It is recommended to always use the same theme used + /// in [`EventLoopBuilderExtWindows::with_theme`] for this method also + /// or use `None` so it automatically uses the theme used in [`EventLoopBuilderExtWindows::with_theme`] + /// or falls back to the system preference, because [`EventLoopBuilderExtWindows::with_theme`] changes + /// the theme for some controls like context menus which is app-wide and can't be changed by this method. + /// + /// [`EventLoopBuilderExtWindows::with_theme`]: crate::platform::windows::EventLoopBuilderExtWindows::with_theme + #[allow(rustdoc::broken_intra_doc_links)] + #[inline] + pub fn with_theme(mut self, theme: Option) -> WindowBuilder { + self.window.preferred_theme = theme; + self + } + + /// Whether the window will be initially focused or not. + /// + /// ## Platform-specific: + /// + /// **Android / iOS:** Unsupported. + #[inline] + pub fn with_focused(mut self, focused: bool) -> WindowBuilder { + self.window.focused = focused; + self + } + + /// Whether the window will be focusable or not. + /// + /// ## Platform-specific: + /// **Android / iOS:** Unsupported. + #[inline] + pub fn with_focusable(mut self, focusable: bool) -> WindowBuilder { + self.window.focusable = focusable; + self + } + + /// Prevents the window contents from being captured by other apps. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Linux:** Unsupported. + #[inline] + pub fn with_content_protection(mut self, protected: bool) -> WindowBuilder { + self.window.content_protection = protected; + self + } + + /// Sets whether the window should be visible on all workspaces. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Windows:** Unsupported. + #[inline] + pub fn with_visible_on_all_workspaces(mut self, visible: bool) -> WindowBuilder { + self.window.visible_on_all_workspaces = visible; + self + } + + /// Sets the window background color. + /// + /// ## Platform-specific: + /// + /// - **Windows:** alpha channel is ignored. Instead manually draw the window, for example using `softbuffer` crate, see + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn with_background_color(mut self, color: RGBA) -> WindowBuilder { + self.window.background_color = Some(color); + self + } + + /// Builds the window. + /// + /// Possible causes of error include denied permission, incompatible system, and lack of memory. + #[inline] + pub fn build( + self, + window_target: &EventLoopWindowTarget, + ) -> Result { + platform_impl::Window::new(&window_target.p, self.window, self.platform_specific).map( + |window| { + window.request_redraw(); + Window { window } + }, + ) + } +} + +/// Base Window functions. +impl Window { + /// Creates a new Window for platforms where this is appropriate. + /// + /// This function is equivalent to [`WindowBuilder::new().build(event_loop)`]. + /// + /// Error should be very rare and only occur in case of permission denied, incompatible system, + /// out of memory, etc. + /// + /// [`WindowBuilder::new().build(event_loop)`]: crate::window::WindowBuilder::build + #[inline] + pub fn new(event_loop: &EventLoopWindowTarget) -> Result { + let builder = WindowBuilder::new(); + builder.build(event_loop) + } + + /// Returns an identifier unique to the window. + #[inline] + pub fn id(&self) -> WindowId { + WindowId(self.window.id()) + } + + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. + /// + /// See the [`dpi`](crate::dpi) module for more information. + /// + /// Note that this value can change depending on user action (for example if the window is + /// moved to another screen); as such, tracking `WindowEvent::ScaleFactorChanged` events is + /// the most robust way to track the DPI you need to use to draw. + /// + /// ## Platform-specific + /// + /// - **Android:** Always returns 1.0. + /// - **iOS:** Can only be called on the main thread. Returns the underlying `UIView`'s + /// [`contentScaleFactor`]. + /// + /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc + #[inline] + pub fn scale_factor(&self) -> f64 { + self.window.scale_factor() + } + + /// Emits a `WindowEvent::RedrawRequested` event in the associated event loop after all OS + /// events have been processed by the event loop. + /// + /// This is the **strongly encouraged** method of redrawing windows, as it can integrate with + /// OS-requested redraws (e.g. when a window gets resized). + /// + /// This function can cause `RedrawRequested` events to be emitted after `Event::MainEventsCleared` + /// but before `Event::NewEvents` if called in the following circumstances: + /// * While processing `MainEventsCleared`. + /// * While processing a `RedrawRequested` event that was sent during `MainEventsCleared` or any + /// directly subsequent `RedrawRequested` event. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. + /// - **Android:** Unsupported. + #[inline] + pub fn request_redraw(&self) { + self.window.request_redraw() + } +} + +/// Position and size functions. +impl Window { + /// Returns the position of the top-left hand corner of the window's client area relative to the + /// top-left hand corner of the desktop. + /// + /// The same conditions that apply to `outer_position` apply to this method. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the + /// window's [safe area] in the screen space coordinate system. + /// - **Android:** Always returns [`NotSupportedError`]. + /// + /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + self.window.inner_position() + } + + /// Returns the position of the top-left hand corner of the window relative to the + /// top-left hand corner of the desktop. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as + /// the screen. If the user uses a desktop with multiple monitors, the top-left hand corner + /// of the desktop is the top-left hand corner of the monitor at the top-left of the desktop. + /// + /// The coordinates can be negative if the top-left hand corner of the window is outside + /// of the visible screen region. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the + /// window in the screen space coordinate system. + /// - **Android:** Always returns [`NotSupportedError`]. + /// - **Linux(Wayland)**: Has no effect, since Wayland doesn't support a global cordinate system + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + self.window.outer_position() + } + + /// Modifies the position of the window. + /// + /// See `outer_position` for more information about the coordinates. This automatically un-maximizes the + /// window if it's maximized. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Sets the top left coordinates of the + /// window in the screen space coordinate system. + /// - **Android / Linux(Wayland):** Unsupported. + #[inline] + pub fn set_outer_position>(&self, position: P) { + self.window.set_outer_position(position.into()) + } + + /// Returns the physical size of the window's client area. + /// + /// The client area is the content of the window, excluding the title bar and borders. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the `PhysicalSize` of the window's + /// [safe area] in screen space coordinates. + /// + /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + self.window.inner_size() + } + + /// Modifies the inner size of the window. + /// + /// See `inner_size` for more information about the values. This automatically un-maximizes the + /// window if it's maximized. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_inner_size>(&self, size: S) { + self.window.set_inner_size(size.into()) + } + + /// Returns the physical size of the entire window. + /// + /// These dimensions include the title bar and borders. If you don't want that (and you usually don't), + /// use `inner_size` instead. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the `PhysicalSize` of the window in + /// screen space coordinates. + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + self.window.outer_size() + } + + /// Sets a minimum dimension size for the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_min_inner_size>(&self, min_size: Option) { + self.window.set_min_inner_size(min_size.map(|s| s.into())) + } + + /// Sets a maximum dimension size for the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_max_inner_size>(&self, max_size: Option) { + self.window.set_max_inner_size(max_size.map(|s| s.into())) + } + + /// Sets inner size constraints for the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_inner_size_constraints(&self, constraints: WindowSizeConstraints) { + self.window.set_inner_size_constraints(constraints) + } +} + +/// Misc. attribute functions. +impl Window { + /// Modifies the title of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_title(&self, title: &str) { + self.window.set_title(title) + } + + /// Gets the current title of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. Returns ane empty string. + #[inline] + pub fn title(&self) -> String { + self.window.title() + } + + /// Modifies the window's visibility. + /// + /// If `false`, this will hide the window. If `true`, this will show the window. + /// ## Platform-specific + /// + /// - **Android:** Unsupported. + /// - **iOS:** Can only be called on the main thread. + #[inline] + pub fn set_visible(&self, visible: bool) { + self.window.set_visible(visible) + } + + /// Bring the window to front and focus. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_focus(&self) { + self.window.set_focus() + } + + /// Sets whether the window is focusable or not. + /// + /// ## Platform-specific + /// + /// - **macOS**: If the window is already focused, it is not possible to unfocus it after calling `set_focusable(false)`. + /// In this case, you might consider calling [`Window::set_focus`] but it will move the window to the back i.e. at the bottom in terms of z-order. + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_focusable(&self, focusable: bool) { + self.window.set_focusable(focusable) + } + + /// Is window active and focused? + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn is_focused(&self) -> bool { + self.window.is_focused() + } + + /// Indicates whether the window is always on top of other windows. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn is_always_on_top(&self) -> bool { + self.window.is_always_on_top() + } + + /// Sets whether the window is resizable or not. + /// + /// Note that making the window unresizable doesn't exempt you from handling `Resized`, as that event can still be + /// triggered by DPI scaling, entering fullscreen mode, etc. + /// + /// ## Platform-specific + /// + /// This only has an effect on desktop platforms. + /// + /// Due to a bug in XFCE, this has no effect on Xfwm. + /// + /// ## Platform-specific + /// + /// - **Linux:** Most size methods like maximized are async and do not work well with calling + /// sequentailly. For setting inner or outer size, you don't need to set resizable to true before + /// it. It can resize no matter what. But if you insist to do so, it has a `100, 100` minimum + /// limitation somehow. For maximizing, it requires resizable is true. If you really want to set + /// resizable to false after it. You might need a mechanism to check the window is really + /// maximized. + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_resizable(&self, resizable: bool) { + self.window.set_resizable(resizable) + } + + /// Sets whether the window is minimizable or not. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + #[inline] + pub fn set_minimizable(&self, minimizable: bool) { + self.window.set_minimizable(minimizable) + } + + /// Sets whether the window is maximizable or not. + /// + /// ## Platform-specific + /// + /// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. + /// - **Linux / iOS / Android:** Unsupported. + #[inline] + pub fn set_maximizable(&self, maximizable: bool) { + self.window.set_maximizable(maximizable) + } + + /// Sets whether the window is closable or not. + /// + /// ## Platform-specific + /// + /// - **Linux:** "GTK+ will do its best to convince the window manager not to show a close button. + /// Depending on the system, this function may not have any effect when called on a window that is already visible" + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_closable(&self, closable: bool) { + self.window.set_closable(closable) + } + + /// Sets the window to minimized or back + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_minimized(&self, minimized: bool) { + self.window.set_minimized(minimized); + } + + /// Sets the window to maximized or back. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_maximized(&self, maximized: bool) { + self.window.set_maximized(maximized) + } + + /// Gets the window's current maximized state. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn is_maximized(&self) -> bool { + self.window.is_maximized() + } + + /// Gets the window's current minimized state. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn is_minimized(&self) -> bool { + self.window.is_minimized() + } + + /// Gets the window's current visibility state. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn is_visible(&self) -> bool { + self.window.is_visible() + } + + /// Gets the window's current resizable state. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn is_resizable(&self) -> bool { + self.window.is_resizable() + } + + /// Gets the window's current minimizable state. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + #[inline] + pub fn is_minimizable(&self) -> bool { + self.window.is_minimizable() + } + + /// Gets the window's current maximizable state. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + #[inline] + pub fn is_maximizable(&self) -> bool { + self.window.is_maximizable() + } + + /// Gets the window's current closable state. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn is_closable(&self) -> bool { + self.window.is_closable() + } + + /// Gets the window's current decoration state. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + pub fn is_decorated(&self) -> bool { + self.window.is_decorated() + } + + /// Sets the window to fullscreen or back. + /// + /// ## Platform-specific + /// + /// - **macOS:** `Fullscreen::Exclusive` provides true exclusive mode with a + /// video mode change. *Caveat!* macOS doesn't provide task switching (or + /// spaces!) while in exclusive fullscreen mode. This mode should be used + /// when a video mode change is desired, but for a better user experience, + /// borderless fullscreen might be preferred. + /// + /// `Fullscreen::Borderless` provides a borderless fullscreen window on a + /// separate space. This is the idiomatic way for fullscreen games to work + /// on macOS. See `WindowExtMacOs::set_simple_fullscreen` if + /// separate spaces are not preferred. + /// + /// The dock and the menu bar are always disabled in fullscreen mode. + /// - **iOS:** Can only be called on the main thread. + /// - **Windows:** Screen saver is disabled in fullscreen mode. + /// - **Linux:** The window will only fullscreen to current monitor no matter which enum variant. + /// - **Android:** Unsupported. + #[inline] + pub fn set_fullscreen(&self, fullscreen: Option) { + self.window.set_fullscreen(fullscreen) + } + + /// Gets the window's current fullscreen state. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. + /// - **Android:** Will always return `None`. + #[inline] + pub fn fullscreen(&self) -> Option { + self.window.fullscreen() + } + + /// Turn window decorations on or off. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + /// + /// [`setPrefersStatusBarHidden`]: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc + #[inline] + pub fn set_decorations(&self, decorations: bool) { + self.window.set_decorations(decorations) + } + + /// Change whether or not the window will always be below other windows. + /// + /// ## Platform-specific + /// + /// - **Windows**: There is no guarantee that the window will be the bottom most but it will try to be. + /// - **Linux(x11):** Result depends on the system's window manager. Consider this setting a suggestion. + /// - **Linux(Wayland) / iOS / Android:** Unsupported. + // TODO: Unsupported in gtk4 + #[inline] + pub fn set_always_on_bottom(&self, always_on_bottom: bool) { + self.window.set_always_on_bottom(always_on_bottom) + } + + /// Change whether or not the window will always be on top of other windows. + /// + /// ## Platform-specific + /// + /// - **Linux(x11):** Result depends on the system's window manager. Consider this setting a suggestion. + /// - **Linux(Wayland) / iOS / Android:** Unsupported. + // TODO: Unsupported in gtk4 + #[inline] + pub fn set_always_on_top(&self, always_on_top: bool) { + self.window.set_always_on_top(always_on_top) + } + + /// Sets the window icon. On Windows and Linux, this is typically the small icon in the top-left + /// corner of the title bar. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / macOS:** Unsupported. + /// + /// On Windows, this sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's + /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. + #[inline] + pub fn set_window_icon(&self, window_icon: Option) { + self.window.set_window_icon(window_icon) + } + + /// Sets location of IME candidate box in client area coordinates relative to the top left. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_ime_position>(&self, position: P) { + self.window.set_ime_position(position.into()) + } + + /// Sets the taskbar progress state. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Unlike windows, progress bar is app-wide and not specific to this window. Only supported desktop environments with `libunity` (e.g. GNOME). + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_progress_bar(&self, _progress: ProgressBarState) { + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "macos", + ))] + self.window.set_progress_bar(_progress) + } + + /// Requests user attention to the window, this has no effect if the application + /// is already focused. How requesting for user attention manifests is platform dependent, + /// see `UserAttentionType` for details. + /// + /// Providing `None` will unset the request for user attention. Unsetting the request for + /// user attention might not be done automatically by the WM when the window receives input. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + /// - **macOS:** `None` has no effect. + /// - **Linux:** Urgency levels have the same effect. + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + self.window.request_user_attention(request_type) + } + + /// Returns the current window theme. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn theme(&self) -> Theme { + self.window.theme() + } + + /// Sets the theme for this window. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Theme is app-wide and not specific to this window. + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_theme(&self, #[allow(unused)] theme: Option) { + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "macos", + ))] + self.window.set_theme(theme) + } + + /// Prevents the window contents from being captured by other apps. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Linux:** Unsupported. + pub fn set_content_protection(&self, #[allow(unused)] enabled: bool) { + #[cfg(any(target_os = "macos", target_os = "windows"))] + self.window.set_content_protection(enabled); + } + + /// Sets whether the window should be visible on all workspaces. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Windows:** Unsupported. + pub fn set_visible_on_all_workspaces(&self, #[allow(unused)] visible: bool) { + #[cfg(any(target_os = "macos", target_os = "linux"))] + self.window.set_visible_on_all_workspaces(visible) + } + + /// Sets the window background color. + /// + /// ## Platform-specific: + /// + /// - **Windows:** alpha channel is ignored. Instead manually draw the window, for example using `softbuffer` crate, see + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_background_color(&self, color: Option) { + self.window.set_background_color(color) + } +} + +/// Cursor functions. +impl Window { + /// Modifies the cursor icon of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_cursor_icon(&self, cursor: CursorIcon) { + self.window.set_cursor_icon(cursor); + } + + /// Changes the position of the cursor in window coordinates. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn set_cursor_position>(&self, position: P) -> Result<(), ExternalError> { + self.window.set_cursor_position(position.into()) + } + + /// Grabs the cursor, preventing it from leaving the window. + /// + /// There's no guarantee that the cursor will be hidden. You should + /// hide it by yourself if you want so. + /// + /// ## Platform-specific + /// + /// - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. + /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { + self.window.set_cursor_grab(grab) + } + + /// Modifies the cursor's visibility. + /// + /// If `false`, this will hide the cursor. If `true`, this will show the cursor. + /// + /// ## Platform-specific + /// + /// - **Windows:** The cursor is only hidden within the confines of the window. + /// - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is + /// outside of the window. + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + self.window.set_cursor_visible(visible) + } + + /// Moves the window with the left mouse button until the button is released. + /// + /// There's no guarantee that this will work unless the left mouse button was pressed + /// immediately before this function is called. + /// + /// ## Platform-specific + /// + /// - **macOS:** May prevent the button release event to be triggered. + /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + self.window.drag_window() + } + + /// Resizes the window with the left mouse button until the button is released. + /// + /// There's no guarantee that this will work unless the left mouse button was pressed + /// immediately before this function is called. + /// + /// ## Platform-specific + /// + /// - **macOS / iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + self.window.drag_resize_window(direction) + } + + /// Modifies whether the window catches cursor events. + /// + /// If `true`, the events are passed through the window such that any other window behind it receives them. + /// If `false` the window will catch the cursor events. By default cursor events are not ignored. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`] + #[inline] + pub fn set_ignore_cursor_events(&self, ignore: bool) -> Result<(), ExternalError> { + self.window.set_ignore_cursor_events(ignore) + } + + /// Returns the current cursor position + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Linux(Wayland)**: Unsupported, returns `0,0`. + #[inline] + pub fn cursor_position(&self) -> Result, ExternalError> { + self.window.cursor_position() + } +} + +/// Monitor info functions. +impl Window { + /// Returns the monitor on which the window currently resides. + /// + /// Returns `None` if current monitor can't be detected. + /// + /// ## Platform-specific + /// + /// **iOS:** Can only be called on the main thread. + #[inline] + pub fn current_monitor(&self) -> Option { + self.window.current_monitor() + } + + #[inline] + /// Returns the monitor that contains the given point. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS:** Unsupported. + pub fn monitor_from_point(&self, x: f64, y: f64) -> Option { + self.window.monitor_from_point(x, y) + } + + /// Returns the list of all the monitors available on the system. + /// + /// This is the same as `EventLoopWindowTarget::available_monitors`, and is provided for convenience. + /// + /// ## Platform-specific + /// + /// **iOS:** Can only be called on the main thread. + #[inline] + pub fn available_monitors(&self) -> impl Iterator { + self + .window + .available_monitors() + .into_iter() + .map(|inner| MonitorHandle { inner }) + } + + /// Returns the primary monitor of the system. + /// + /// Returns `None` if it can't identify any monitor as a primary one. + /// + /// This is the same as `EventLoopWindowTarget::primary_monitor`, and is provided for convenience. + /// + /// ## Platform-specific + /// + /// **iOS:** Can only be called on the main thread. + #[inline] + pub fn primary_monitor(&self) -> Option { + self.window.primary_monitor() + } +} + +#[cfg(feature = "rwh_04")] +unsafe impl rwh_04::HasRawWindowHandle for Window { + fn raw_window_handle(&self) -> rwh_04::RawWindowHandle { + self.window.raw_window_handle_rwh_04() + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawWindowHandle for Window { + fn raw_window_handle(&self) -> rwh_05::RawWindowHandle { + self.window.raw_window_handle_rwh_05() + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for Window { + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + self.window.raw_display_handle_rwh_05() + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasWindowHandle for Window { + fn window_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.window.raw_window_handle_rwh_06()?; + // SAFETY: The window handle will never be deallocated while the window is alive. + Ok(unsafe { rwh_06::WindowHandle::borrow_raw(raw) }) + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for Window { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.window.raw_display_handle_rwh_06()?; + // SAFETY: The window handle will never be deallocated while the window is alive. + Ok(unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }) + } +} + +/// Describes the appearance of the mouse cursor. +#[non_exhaustive] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Default)] +pub enum CursorIcon { + /// The platform-dependent default cursor. + #[default] + Default, + /// A simple crosshair. + Crosshair, + /// A hand (often used to indicate links in web browsers). + Hand, + /// Self explanatory. + Arrow, + /// Indicates something is to be moved. + Move, + /// Indicates text that may be selected or edited. + Text, + /// Program busy indicator. + Wait, + /// Help indicator (often rendered as a "?") + Help, + /// Progress indicator. Shows that processing is being done. But in contrast + /// with "Wait" the user may still interact with the program. Often rendered + /// as a spinning beach ball, or an arrow with a watch or hourglass. + Progress, + + /// Cursor showing that something cannot be done. + NotAllowed, + ContextMenu, + Cell, + VerticalText, + Alias, + Copy, + NoDrop, + /// Indicates something can be grabbed. + Grab, + /// Indicates something is grabbed. + Grabbing, + AllScroll, + ZoomIn, + ZoomOut, + + /// Indicate that some edge is to be moved. For example, the 'SeResize' cursor + /// is used when the movement starts from the south-east corner of the box. + EResize, + NResize, + NeResize, + NwResize, + SResize, + SeResize, + SwResize, + WResize, + EwResize, + NsResize, + NeswResize, + NwseResize, + ColResize, + RowResize, +} + +/// Fullscreen modes. +#[non_exhaustive] +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug, PartialEq)] +pub enum Fullscreen { + Exclusive(VideoMode), + + /// Providing `None` to `Borderless` will fullscreen on the current monitor. + Borderless(Option), +} + +#[non_exhaustive] +#[derive(Default, Clone, Copy, Debug, PartialEq)] +pub enum Theme { + #[default] + Light, + Dark, +} + +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum UserAttentionType { + /// ## Platform-specific + /// - **macOS:** Bounces the dock icon until the application is in focus. + /// - **Windows:** Flashes both the window and the taskbar button until the application is in focus. + Critical, + /// ## Platform-specific + /// - **macOS:** Bounces the dock icon once. + /// - **Windows:** Flashes the taskbar button until the application is in focus. + #[default] + Informational, +} + +/// Window size constraints +#[derive(Clone, Copy, PartialEq, Debug, Default)] +pub struct WindowSizeConstraints { + /// The minimum width a window can be, If this is `None`, the window will have no minimum width (aside from reserved). + /// + /// The default is `None`. + pub min_width: Option, + /// The minimum height a window can be, If this is `None`, the window will have no minimum height (aside from reserved). + /// + /// The default is `None`. + pub min_height: Option, + /// The maximum width a window can be, If this is `None`, the window will have no maximum width (aside from reserved). + /// + /// The default is `None`. + pub max_width: Option, + /// The maximum height a window can be, If this is `None`, the window will have no maximum height (aside from reserved). + /// + /// The default is `None`. + pub max_height: Option, +} + +impl WindowSizeConstraints { + pub fn new( + min_width: Option, + min_height: Option, + max_width: Option, + max_height: Option, + ) -> Self { + Self { + min_width, + min_height, + max_width, + max_height, + } + } + + /// Returns true if `min_width` or `min_height` is set. + pub fn has_min(&self) -> bool { + self.min_width.is_some() || self.min_height.is_some() + } + /// Returns true if `max_width` or `max_height` is set. + pub fn has_max(&self) -> bool { + self.max_width.is_some() || self.max_height.is_some() + } + + /// Returns a physical size that represents the minimum constraints set and fallbacks to [`PixelUnit::MIN`] for unset values + pub fn min_size_physical(&self, scale_factor: f64) -> PhysicalSize { + PhysicalSize::new( + self + .min_width + .unwrap_or(PixelUnit::MIN) + .to_physical(scale_factor) + .0, + self + .min_height + .unwrap_or(PixelUnit::MIN) + .to_physical(scale_factor) + .0, + ) + } + + /// Returns a logical size that represents the minimum constraints set and fallbacks to [`PixelUnit::MIN`] for unset values + pub fn min_size_logical(&self, scale_factor: f64) -> LogicalSize { + LogicalSize::new( + self + .min_width + .unwrap_or(PixelUnit::MIN) + .to_logical(scale_factor) + .0, + self + .min_height + .unwrap_or(PixelUnit::MIN) + .to_logical(scale_factor) + .0, + ) + } + + /// Returns a physical size that represents the maximum constraints set and fallbacks to [`PixelUnit::MAX`] for unset values + pub fn max_size_physical(&self, scale_factor: f64) -> PhysicalSize { + PhysicalSize::new( + self + .max_width + .unwrap_or(PixelUnit::MAX) + .to_physical(scale_factor) + .0, + self + .max_height + .unwrap_or(PixelUnit::MAX) + .to_physical(scale_factor) + .0, + ) + } + + /// Returns a logical size that represents the maximum constraints set and fallbacks to [`PixelUnit::MAX`] for unset values + pub fn max_size_logical(&self, scale_factor: f64) -> LogicalSize { + LogicalSize::new( + self + .max_width + .unwrap_or(PixelUnit::MAX) + .to_logical(scale_factor) + .0, + self + .max_height + .unwrap_or(PixelUnit::MAX) + .to_logical(scale_factor) + .0, + ) + } + + /// Clamps the desired size based on the constraints set + pub fn clamp(&self, desired_size: Size, scale_factor: f64) -> Size { + let min_size: PhysicalSize = self.min_size_physical(scale_factor); + let max_size: PhysicalSize = self.max_size_physical(scale_factor); + Size::clamp(desired_size, min_size.into(), max_size.into(), scale_factor) + } +} + +/// Defines the orientation that a window resize will be performed. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum ResizeDirection { + East, + North, + NorthEast, + NorthWest, + South, + SouthEast, + SouthWest, + West, +} + +pub(crate) fn hit_test( + (left, top, right, bottom): (i32, i32, i32, i32), + cx: i32, + cy: i32, + border_x: i32, + border_y: i32, +) -> Option { + const LEFT: isize = 0b0001; + const RIGHT: isize = 0b0010; + const TOP: isize = 0b0100; + const BOTTOM: isize = 0b1000; + const TOPLEFT: isize = TOP | LEFT; + const TOPRIGHT: isize = TOP | RIGHT; + const BOTTOMLEFT: isize = BOTTOM | LEFT; + const BOTTOMRIGHT: isize = BOTTOM | RIGHT; + + #[rustfmt::skip] + let result = (LEFT * (cx < left + border_x) as isize) + | (RIGHT * (cx >= right - border_x) as isize) + | (TOP * (cy < top + border_y) as isize) + | (BOTTOM * (cy >= bottom - border_y) as isize); + + match result { + LEFT => Some(ResizeDirection::West), + RIGHT => Some(ResizeDirection::East), + TOP => Some(ResizeDirection::North), + BOTTOM => Some(ResizeDirection::South), + TOPLEFT => Some(ResizeDirection::NorthWest), + TOPRIGHT => Some(ResizeDirection::NorthEast), + BOTTOMLEFT => Some(ResizeDirection::SouthWest), + BOTTOMRIGHT => Some(ResizeDirection::SouthEast), + _ => None, + } +}