diff --git a/Cargo.toml b/Cargo.toml index faccd6c..681e6df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,5 +150,16 @@ name = "chd_extract" path = "src/bin/chd_extract.rs" required-features = ["chd"] +# Mac App Store fix (guideline 2.5.1): eframe's winit (0.30) calls the private +# SkyLight API `CGSSetWindowBackgroundBlurRadius`, whose symbol Apple's binary +# scan rejects. Our vendored copy stubs WindowDelegate::set_blur to a no-op and +# drops the private extern declarations; iris-gui never requests window blur, +# so behaviour is unchanged. Only the 0.30.x requirement (eframe → egui-winit → +# glutin-winit) matches this patch; iris's own winit 0.29 dependency is the +# keyboard-KeyCode type only and creates no window in iris-gui, so its blur code +# is dead-stripped. See rules/macos/appstore-private-api.md. +[patch.crates-io] +winit = { path = "third_party/winit-0.30.13" } + diff --git a/docs/appstore-review-response.md b/docs/appstore-review-response.md new file mode 100644 index 0000000..6cd9444 --- /dev/null +++ b/docs/appstore-review-response.md @@ -0,0 +1,108 @@ +# App Store review response — IRIS (Submission 2ed07ab1…) + +Covers the two issues raised on the 1.0 (20260610.2118) review (June 16, 2026): + +1. **Guideline 2.5.1** — private API `_CGSSetWindowBackgroundBlurRadius`. +2. **Guideline 2.4.5(i)** — entitlements without obvious matching functionality + (`com.apple.security.device.camera`, `com.apple.security.network.server`). + +--- + +## 1. Guideline 2.5.1 — private API (fixed in binary) + +The symbol came from the `winit` windowing crate (pulled in by `eframe`), whose +macOS backend calls `CGSSetWindowBackgroundBlurRadius` in `WindowDelegate::set_blur`. +IRIS never requests window blur, but the symbol is linked in regardless and +Apple's static scan flags it. + +**Fix:** vendored a patched `winit` (`third_party/winit-0.30.13/`, wired via +`[patch.crates-io]` in the root `Cargo.toml`) that removes the private extern +declarations and makes `set_blur` a no-op. Verified the symbol is gone from the +linked binary: + +``` +nm -u target/release/iris-gui | grep CGSSetWindowBackgroundBlurRadius # → no output +``` + +(`_CGShieldingWindowLevel` remains; it is a public CoreGraphics API and was not +flagged.) See `rules/macos/appstore-private-api.md`. + +A new binary is required for this fix, so we also strengthened the two +entitlements below with visible, testable functionality rather than removing +them. + +--- + +## 2. Guideline 2.4.5(i) — entitlement justifications + +Both entitlements back real functionality. To make them easy to verify we added +in-app features that exercise each one directly, without needing to boot IRIX. + +### `com.apple.security.device.camera` + +**What it's for:** IRIS emulates the SGI Indy's **IndyCam** / VINO video-input +hardware. When the user selects the host camera as the video source, IRIS +captures live frames from the Mac's camera (AVFoundation) and feeds them to the +emulated VINO device. The matching `NSCameraUsageDescription` is in `Info.plist`. + +**How to test (reviewer steps):** +1. Launch IRIS. In the launcher, open the **Video-In** tab. +2. Click **📷 Test Camera**. +3. macOS shows the camera-permission prompt; allow it. +4. A live preview from the Mac camera appears, with a status line showing the + capture resolution and a rising frame count. Closing the window releases the + camera (indicator light turns off). + +> Paste-ready reply: +> +> IRIS emulates the SGI Indy IndyCam (VINO video-input) hardware. The camera +> entitlement lets the app capture live video from the Mac's camera and feed it +> to the emulated video-input device. You can verify this directly: open the +> **Video-In** tab and click **Test Camera** — macOS will prompt for camera +> access and the app then shows a live preview from the camera. The matching +> NSCameraUsageDescription is included in Info.plist. + +### `com.apple.security.network.server` + +**What it's for:** two things — +- The emulator exposes the emulated machine's **serial console** (IRIX ttyd1, + `127.0.0.1:8881`) and **PROM monitor** on loopback TCP, so a terminal can + attach to the guest console. The app's own **Serial console…** window connects + to this server (loopback), which is the visible end-to-end demonstration: the + emulator *listens* (network.server) and the viewer *connects* (network.client). +- It binds **inbound port-forwards** into the emulated SGI Ethernet (SEEQ 8003) + when the user configures them on the Networking tab. + +(The clean-shutdown "Send IRIX halt" action no longer uses a socket — it now +types at the console in-process — so the server entitlement is only used for the +genuine server features above.) + +**How to test (reviewer steps):** +1. Launch IRIS and **Start** a machine (the bundled config boots to the PROM). +2. Open **Machine → Serial console…**. +3. The window shows "● connected to 127.0.0.1:8881" and streams the live guest + serial console. Typing a line and pressing Enter sends it to the guest. + This confirms the app's loopback serial **server** is live and accepting a + connection. + +> Paste-ready reply: +> +> IRIS exposes the emulated workstation's serial console and PROM monitor as +> loopback TCP servers (e.g. 127.0.0.1:8881) so a terminal can attach to the +> guest console, and it binds user-configured inbound port-forwards into the +> emulated Ethernet. You can verify this without external tools: Start a machine +> and open **Machine → Serial console…** — the app connects to its own loopback +> serial server and streams the live guest console, and you can type into it. + +--- + +## Summary of binary changes in this resubmission + +- Vendored/patched `winit` to drop the private blur API (2.5.1). No window blur + was ever used. +- Added **Video-In → Test Camera** (live host-camera preview) so the camera + entitlement is user-visible. +- Added **Machine → Serial console…** (in-app loopback serial viewer) so the + network.server entitlement is user-visible. +- Moved "Send IRIX halt" to an in-process console path (no longer opens a + loopback socket). diff --git a/installer/iris-gui.entitlements b/installer/iris-gui.entitlements index 7631742..71203c7 100644 --- a/installer/iris-gui.entitlements +++ b/installer/iris-gui.entitlements @@ -19,7 +19,11 @@ com.apple.security.cs.allow-jit - + com.apple.security.device.camera @@ -34,9 +38,15 @@ com.apple.security.files.bookmarks.app-scope - + com.apple.security.network.client + com.apple.security.network.server diff --git a/iris-gui/assets/nvram-default.bin b/iris-gui/assets/nvram-default.bin new file mode 100644 index 0000000..264e632 Binary files /dev/null and b/iris-gui/assets/nvram-default.bin differ diff --git a/iris-gui/src/camera_test.rs b/iris-gui/src/camera_test.rs new file mode 100644 index 0000000..7843072 --- /dev/null +++ b/iris-gui/src/camera_test.rs @@ -0,0 +1,150 @@ +//! "Test Camera" support. +//! +//! Opens the host camera through the same [`iris::camera::CameraSource`] the +//! VINO / IndyCam emulation uses, on a background thread, and parks the latest +//! frame (converted to RGBA) plus a status line for the GUI to display. This +//! gives the user — and an App Review tester — a way to confirm the host-camera +//! capability works (it triggers the macOS camera-permission prompt and shows a +//! live preview) without booting IRIX and configuring the VINO video source. +//! +//! Dropping `CameraTest` stops the worker, which drops the `CameraSource` and +//! releases the camera (indicator light off). + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::JoinHandle; + +use parking_lot::Mutex; + +use iris::camera::CameraSource; +use iris::video_source::{Field, VideoSource, VideoStandard}; + +#[derive(Default)] +struct Shared { + /// One-line capture status (frame count, capture resolution, …). + status: String, + /// Set if the camera could not be opened (permission denied / no device). + error: Option, + /// Latest preview frame: (width, height, RGBA bytes). + frame: Option<(u32, u32, Vec)>, + /// Bumped on each new frame so the GUI can skip redundant texture uploads. + seq: u64, +} + +pub struct CameraTest { + shared: Arc>, + running: Arc, + worker: Option>, +} + +impl CameraTest { + /// Start capturing from host camera `index` using `standard`'s field size. + pub fn start(standard: VideoStandard, index: u32) -> Self { + let shared = Arc::new(Mutex::new(Shared { + status: "opening camera…".into(), + ..Shared::default() + })); + let running = Arc::new(AtomicBool::new(true)); + let s2 = shared.clone(); + let r2 = running.clone(); + + let worker = std::thread::Builder::new() + .name("iris-gui-camtest".into()) + .spawn(move || { + let cam = match CameraSource::new_with_index(standard, index) { + Ok(c) => c, + Err(e) => { + s2.lock().error = Some(e); + return; + } + }; + // next_field() paces itself to the field rate, so this loop + // runs at ~50–60 Hz without a manual sleep. + while r2.load(Ordering::Relaxed) { + let field = cam.next_field(); + let rgba = uyvy_field_to_rgba(&field); + let status = cam.status(); + let mut g = s2.lock(); + g.status = status; + g.frame = Some((field.width, field.height, rgba)); + g.seq = g.seq.wrapping_add(1); + g.error = None; + } + // `cam` drops here → camera stream closed, device released. + }) + .expect("spawn camera-test worker"); + + Self { shared, running, worker: Some(worker) } + } + + pub fn status(&self) -> String { + self.shared.lock().status.clone() + } + + pub fn error(&self) -> Option { + self.shared.lock().error.clone() + } + + /// Return the latest frame if it is newer than `last_seq` (which is then + /// advanced). `None` when there is nothing new to upload. + pub fn take_new_frame(&self, last_seq: &mut u64) -> Option<(u32, u32, Vec)> { + let g = self.shared.lock(); + if g.seq != *last_seq { + *last_seq = g.seq; + g.frame.clone() + } else { + None + } + } +} + +impl Drop for CameraTest { + fn drop(&mut self) { + self.running.store(false, Ordering::Relaxed); + if let Some(w) = self.worker.take() { + let _ = w.join(); + } + } +} + +/// Convert one packed UYVY 4:2:2 field to RGBA8 (BT.601 limited range). +/// Each 4-byte group `U Y0 V Y1` yields two pixels sharing the U/V chroma. +fn uyvy_field_to_rgba(field: &Field) -> Vec { + let w = field.width as usize; + let h = field.height as usize; + let src = &field.pixels; + let mut out = vec![0u8; w * h * 4]; + + for y in 0..h { + let row = y * w * 2; + for pair in 0..(w / 2) { + let i = row + pair * 4; + if i + 3 >= src.len() { + break; + } + let u = src[i] as i32; + let y0 = src[i + 1] as i32; + let v = src[i + 2] as i32; + let y1 = src[i + 3] as i32; + + let o = (y * w + pair * 2) * 4; + yuv_to_rgba(y0, u, v, &mut out[o..o + 4]); + yuv_to_rgba(y1, u, v, &mut out[o + 4..o + 8]); + } + } + out +} + +#[inline] +fn yuv_to_rgba(y: i32, u: i32, v: i32, out: &mut [u8]) { + let c = y - 16; + let d = u - 128; + let e = v - 128; + let r = (298 * c + 409 * e + 128) >> 8; + let g = (298 * c - 100 * d - 208 * e + 128) >> 8; + let b = (298 * c + 516 * d + 128) >> 8; + out[0] = r.clamp(0, 255) as u8; + out[1] = g.clamp(0, 255) as u8; + out[2] = b.clamp(0, 255) as u8; + out[3] = 255; +} diff --git a/iris-gui/src/config_ui.rs b/iris-gui/src/config_ui.rs index 5f057d2..acfbbeb 100644 --- a/iris-gui/src/config_ui.rs +++ b/iris-gui/src/config_ui.rs @@ -93,6 +93,10 @@ pub enum ConfigAction { /// and, if accepted, clear `cfg.prom` (an empty path falls back to the /// built-in PROM in `iris::prom::Prom::from_file_or_embedded`). RequestEmbeddedProm, + /// User clicked "Test Camera" on the Video-In tab; the app should open the + /// host camera and show a live preview (using the current `[vino]` standard + /// and camera index). + TestCamera, } pub fn show_tab(ui: &mut Ui, tab: Tab, cfg: &mut MachineConfig, jit: &mut JitEnv) -> ConfigAction { @@ -102,7 +106,7 @@ pub fn show_tab(ui: &mut Ui, tab: Tab, cfg: &mut MachineConfig, jit: &mut JitEnv Tab::Network => { show_network(ui, cfg); ConfigAction::None } Tab::Memory => { show_memory(ui, cfg); ConfigAction::None } Tab::Display => { show_display(ui, cfg); ConfigAction::None } - Tab::VideoIn => { show_vino(ui, cfg); ConfigAction::None } + Tab::VideoIn => show_vino(ui, cfg), Tab::Debug => { show_debug(ui, cfg, jit); ConfigAction::None } Tab::Ci => { show_ci(ui, cfg); ConfigAction::None } }).inner @@ -370,7 +374,8 @@ fn show_network(ui: &mut Ui, cfg: &mut MachineConfig) { } } -fn show_vino(ui: &mut Ui, cfg: &mut MachineConfig) { +fn show_vino(ui: &mut Ui, cfg: &mut MachineConfig) -> ConfigAction { + let mut action = ConfigAction::None; ui.heading("Video-In (IndyCam)"); Grid::new("vino_grid").num_columns(2).striped(true).show(ui, |ui| { ui.label("Source"); @@ -407,6 +412,42 @@ fn show_vino(ui: &mut Ui, cfg: &mut MachineConfig) { ui.add(DragValue::new(&mut cfg.vino.camera_index).range(0..=15)); ui.end_row(); }); + + // Live host-camera test: opens the selected camera directly (no IRIX boot + // needed) so the user can confirm the capture path works — and, on macOS, + // grant the camera permission. This exercises the same host-capture code + // the VINO/IndyCam source uses. + ui.add_space(8.0); + if build_features::CAMERA { + ui.horizontal(|ui| { + if ui.button("📷 Test Camera").clicked() { + action = ConfigAction::TestCamera; + } + ui.label( + RichText::new(format!( + "Preview host camera #{} live ({}).", + cfg.vino.camera_index, + match cfg.vino.standard { VinoStandard::Ntsc => "NTSC", VinoStandard::Pal => "PAL" }, + )) + .weak(), + ); + }); + ui.label( + RichText::new( + "On first use macOS will ask for camera permission. The camera \ + is released when you close the preview.", + ) + .weak() + .small(), + ); + } else { + ui.label( + RichText::new("Camera test unavailable — this build was compiled without --features camera.") + .weak(), + ); + } + + action } fn show_debug(ui: &mut Ui, cfg: &mut MachineConfig, jit: &mut JitEnv) { diff --git a/iris-gui/src/dialogs/create_disk.rs b/iris-gui/src/dialogs/create_disk.rs index 0f02c53..c8cd272 100644 --- a/iris-gui/src/dialogs/create_disk.rs +++ b/iris-gui/src/dialogs/create_disk.rs @@ -2,7 +2,6 @@ use eframe::egui::{self, Color32, Grid, RichText, Slider, TextEdit}; use std::path::PathBuf; /// Modal that creates a blank zero-filled disk image for a chosen SCSI ID. -/// Mirrors snow's DiskImageDialog. pub struct CreateDiskDialog { open: bool, scsi_id: u8, @@ -25,7 +24,9 @@ impl Default for CreateDiskDialog { impl CreateDiskDialog { pub fn open_for(&mut self, scsi_id: u8) { self.scsi_id = scsi_id; - self.filename = format!("scsi{scsi_id}.raw"); + // Absolute, app-managed default location (writable in the App Store + // sandbox too) so a new disk never lands in the working dir. + self.filename = crate::settings::GuiSettings::default_disk_path(scsi_id); self.size_mb = 1024.0; self.result = None; self.open = true; @@ -46,11 +47,16 @@ impl CreateDiskDialog { ui.horizontal(|ui| { ui.add(TextEdit::singleline(&mut self.filename).desired_width(220.0)); if ui.button("📁").clicked() { - if let Some(p) = rfd::FileDialog::new() - .add_filter("Disk image", &["raw", "img"]) - .set_file_name(&self.filename) - .save_file() - { + let cur = std::path::Path::new(&self.filename); + let mut dlg = rfd::FileDialog::new().add_filter("Disk image", &["raw", "img"]); + if let Some(dir) = cur.parent().filter(|d| !d.as_os_str().is_empty()) { + let _ = std::fs::create_dir_all(dir); + dlg = dlg.set_directory(dir); + } + if let Some(name) = cur.file_name().and_then(|s| s.to_str()) { + dlg = dlg.set_file_name(name); + } + if let Some(p) = dlg.save_file() { self.filename = p.to_string_lossy().into_owned(); } } @@ -70,8 +76,12 @@ impl CreateDiskDialog { if ui.add(egui::Button::new("Create") .fill(Color32::from_rgb(60, 110, 60))).clicked() { - // Create file on disk now. + // Create file on disk now (making the parent dir first, + // e.g. the managed /disks on first use). let path = PathBuf::from(&self.filename); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } let size_bytes = (self.size_mb * 1024.0 * 1024.0) as u64; match std::fs::File::create(&path) .and_then(|f| f.set_len(size_bytes)) diff --git a/iris-gui/src/dialogs/new_machine.rs b/iris-gui/src/dialogs/new_machine.rs index 0462f54..d50fd28 100644 --- a/iris-gui/src/dialogs/new_machine.rs +++ b/iris-gui/src/dialogs/new_machine.rs @@ -1,7 +1,7 @@ use eframe::egui::{self, Color32, ComboBox, Grid, RichText, TextEdit}; use iris::config::{MachineConfig, ScsiDeviceConfig, VALID_BANK_SIZES}; -/// "New machine" startup dialog — analogous to snow's ModelSelectionDialog. +/// "New machine" startup dialog. /// Pops up at first run (or on `File → New machine…`) to bootstrap a config. pub struct NewMachineDialog { open: bool, @@ -32,11 +32,11 @@ impl Default for NewMachineDialog { name: "indy".into(), prom_path: "prom.bin".into(), use_embedded_prom: true, - nvram_path: "nvram.bin".into(), + nvram_path: crate::settings::GuiSettings::default_nvram_path(), ram_total_mb: 256, ram_advanced: false, ram_banks: [128, 128, 0, 0], - scsi1_path: "scsi1.raw".into(), + scsi1_path: crate::settings::GuiSettings::default_disk_path(1), create_blank_scsi1: false, cdrom4_path: String::new(), attach_cdrom: false, diff --git a/iris-gui/src/framebuffer.rs b/iris-gui/src/framebuffer.rs index 02edaf8..522af68 100644 --- a/iris-gui/src/framebuffer.rs +++ b/iris-gui/src/framebuffer.rs @@ -44,6 +44,14 @@ impl FrameSink { /// copying the whole buffer on every repaint when nothing is new. pub fn snapshot(&self) -> Frame { self.frame.lock().clone() } + /// Reset to the "no frame yet" state (seq 0, blank frame). Call before a + /// fresh run starts rendering so a restart shows the "waiting for first + /// frame" placeholder instead of the previous run's last frame. + pub fn reset(&self) { + *self.frame.lock() = Frame::default(); + self.seq.store(0, Ordering::Release); + } + fn lock(&self) -> MutexGuard<'_, Frame> { self.frame.lock() } } diff --git a/iris-gui/src/handle.rs b/iris-gui/src/handle.rs index 5313edc..e9c696b 100644 --- a/iris-gui/src/handle.rs +++ b/iris-gui/src/handle.rs @@ -12,6 +12,9 @@ use std::thread::JoinHandle; pub enum Cmd { Start(Box), Stop, + /// Type `halt\n` at the IRIX serial console in-process (no loopback socket) + /// for a clean guest shutdown. + HaltIrix, SaveState(String), RestoreState(String), Screenshot(PathBuf), @@ -45,6 +48,10 @@ pub struct Status { pub dirty_cow: usize, /// Approximate instructions/sec (millions). pub mips: f32, + /// The CPU is not executing: either stopped (soft power-off) or idle at the + /// PROM after an IRIX `halt` (0 MIPS). When set, the guest has shut down and + /// stopping the machine can't corrupt a disk — see [`crate::safe_stop`]. + pub cpu_halted: bool, } pub struct EmulatorHandle { @@ -105,6 +112,7 @@ impl EmulatorHandle { // events, so merge rather than replace to avoid clobbering them. self.status.mips = s.mips; self.status.dirty_cow = s.dirty_cow; + self.status.cpu_halted = s.cpu_halted; } match &evt { Evt::Started => self.status.running = true, @@ -174,7 +182,12 @@ fn worker_loop( let mips = (dc as f64 / dt / 1_000_000.0 * 10.0).round() as f32 / 10.0; prev_cycles = cur; prev_tick = now; - let _ = evt_tx.send(Evt::Status(Status { mips, ..Status::default() })); + // The guest has shut down when the CPU thread has stopped + // (soft power-off calls Machine::stop) or has retired no + // instructions this window (halted/idle at the PROM, 0 MIPS). + let cpu_stopped = machine.as_ref().map_or(true, |m| !m.cpu_is_running()); + let cpu_halted = cpu_stopped || mips == 0.0; + let _ = evt_tx.send(Evt::Status(Status { mips, cpu_halted, ..Status::default() })); } } continue; @@ -184,6 +197,10 @@ fn worker_loop( let _ = evt_tx.send(Evt::Error("emulator already running".into())); continue; } + // Clear the previous run's last frame so the restarted machine + // shows the "waiting for first REX3 frame" placeholder instead + // of the stale screen until its first frame is rendered. + frame_sink.reset(); // Wrap construction in catch_unwind: Machine::new and // friends may panic on missing files, bad images, etc. // We surface those as Evt::Error toasts instead of @@ -228,6 +245,12 @@ fn worker_loop( } } } + Ok(Cmd::HaltIrix) => { + match machine.as_ref() { + Some(m) => m.inject_serial_console(b"halt\n"), + None => { let _ = evt_tx.send(Evt::Error("halt: not running".into())); } + } + } Ok(Cmd::Stop) => { if let Some(m) = machine.take() { *ps2_slot.lock() = None; diff --git a/iris-gui/src/input.rs b/iris-gui/src/input.rs index e7fe4c1..5ed314a 100644 --- a/iris-gui/src/input.rs +++ b/iris-gui/src/input.rs @@ -18,11 +18,13 @@ //! captured we forward nothing, so menu clicks and typing into the config //! side panel stay with egui. //! -//! The framebuffer panel calls `pump(...)` each frame with the rect the REX3 -//! image occupies in screen space (used only to decide where a capturing -//! click counts). +//! The framebuffer panel calls `pump(...)` each frame, passing whether the REX3 +//! image widget itself was clicked this frame. Using the widget's own +//! `Response::clicked()` (rather than a raw point-in-rect test) means egui's +//! hit-testing already routed clicks on open menus / popups to those widgets, +//! so navigating menus over the display never gets "eaten" into a capture. -use egui::{CursorGrab, Event, Key, Modifiers, MouseWheelUnit, PointerButton, Rect, ViewportCommand}; +use egui::{CursorGrab, Event, Key, Modifiers, MouseWheelUnit, PointerButton, ViewportCommand}; use iris::ps2::Ps2Controller; use winit::keyboard::KeyCode; @@ -39,7 +41,7 @@ impl Default for InputState { } } -pub fn pump(ctx: &egui::Context, fb_rect: Rect, ps2: &Ps2Controller, state: &mut InputState, scroll_pixels_per_line: f64) { +pub fn pump(ctx: &egui::Context, fb_clicked: bool, ps2: &Ps2Controller, state: &mut InputState, scroll_pixels_per_line: f64) { // Collect everything we need inside the input borrow, then act afterwards // (sending viewport commands / PS2 writes outside the `input()` closure). let mut want_enter = false; @@ -53,14 +55,11 @@ pub fn pump(ctx: &egui::Context, fb_rect: Rect, ps2: &Ps2Controller, state: &mut ctx.input(|i| { if !state.captured { - // Not captured: the only thing we care about is a primary click - // inside the framebuffer, which grabs input. Everything else is - // left to egui (menus, config side panel, …). - if i.pointer.button_pressed(PointerButton::Primary) { - if let Some(p) = i.pointer.interact_pos().or_else(|| i.pointer.latest_pos()) { - if fb_rect.contains(p) { want_enter = true; } - } - } + // Not captured: capture only when the framebuffer Image widget + // itself was clicked. egui routes clicks on menus/popups/panels to + // those widgets first, so this never fires for menu navigation that + // happens to overlap the display. Everything else stays with egui. + if fb_clicked { want_enter = true; } return; } @@ -250,6 +249,11 @@ fn map_key(k: Key) -> Option { Key::OpenBracket => KeyCode::BracketLeft, Key::CloseBracket => KeyCode::BracketRight, Key::Backtick => KeyCode::Backquote, + // egui reports the *shifted* symbol as its own Key; these two share a + // physical key with Backslash/Slash (Shift is sent separately, so the + // guest forms '|' and '?'). Without them those keys send nothing. + Key::Pipe => KeyCode::Backslash, + Key::Questionmark => KeyCode::Slash, // F-keys (egui has no F5; iris likely doesn't need F13+ either) Key::F1 => KeyCode::F1, Key::F2 => KeyCode::F2, Key::F3 => KeyCode::F3, Key::F4 => KeyCode::F4, Key::F6 => KeyCode::F6, Key::F7 => KeyCode::F7, diff --git a/iris-gui/src/main.rs b/iris-gui/src/main.rs index 7b2ef48..0524748 100644 --- a/iris-gui/src/main.rs +++ b/iris-gui/src/main.rs @@ -1,5 +1,6 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +mod camera_test; mod config_ui; mod dialogs; mod framebuffer; @@ -8,6 +9,7 @@ mod input; mod macos_sandbox; mod safe_stop; mod scsi_menu; +mod serial_console; mod settings; mod single_instance; @@ -19,7 +21,7 @@ use egui::{Color32, RichText, ViewportCommand}; use handle::{Cmd, EmulatorHandle, Evt}; use iris::config::MachineConfig; use safe_stop::{evaluate, reason_lines}; -use settings::{GuiSettings, UI_SCALE_MAX, UI_SCALE_MIN}; +use settings::{GuiSettings, UI_SCALE_MAX, UI_SCALE_MIN, WINDOW_DEFAULT_SIZE}; use std::path::PathBuf; /// Decode the bundled window/taskbar icon (256×256 RGBA PNG, generated by @@ -40,6 +42,38 @@ fn load_icon() -> egui::IconData { } } +/// Largest aspect-preserving size (egui points) that fits `avail` for a +/// framebuffer whose native pixel dimensions are `px`. +fn fb_fit_size(avail: egui::Vec2, px: egui::Vec2) -> egui::Vec2 { + let fb_aspect = px.x / px.y; + if avail.x / avail.y > fb_aspect { + egui::vec2(avail.y * fb_aspect, avail.y) + } else { + egui::vec2(avail.x, avail.x / fb_aspect) + } +} + +/// Resolve a config path to an absolute string for display — a relative path is +/// joined to the process working directory (where the emulator actually looks +/// for it), so the user never sees a bare `scsi3.raw` with no idea where it is. +fn abs_path(p: &str) -> String { + let path = std::path::Path::new(p); + if path.is_absolute() || p.is_empty() { + return p.to_string(); + } + std::env::current_dir() + .map(|d| d.join(path).to_string_lossy().into_owned()) + .unwrap_or_else(|_| p.to_string()) +} + +/// True when `scale` (device pixels per emulated pixel) is close enough to a +/// positive integer that nearest-neighbour sampling stays pixel-perfect. Off +/// an integer, bilinear filtering avoids uneven pixel doubling. +fn is_integer_scale(scale: f32) -> bool { + let rounded = scale.round(); + rounded >= 1.0 && (scale - rounded).abs() <= 0.01 +} + fn main() -> eframe::Result<()> { env_logger::init(); // Prevent the iris lib from calling process::exit on guest soft-power-off @@ -73,17 +107,25 @@ fn main() -> eframe::Result<()> { // once, before any worker thread (CPU / REX3) can read it. #[cfg(feature = "appstore")] std::env::set_var("IRIS_NO_JIT", "1"); - let mut viewport = egui::ViewportBuilder::default() + let viewport = egui::ViewportBuilder::default() .with_title("iris — SGI Indy emulator") // app_id sets the X11 WM_CLASS / Wayland app_id so the compositor can // match an installed .desktop/icon (icons regenerated by // scripts/generate-icon.sh from iris-gui/assets/icon-original.png). .with_app_id("iris-gui") .with_icon(load_icon()) - .with_inner_size(prefs.window_size.unwrap_or([1100.0, 720.0])); - if prefs.fullscreen { - viewport = viewport.with_fullscreen(true); - } + // Open large enough for the 1280×1024 display + chrome so it looks right + // immediately; clamp to the monitor so it can't overflow a smaller + // screen. Persisted size (once saved) takes precedence over the default. + .with_inner_size(prefs.window_size.unwrap_or(WINDOW_DEFAULT_SIZE)) + .with_clamp_size_to_monitor_size(true) + // Start hidden so the first frame can fit the window to the monitor + // (see the reveal logic in `update`) before it's shown — the window + // then appears already at the right size instead of opening at the + // default and visibly resizing. + .with_visible(false); + // Intentionally do NOT restore fullscreen on launch — the app always opens + // windowed; fullscreen is only ever entered when the user asks (F11). let opts = eframe::NativeOptions { viewport, ..Default::default() @@ -130,9 +172,56 @@ struct App { /// Sequence number of the last frame we uploaded; used to skip the /// upload when the renderer hasn't produced a new frame. last_fb_seq: u64, + /// Filter the framebuffer texture is currently uploaded with: `true` = + /// NEAREST (crisp; used at integer device-pixel scales), `false` = LINEAR + /// (smooths the uneven pixel doubling at fractional scales). Tracked so we + /// only re-upload when the integer/fractional status actually flips. + fb_nearest: bool, + /// Current on-screen magnification of the emulated display: logical points + /// per emulated pixel (1.0 = native, i.e. the picture at its true size; <1 + /// shrunk to fit; >1 enlarged). 0.0 until the first frame is drawn. Shown in + /// the status footer next to MIPS. Crispness (NEAREST vs LINEAR) tracks the + /// *device-pixel* scale via `fb_nearest`, which this can differ from on HiDPI. + fb_scale: f32, + /// Set on a VM-scale slider or UI-zoom change; consumed on the next real + /// REX3 frame to resize the window so the display lands at the chosen VM + /// scale (clamped + ½×-snapped to fit the monitor; see `framebuffer_panel`). + /// Not set on Start — the window size is latched at app load, so launching + /// the VM never resizes the window. + pending_fb_snap: bool, + /// True on a first-ever launch (no persisted window size). Consumed on the + /// first frame that knows the monitor size, to fit the window to a 1280×1024 + /// display at the chosen VM scale so it opens at a sensible, windowed size + /// before the first Start. Returning users just reopen at their saved size. + pending_launcher_fit: bool, + /// The window starts hidden (`with_visible(false)`) so the first frame can + /// fit it to the monitor before it's shown. Set true once we've revealed it. + revealed: bool, + /// Frames rendered since launch; gates the reveal so the startup fit's + /// resize is applied before the window appears (and as a hard fallback so a + /// missing monitor size can't leave the window hidden). + startup_frame: u32, /// Per-frame state for the egui→PS2 input pump (modifier diff, /// mouse button mask, last cursor position). input_state: input::InputState, + /// Previous frame's `cpu_halted` status, so we can edge-detect the guest + /// becoming "safe to stop" and auto-release the captured mouse/keyboard + /// exactly once on that transition. Reset to true on Start so the initial + /// idle-at-PROM state doesn't count as a halt. + prev_cpu_halted: bool, + /// Active "Test Camera" preview (None when the window is closed). Opening it + /// starts host-camera capture; dropping it releases the device. + camera_test: Option, + /// egui texture for the camera-test preview + the last frame seq uploaded. + camera_test_tex: Option, + camera_test_seq: u64, + /// Active in-app IRIX serial-console viewer (None when closed). Connects to + /// the loopback serial server; dropping it closes the connection. + serial_console: Option, + /// Pending line typed into the serial console input field. + serial_input: String, + /// Whether the "How camera & networking work" Help window is open. + show_help_info: bool, } struct StopModal { @@ -215,9 +304,18 @@ impl App { opened_new_machine = true; } let _ = opened_new_machine; // (kept for future telemetry) + // Anchor the live config's NVRAM to the stable data dir too — covers the + // legacy-TOML import path above, which doesn't go through load()'s + // per-machine migration. + GuiSettings::migrate_nvram_path(&mut cfg.nvram); Self { - fullscreen: prefs.fullscreen, + fullscreen: false, + // First-ever launch (no saved size) → fit the window to the monitor + // on the first frame instead of using the static default verbatim. + pending_launcher_fit: prefs.window_size.is_none(), + revealed: false, + startup_frame: 0, prefs, cfg, cfg_path, @@ -237,7 +335,17 @@ impl App { restore_state_name: "snap1".into(), fb_tex: None, last_fb_seq: 0, + fb_nearest: true, + fb_scale: 0.0, + pending_fb_snap: false, input_state: input::InputState::default(), + prev_cpu_halted: true, + camera_test: None, + camera_test_tex: None, + camera_test_seq: 0, + serial_console: None, + serial_input: String::new(), + show_help_info: false, } } @@ -336,8 +444,44 @@ impl App { } self.jit.export(); self.emu.send(Cmd::Start(Box::new(self.cfg.clone()))); + // Don't resize the window when the VM launches — its size is latched at + // app load (the saved window size, or the first-launch fit to vm_scale) + // and the guest display is letterboxed into it. Only the VM-scale slider + // and UI zoom re-snap the window after that. + // Drop the previous run's cached texture so we never flash its last + // frame before the new run renders (the shared FrameSink is reset + // worker-side on Start; this clears the GUI's mirror of it). + self.fb_tex = None; + self.last_fb_seq = 0; + // Assume halted at boot (idle at the PROM) so the auto-release only + // fires on a later running→halted transition, not at startup. + self.prev_cpu_halted = true; + // If the NVRAM has no Ethernet MAC, IRIX won't attach ec0 — networking, + // System Manager, and Disk Manager all fail. Offer to set one up, and + // hold the machine at the PROM by interrupting autoboot (see the Esc + // guard in `update`) so the user sets the MAC before IRIX boots — one + // boot instead of boot-then-reboot. + // Seed a default NVRAM if there's none yet (a fresh install / bundled + // app has nothing in the working dir to migrate), so the machine boots + // with proper PROM env instead of a blank one. + if settings::ensure_nvram_seeded(&self.cfg.nvram) { + self.toast("seeded a default NVRAM"); + } + // Networking needs an Ethernet MAC in NVRAM (6 raw bytes at a fixed + // offset). If there's none, write a generated one *now*, before boot — + // so IRIX attaches ec0 on the first boot, no PROM monitor / reboot. + if !settings::nvram_has_mac(&self.cfg.nvram) { + let seed = self.prefs.active_machine.as_deref().unwrap_or("indy"); + let mac = settings::generate_mac_bytes(seed); + match settings::write_nvram_mac(&self.cfg.nvram, mac) { + Ok(true) => self.toast(format!("no Ethernet MAC in NVRAM — wrote {}", settings::mac_to_string(mac))), + Ok(false) => self.toast("no Ethernet MAC, and no NVRAM file yet to write into"), + Err(e) => self.toast(format!("couldn't write MAC to NVRAM: {e}")), + } + } } + /// Walk the configured SCSI devices and report any whose image file /// is missing. Scratch volumes are skipped (iris auto-creates those). /// For CD-ROMs the device is "present" if either the primary path or @@ -397,9 +541,11 @@ impl App { ctx.request_repaint_after(std::time::Duration::from_millis(next)); } - fn menu_bar(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { - egui::menu::bar(ui, |ui| { - ui.menu_button("File", |ui| { + /// The File/Machine/Memory/SCSI/View/Help menus, stacked vertically for the + /// left control column. Each is a full-width drop-down button. + fn menu_list(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { + ui.vertical(|ui| { + ui.menu_button("File ▶", |ui| { ui.set_min_width(220.0); if ui.button("New machine…").clicked() { self.new_machine.open(); @@ -491,7 +637,7 @@ impl App { ctx.send_viewport_cmd(ViewportCommand::Close); } }); - ui.menu_button("Machine", |ui| { + ui.menu_button("Machine ▶", |ui| { let running = self.emu.is_running(); if ui.add_enabled(!running, egui::Button::new("Start")).clicked() { self.start_emulator(); @@ -506,6 +652,23 @@ impl App { self.start_emulator(); ui.close_menu(); } + if ui.add_enabled(!running, egui::Button::new("Reset NVRAM (fresh PRAM)")) + .on_hover_text(format!( + "Restore this machine's NVRAM to defaults and assign a fresh Ethernet MAC.\n{}", + abs_path(&self.cfg.nvram))) + .clicked() + { + match settings::reset_nvram(&self.cfg.nvram) { + Ok(()) => { + let seed = self.prefs.active_machine.as_deref().unwrap_or("indy"); + let mac = settings::generate_mac_bytes(seed); + let _ = settings::write_nvram_mac(&self.cfg.nvram, mac); + self.toast(format!("NVRAM reset — new MAC {}", settings::mac_to_string(mac))); + } + Err(e) => self.toast(format!("NVRAM reset failed: {e}")), + } + ui.close_menu(); + } ui.separator(); ui.horizontal(|ui| { ui.label("Save state:"); @@ -528,8 +691,15 @@ impl App { } ui.close_menu(); } + if ui.add_enabled(running, egui::Button::new("Serial console…")) + .on_hover_text("View the IRIX serial console (ttyd1) over the loopback serial server") + .clicked() + { + self.open_serial_console(); + ui.close_menu(); + } }); - ui.menu_button("Memory", |ui| { + ui.menu_button("Memory ▶", |ui| { ui.set_min_width(220.0); let total: u32 = self.cfg.banks.iter().sum(); ui.label(RichText::new(format!("Total: {total} MB")).strong()); @@ -557,7 +727,7 @@ impl App { }); } }); - ui.menu_button("SCSI", |ui| { + ui.menu_button("SCSI ▶", |ui| { let action = scsi_menu::draw(ui, &self.cfg); match action { scsi_menu::ScsiAction::None => {} @@ -572,7 +742,7 @@ impl App { } } }); - ui.menu_button("View", |ui| { + ui.menu_button("View ▶", |ui| { if ui.button(if self.fullscreen { "Exit fullscreen (F11)" } else { "Fullscreen (F11)" }).clicked() { self.fullscreen = !self.fullscreen; ctx.send_viewport_cmd(ViewportCommand::Fullscreen(self.fullscreen)); @@ -587,14 +757,54 @@ impl App { ui.add(egui::Slider::new(&mut self.prefs.ui_scale, UI_SCALE_MIN..=UI_SCALE_MAX)); if ui.button("Apply").clicked() { ctx.set_zoom_factor(self.prefs.ui_scale); + // Re-fit the window so the bigger/smaller controls grow + // the window rather than squeezing the picture. + self.pending_fb_snap = true; } }); ui.label(RichText::new("Ctrl+= / Ctrl+- / Ctrl+0 to zoom").weak().small()); + ui.separator(); + ui.horizontal(|ui| { + ui.label("VM screen"); + // Sets the emulated-display magnification directly (1× = + // native), independent of UI scale. The window resizes to + // hold the picture at the chosen size on the next frame. + let changed = ui.add( + egui::Slider::new(&mut self.prefs.vm_scale, settings::VM_SCALE_MIN..=settings::VM_SCALE_MAX) + .step_by(settings::VM_SCALE_STEP) + .suffix("×"), + ).changed(); + if changed { self.pending_fb_snap = true; } + }); + ui.label(RichText::new("1× = native pixels; ¼× steps (½-integers crispest on Retina)").weak().small()); }); - ui.menu_button("Help", |ui| { + ui.menu_button("Help ▶", |ui| { ui.label(RichText::new("IRIS — SGI Indy (MIPS R4400) Emulator").strong()); ui.label(format!("Version {}", env!("APP_VERSION"))); ui.separator(); + ui.label(RichText::new("Diagnostics").strong()); + let running = self.emu.is_running(); + if ui.add_enabled(running, egui::Button::new("📷 Test Camera…")) + .on_hover_text("Preview the host camera used for the emulated IndyCam") + .on_disabled_hover_text("Start a machine first") + .clicked() + { + self.open_camera_test(); + ui.close_menu(); + } + if ui.add_enabled(running, egui::Button::new("🌐 Network test (serial console)…")) + .on_hover_text("Connect to the emulator's loopback serial server (127.0.0.1:8881)") + .on_disabled_hover_text("Start a machine first") + .clicked() + { + self.open_serial_console(); + ui.close_menu(); + } + if ui.button("ℹ How camera & networking work…").clicked() { + self.show_help_info = true; + ui.close_menu(); + } + ui.separator(); ui.label(RichText::new("Authors").strong()); ui.label("Original: techomancer"); ui.label("iris-gui fork: Dani Sarfati (danifunker)"); @@ -619,33 +829,42 @@ impl App { }); } - fn toolbar(&mut self, ui: &mut egui::Ui) { - ui.horizontal(|ui| { - let running = self.emu.is_running(); - if !running { - if ui.add(egui::Button::new(RichText::new("▶ Start").size(16.0)) - .fill(Color32::from_rgb(40, 110, 40))).clicked() - { - self.start_emulator(); - } - } else if ui.add(egui::Button::new(RichText::new("■ Stop").size(16.0)) - .fill(Color32::from_rgb(160, 60, 60))).clicked() + /// Start/Stop + save-state controls, stacked full-width for the left column. + fn machine_controls(&mut self, ui: &mut egui::Ui) { + let running = self.emu.is_running(); + let full = egui::vec2(ui.available_width(), 0.0); + if !running { + if ui.add_sized(full, egui::Button::new(RichText::new("▶ Start").size(16.0)) + .fill(Color32::from_rgb(40, 110, 40))).clicked() { - self.request_stop(); - } - ui.separator(); - if ui.add_enabled(running, egui::Button::new("💾 Save state")).clicked() { - self.emu.send(Cmd::SaveState(self.save_state_name.clone())); - } - if ui.add_enabled(running, egui::Button::new("Restore state")).clicked() { - self.emu.send(Cmd::RestoreState(self.restore_state_name.clone())); - } - ui.separator(); - let edit_label = if self.show_config_editor { "Hide config editor" } else { "Edit config…" }; - if ui.button(edit_label).clicked() { - self.show_config_editor = !self.show_config_editor; + self.start_emulator(); } - if self.show_config_editor { + } else if ui.add_sized(full, egui::Button::new(RichText::new("■ Stop").size(16.0)) + .fill(Color32::from_rgb(160, 60, 60))).clicked() + { + self.request_stop(); + } + if ui.add_enabled_ui(running, |ui| { + ui.add_sized(egui::vec2(ui.available_width(), 0.0), egui::Button::new("💾 Save state")).clicked() + }).inner { + self.emu.send(Cmd::SaveState(self.save_state_name.clone())); + } + if ui.add_enabled_ui(running, |ui| { + ui.add_sized(egui::vec2(ui.available_width(), 0.0), egui::Button::new("Restore state")).clicked() + }).inner { + self.emu.send(Cmd::RestoreState(self.restore_state_name.clone())); + } + } + + /// "Edit config" toggle plus the quick-jump tab buttons (shown only while + /// the config editor is open). Stacked for the left column. + fn config_quick_buttons(&mut self, ui: &mut egui::Ui) { + let edit_label = if self.show_config_editor { "Hide config editor" } else { "Edit config…" }; + if ui.add_sized(egui::vec2(ui.available_width(), 0.0), egui::Button::new(edit_label)).clicked() { + self.show_config_editor = !self.show_config_editor; + } + if self.show_config_editor { + ui.indent("quick_tabs", |ui| { if ui.button("Network").clicked() { self.tab = Tab::Network; } if ui.button("Video-In").clicked() { self.tab = Tab::VideoIn; } // Debug/JIT is compiled out of lightning builds; CI is hidden @@ -657,24 +876,96 @@ impl App { if !cfg!(feature = "appstore") && ui.button("CI").clicked() { self.tab = Tab::Ci; } - } - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let status = if self.emu.status.power_off_seen { - RichText::new("halted").color(Color32::LIGHT_GRAY) - } else if self.emu.status.in_prom { - RichText::new("PROM").color(Color32::LIGHT_BLUE) - } else if running { - RichText::new("IRIX running").color(Color32::LIGHT_GREEN) - } else { - RichText::new("stopped").color(Color32::GRAY) - }; - ui.label(status); - if running { - ui.label(format!("{:.0} MIPS", self.emu.status.mips)); - } }); - }); + } + } + + /// Run-state line (IRIX running / PROM / halted / stopped + MIPS). Used in + /// the control column's status footer. + fn run_state_label(&self, ui: &mut egui::Ui) { + let running = self.emu.is_running(); + let halted = running && self.emu.status.cpu_halted; + let status = if halted { + RichText::new("halted — safe to stop").color(Color32::LIGHT_GRAY) + } else if self.emu.status.in_prom { + RichText::new("PROM").color(Color32::LIGHT_BLUE) + } else if running { + RichText::new("IRIX running").color(Color32::LIGHT_GREEN) + } else { + RichText::new("stopped").color(Color32::GRAY) + }; + ui.label(status); + if running && !halted { + ui.label(format!("{:.0} MIPS", self.emu.status.mips)); + } + if running && self.fb_scale > 0.0 { + // How magnified the emulated display currently is (1× = native). + // Round-snap the readout so a whole-number scale reads cleanly. + let mag = self.fb_scale; + let whole = (mag - mag.round()).abs() <= 0.01; + let num = if whole { format!("{:.0}×", mag.round()) } else { format!("{mag:.2}×") }; + let label = if self.fb_nearest { + RichText::new(format!("{num} scale")).color(Color32::LIGHT_GRAY) + } else { + RichText::new(format!("{num} scale · filtered")).color(Color32::from_rgb(200, 175, 90)) + }; + ui.label(label).on_hover_text( + "On-screen size of the emulated display: logical points per emulated \ + pixel (1× = native). \"filtered\" means the current size isn't a whole \ + device-pixel multiple, so the image is smoothed rather than pixel-crisp.", + ); + } + } + + /// Resize the window so the central panel exactly holds the emulated + /// display at native scale — 1 framebuffer pixel = 1 logical point, which + /// is an integer device-pixel scale on both standard (1×) and HiDPI (2×) + /// screens, so the picture stays crisp and fills the window without + /// letterbox bars. Shrinks below native only if that wouldn't fit the + /// monitor work area. `central_avail` is the framebuffer panel's free space; + /// `screen_rect − central_avail` is the surrounding chrome (the left control + /// column, plus the config editor when open) we must keep room for. The math + /// is orientation-agnostic, so it works whether chrome is a side column or + /// the older top/bottom bars. + /// Resizes the window so the display lands at `vm_scale` (clamped to fit the + /// monitor). Does NOT write the clamped value back to `prefs.vm_scale` — the + /// slider stays the user's requested scale so it can always be dragged; the + /// footer readout reports the scale actually achieved. + fn snap_window_to_fb(ctx: &egui::Context, fb_px: egui::Vec2, central_avail: egui::Vec2, vm_scale: f32) { + if fb_px.x < 1.0 || fb_px.y < 1.0 { return; } + // egui-winit reports screen_rect / available_size / monitor_size *and* + // interprets ViewportCommand::InnerSize all in the same (zoom-scaled) + // point space, so chrome math stays in egui points. + let screen = ctx.screen_rect().size(); + let chrome_w = (screen.x - central_avail.x).max(0.0); + let chrome_h = (screen.y - central_avail.y).max(0.0); + let zoom = ctx.zoom_factor().max(0.1); + // Target points-per-pixel for the requested VM scale (vm_scale device + // pixels per emulated pixel at native backing). Dividing by zoom keeps + // the picture decoupled from the UI scale, so scaling the controls + // widens the window instead of shrinking the display. + let target = (vm_scale / zoom).max(0.01); + // Always shrink to fit the work area, leaving a clear margin so the + // window stays obviously windowed (not edge-to-edge). Fall back to a + // conservative cap if the monitor size isn't reported, so a high VM + // scale can never blow the window up off-screen and push the controls + // out of view. + const MARGIN: f32 = 0.85; + let (avail_w, avail_h) = match ctx.input(|i| i.viewport().monitor_size) { + Some(m) => (m.x * MARGIN - chrome_w, m.y * MARGIN - chrome_h), + None => (1400.0 - chrome_w, 900.0 - chrome_h), + }; + // Use the requested scale, or the largest that fits when the monitor is + // the binding constraint. The slider already restricts requests to clean + // ¼× steps, so we don't snap here — when we must clamp to the monitor we + // use the full fitting size (the footer readout reports the actual scale + // and tags non-crisp ones) rather than dropping a whole step. + let scale = target + .min((avail_w.max(64.0)) / fb_px.x) + .min((avail_h.max(64.0)) / fb_px.y) + .clamp(0.05, target); + let inner = egui::vec2(fb_px.x * scale + chrome_w, fb_px.y * scale + chrome_h); + ctx.send_viewport_cmd(ViewportCommand::InnerSize(inner)); } /// Draw the live REX3 framebuffer as an egui image, scaled to fit @@ -693,32 +984,70 @@ impl App { return; } - if self.fb_tex.is_none() || seq != self.last_fb_seq { + let avail = ui.available_size(); + + // Pick the texture filter from the *device-pixel* scale at which the + // framebuffer will actually be drawn. At an integer scale (native 1×, + // 2×, 3×, …) NEAREST keeps every emulated pixel crisp and square. At a + // fractional scale — which is what most users hit once they set a + // non-100% UI scale or resize the window freely — NEAREST has to double + // some source pixels and not others, so e.g. the strokes of a "T" come + // out uneven; LINEAR (bilinear) spreads the error and looks right. We + // need the native size to compute this, so on the very first frame + // (no texture yet) we default to NEAREST and correct on the next frame. + let zoom = ui.ctx().zoom_factor(); + let want_nearest = match self.fb_tex.as_ref().map(|t| t.size_vec2()) { + Some(px) if px.x >= 1.0 && px.y >= 1.0 => { + let size = fb_fit_size(avail, px); + let scale = size.y * ui.ctx().pixels_per_point() / px.y; + is_integer_scale(scale) + } + _ => true, + }; + + if self.fb_tex.is_none() || seq != self.last_fb_seq || want_nearest != self.fb_nearest { let frame = self.emu.frame_sink.snapshot(); if frame.width == 0 || frame.height == 0 { return; } let img = egui::ColorImage::from_rgba_unmultiplied( [frame.width, frame.height], &frame.rgba); + let opts = if want_nearest { + egui::TextureOptions::NEAREST + } else { + egui::TextureOptions::LINEAR + }; match &mut self.fb_tex { - Some(t) => t.set(img, egui::TextureOptions::NEAREST), + Some(t) => t.set(img, opts), None => { - self.fb_tex = Some(ui.ctx().load_texture( - "rex3_fb", img, egui::TextureOptions::NEAREST)); + self.fb_tex = Some(ui.ctx().load_texture("rex3_fb", img, opts)); } } self.last_fb_seq = frame.seq; + self.fb_nearest = want_nearest; } + // Consume the snap request before the immutable borrow of self.fb_tex. + let do_snap = std::mem::take(&mut self.pending_fb_snap); + let mut fb_rect = egui::Rect::NOTHING; + let mut fb_clicked = false; + let mut new_fb_scale = 0.0; if let Some(tex) = &self.fb_tex { - let avail = ui.available_size(); let tex_size = tex.size_vec2(); - let fb_aspect = tex_size.x / tex_size.y; - let avail_aspect = avail.x / avail.y; - let size = if avail_aspect > fb_aspect { - egui::vec2(avail.y * fb_aspect, avail.y) - } else { - egui::vec2(avail.x, avail.x / fb_aspect) - }; + // First frame after Start (or after a VM/UI scale change): size the + // window so the picture lands at the requested VM scale. + if do_snap { + Self::snap_window_to_fb(ui.ctx(), tex_size, avail, self.prefs.vm_scale); + } + // Fill the available area (aspect-preserved). The window — not the + // image — carries the chosen scale (set by the snap above), so the + // steady-state draw is stable: no per-frame resize, no jitter. + let size = fb_fit_size(avail, tex_size); + // Reported VM scale: device pixels per emulated pixel relative to + // native backing (1.0 = native). `size` is in zoom-scaled points, so + // multiply by zoom to recover the zoom-independent figure. + if tex_size.y >= 1.0 { + new_fb_scale = size.y * zoom / tex_size.y; + } ui.centered_and_justified(|ui| { let response = ui.add( egui::Image::new((tex.id(), size)).fit_to_exact_size(size).sense(egui::Sense::click()) @@ -727,16 +1056,30 @@ impl App { // Take keyboard focus so that egui delivers Key events // to us instead of routing them to other widgets when // the user clicks into the FB. - if response.clicked() { response.request_focus(); } + if response.clicked() { response.request_focus(); fb_clicked = true; } }); } + self.fb_scale = new_fb_scale; + + // When the guest becomes "safe to stop" (CPU halted — a clean IRIX + // shutdown / `halt`), auto-release the captured mouse & keyboard so the + // user gets their cursor back without pressing Ctrl+Alt+Esc. Edge- + // triggered on the running→halted transition (prev is reset to true at + // Start, so idling at the PROM during boot doesn't count), and they can + // still click the display to re-capture. + let halted = self.emu.status.cpu_halted; + if halted && !self.prev_cpu_halted && self.input_state.captured { + input::force_release(ui.ctx(), &mut self.input_state); + self.toast("guest halted — mouse released (click display to re-capture)"); + } + self.prev_cpu_halted = halted; // Pump egui input → PS/2 controller. Mouse/keyboard only reach the // guest while captured (click the framebuffer to capture, Ctrl+Alt+Esc // to release), so menu clicks and config typing don't leak in. let ps2 = self.emu.ps2.lock().clone(); if let Some(ps2) = ps2 { - input::pump(ui.ctx(), fb_rect, &ps2, &mut self.input_state, self.cfg.mouse_scroll_pixels_per_line); + input::pump(ui.ctx(), fb_clicked, &ps2, &mut self.input_state, self.cfg.mouse_scroll_pixels_per_line); } // Capture hint, drawn over the framebuffer. @@ -765,10 +1108,248 @@ impl App { ui.separator(); match show_tab(ui, self.tab, &mut self.cfg, &mut self.jit) { ConfigAction::RequestEmbeddedProm => self.confirm_embedded_prom = true, + ConfigAction::TestCamera => self.open_camera_test(), ConfigAction::None => {} } } + /// Open (or restart) the live host-camera preview using the current + /// `[vino]` standard and camera index. Releases any previous test first. + fn open_camera_test(&mut self) { + use iris::video_source::VideoStandard; + let standard = match self.cfg.vino.standard { + iris::config::VinoStandard::Ntsc => VideoStandard::Ntsc, + iris::config::VinoStandard::Pal => VideoStandard::Pal, + }; + // Drop the previous instance first so the camera is fully released + // before re-opening (its Drop joins the capture thread). + self.camera_test = None; + self.camera_test_tex = None; + self.camera_test_seq = 0; + self.camera_test = Some(camera_test::CameraTest::start(standard, self.cfg.vino.camera_index)); + } + + /// Draw the live host-camera preview window (no-op when closed). Closing it + /// (or pressing Stop) drops the `CameraTest`, releasing the camera. + fn camera_test_window(&mut self, ctx: &egui::Context) { + let Some(test) = &self.camera_test else { return }; + + // Pull the latest frame and upload it to the preview texture. + if let Some((w, h, rgba)) = test.take_new_frame(&mut self.camera_test_seq) { + let image = egui::ColorImage::from_rgba_unmultiplied([w as usize, h as usize], &rgba); + match &mut self.camera_test_tex { + Some(tex) => tex.set(image, egui::TextureOptions::LINEAR), + None => { + self.camera_test_tex = + Some(ctx.load_texture("camera-test", image, egui::TextureOptions::LINEAR)); + } + } + } + let status = test.status(); + let error = test.error(); + + let mut open = true; + let mut close_now = false; + egui::Window::new("Test Camera") + .open(&mut open) + .collapsible(false) + .resizable(false) + .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) + .show(ctx, |ui| { + if let Some(e) = &error { + ui.colored_label(Color32::from_rgb(200, 80, 80), + format!("Camera unavailable: {e}")); + ui.label(RichText::new( + "Check that a camera is connected and that IRIS has camera \ + permission (System Settings → Privacy & Security → Camera).") + .weak()); + } else if let Some(tex) = &self.camera_test_tex { + // The capture field is half-height (interlaced), so present + // it at ~4:3 by drawing into a fixed-size rect. + ui.add(egui::Image::new(&*tex) + .fit_to_exact_size(egui::vec2(480.0, 360.0)) + .rounding(4.0)); + } else { + ui.add_space(110.0); + ui.label("Starting capture…"); + ui.label(RichText::new( + "If no image appears, grant camera access in System \ + Settings → Privacy & Security → Camera, then reopen.") + .weak().small()); + } + ui.add_space(6.0); + ui.label(RichText::new(&status).weak().small()); + ui.add_space(6.0); + if ui.button("Close").clicked() { + close_now = true; + } + }); + + if !open || close_now { + // Drop releases the camera (CameraTest::Drop joins the worker). + self.camera_test = None; + self.camera_test_tex = None; + self.camera_test_seq = 0; + } else { + // Keep the preview animating even when egui is otherwise idle. + ctx.request_repaint_after(std::time::Duration::from_millis(33)); + } + } + + /// Open (or reconnect) the in-app IRIX serial-console viewer. Connects to + /// the loopback serial server the running emulator exposes. + fn open_serial_console(&mut self) { + self.serial_console = Some(serial_console::SerialConsole::connect()); + } + + /// Draw the in-app serial-console window (no-op when closed). Demonstrates + /// the loopback serial server: the emulator listens on 127.0.0.1:8881 and + /// this viewer connects to it. + fn serial_console_window(&mut self, ctx: &egui::Context) { + let Some(console) = &self.serial_console else { return }; + let (text, connected, error, _seq) = console.snapshot(); + + let mut open = true; + let mut close_now = false; + let mut clear_now = false; + let mut to_send: Option = None; + egui::Window::new("IRIX Serial Console (ttyd1)") + .open(&mut open) + .default_width(620.0) + .default_height(420.0) + .resizable(true) + .show(ctx, |ui| { + ui.horizontal(|ui| { + if let Some(e) = &error { + ui.colored_label(Color32::from_rgb(200, 80, 80), e); + } else if connected { + ui.colored_label(Color32::from_rgb(90, 170, 90), + format!("● connected to {}", serial_console::SERIAL_ADDR)); + } else { + ui.label("disconnected"); + } + }); + ui.separator(); + + egui::ScrollArea::vertical() + .stick_to_bottom(true) + .auto_shrink([false, false]) + .max_height(320.0) + .show(ui, |ui| { + ui.add( + egui::Label::new( + RichText::new(&text).monospace().size(12.0), + ) + .wrap(), + ); + }); + + ui.separator(); + ui.horizontal(|ui| { + let resp = ui.add( + egui::TextEdit::singleline(&mut self.serial_input) + .hint_text("type a command, press Enter") + .desired_width(420.0), + ); + let entered = resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); + if (ui.button("Send").clicked() || entered) && connected { + to_send = Some(std::mem::take(&mut self.serial_input)); + resp.request_focus(); + } + if ui.button("Clear").clicked() { + clear_now = true; + } + if ui.button("Close").clicked() { + close_now = true; + } + }); + }); + + if let Some(line) = to_send { + let mut bytes = line.into_bytes(); + bytes.push(b'\n'); + console.send(&bytes); + } + if clear_now { + console.clear(); + } + if !open || close_now { + self.serial_console = None; + } else if connected { + // Poll for new console output even when egui is otherwise idle. + ctx.request_repaint_after(std::time::Duration::from_millis(80)); + } + } + + /// Explains the camera (IndyCam) and networking features — what they do and + /// how to use them (for end users), and what host capabilities they use and + /// why (for App Review). Opened from Help → "How camera & networking work". + fn help_info_window(&mut self, ctx: &egui::Context) { + if !self.show_help_info { + return; + } + let mut open = true; + egui::Window::new("How camera & networking work") + .open(&mut open) + .default_width(560.0) + .default_height(480.0) + .collapsible(false) + .resizable(true) + .show(ctx, |ui| { + egui::ScrollArea::vertical().max_height(420.0).auto_shrink([false, false]).show(ui, |ui| { + ui.heading("📷 Camera — the IndyCam"); + ui.label( + "IRIS emulates the SGI Indy's IndyCam video-input hardware (the VINO device). \ + When you pick your Mac's camera as the video source, IRIS captures live frames \ + from it and feeds them to the emulated video input — just as a real IndyCam fed \ + a real Indy.", + ); + ui.add_space(6.0); + ui.label(RichText::new("How to use it").strong()); + ui.label("• Help → Diagnostics → Test Camera shows a live preview (start a machine first)."); + ui.label("• Or set Video-In → Source = camera, boot IRIX, and run an IndyCam app like vino/cam."); + ui.label("• On first use macOS asks for camera permission. Closing the preview releases the camera."); + ui.add_space(6.0); + ui.label(RichText::new("Privacy / for App Review").strong()); + ui.label( + "Camera frames are used only as the emulated video input — IRIS never records, \ + stores, or transmits them. It uses the public AVFoundation API with the \ + com.apple.security.device.camera entitlement and the NSCameraUsageDescription \ + purpose string.", + ); + + ui.separator(); + ui.heading("🌐 Networking"); + ui.label( + "The emulated Indy reaches the internet through a built-in user-mode NAT, like a \ + home router: outbound connections from IRIX are translated onto the host. No \ + system network settings or elevated privileges are touched.", + ); + ui.add_space(6.0); + ui.label(RichText::new("How to use it").strong()); + ui.label("• The guest serial console (ttyd1) and PROM monitor are exposed on loopback"); + ui.label(" TCP (127.0.0.1:8881 / 8888) so you can attach a terminal."); + ui.label("• Help → Diagnostics → Network test opens an in-app viewer of that console."); + ui.label("• Optional inbound port-forwards (Networking tab) let you reach guest services."); + ui.add_space(6.0); + ui.label(RichText::new("Privacy / for App Review").strong()); + ui.label( + "Outbound guest traffic uses com.apple.security.network.client. The loopback \ + serial/monitor servers and any inbound port-forwards use \ + com.apple.security.network.server. Every socket is on loopback or user-initiated; \ + IRIS opens no network connections on its own.", + ); + }); + ui.separator(); + if ui.button("Close").clicked() { + self.show_help_info = false; + } + }); + if !open { + self.show_help_info = false; + } + } + fn welcome_panel(&mut self, ui: &mut egui::Ui) { ui.add_space(8.0); ui.heading("iris — SGI Indy emulator"); @@ -786,13 +1367,13 @@ impl App { egui::Grid::new("summary_grid").num_columns(2).striped(true).show(ui, |ui| { ui.label("PROM"); ui.label(if std::path::Path::new(&self.cfg.prom).exists() { - self.cfg.prom.clone() + abs_path(&self.cfg.prom) } else { format!("{} (missing -> embedded fallback)", self.cfg.prom) }); ui.end_row(); ui.label("NVRAM"); - ui.label(&self.cfg.nvram); + ui.label(abs_path(&self.cfg.nvram)); ui.end_row(); ui.label("RAM"); ui.label(format!("{total_ram} MB ({:?})", self.cfg.banks)); @@ -807,7 +1388,7 @@ impl App { for id in ids { let d = &self.cfg.scsi[&id]; let kind = if d.cdrom { "CD" } else { "HDD" }; - ui.label(format!("scsi{id} {kind}: {}", d.path)); + ui.label(format!("scsi{id} {kind}: {}", abs_path(&d.path))); } ui.label(RichText::new("Use the SCSI menu to attach / detach / replace.").weak().small()); }); @@ -829,21 +1410,41 @@ impl App { } } - fn status_bar(&mut self, ui: &mut egui::Ui) { - ui.horizontal(|ui| { - let name = self.prefs.active_machine.as_deref().unwrap_or("(unsaved)"); - ui.label(format!("Machine: {name}{}", if self.cfg_dirty { " *" } else { "" })); - ui.separator(); - ui.label(format!("Dirty COW: {}", self.emu.status.dirty_cow)); - if let Some((msg, when)) = self.toast.clone() { - if when.elapsed().as_secs() < 5 { - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label(RichText::new(msg).color(Color32::YELLOW)); - }); - } else { - self.toast = None; - } + /// Status footer for the left control column: run-state, machine name, + /// dirty-COW count, and the transient toast. Laid out vertically. + fn status_block(&mut self, ui: &mut egui::Ui) { + ui.add_space(4.0); + self.run_state_label(ui); + ui.separator(); + let name = self.prefs.active_machine.as_deref().unwrap_or("(unsaved)"); + ui.label(format!("Machine: {name}{}", if self.cfg_dirty { " *" } else { "" })); + ui.label(format!("Dirty COW: {}", self.emu.status.dirty_cow)); + if let Some((msg, when)) = self.toast.clone() { + if when.elapsed().as_secs() < 5 { + ui.add_space(2.0); + ui.label(RichText::new(msg).color(Color32::YELLOW)); + } else { + self.toast = None; } + } + ui.add_space(2.0); + } + + /// The full left control column: machine controls, menus, and config + /// quick-buttons stacked vertically, with the status block pinned to the + /// bottom. This replaces the old top menu bar + toolbar + bottom status bar, + /// freeing vertical space for the (tall, 5:4) emulated display. + fn control_panel(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { + egui::TopBottomPanel::bottom("ctl_status") + .show_inside(ui, |ui| self.status_block(ui)); + + egui::ScrollArea::vertical().show(ui, |ui| { + ui.add_space(4.0); + self.machine_controls(ui); + ui.separator(); + self.menu_list(ui, ctx); + ui.separator(); + self.config_quick_buttons(ui); }); } } @@ -853,6 +1454,17 @@ impl eframe::App for App { self.handle_events(ctx); self.maybe_autosave(); + // Remember the current window size so the next launch reopens at it. + // inner_rect is in logical points — the same unit ViewportBuilder's + // with_inner_size() takes — so this round-trips regardless of UI zoom. + // Stored in-memory here; on_exit() (and other save() calls) persist it. + if let Some(r) = ctx.input(|i| i.viewport().inner_rect) { + let sz = r.size(); + if sz.x.is_finite() && sz.y.is_finite() && sz.x >= 480.0 && sz.y >= 360.0 { + self.prefs.window_size = Some([sz.x.round(), sz.y.round()]); + } + } + // F11 toggles fullscreen. if ctx.input(|i| i.key_pressed(egui::Key::F11)) { self.fullscreen = !self.fullscreen; @@ -869,18 +1481,16 @@ impl eframe::App for App { if zoom_in { self.prefs.ui_scale = (self.prefs.ui_scale + 0.1).min(UI_SCALE_MAX); ctx.set_zoom_factor(self.prefs.ui_scale); } if zoom_out { self.prefs.ui_scale = (self.prefs.ui_scale - 0.1).max(UI_SCALE_MIN); ctx.set_zoom_factor(self.prefs.ui_scale); } if zoom_reset { self.prefs.ui_scale = settings::UI_SCALE_DEFAULT; ctx.set_zoom_factor(self.prefs.ui_scale); } + // Any UI-zoom change re-fits the window so the controls grow it instead + // of squeezing the (decoupled) VM screen. + if zoom_in || zoom_out || zoom_reset { self.pending_fb_snap = true; } - // In fullscreen, only reveal menu/toolbar when the cursor is near the top. - let pointer_y = ctx.input(|i| i.pointer.latest_pos().map(|p| p.y).unwrap_or(f32::MAX)); - let chrome_visible = !self.fullscreen || pointer_y < 36.0; - - if chrome_visible { - egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| self.menu_bar(ui, ctx)); - egui::TopBottomPanel::top("toolbar").show(ctx, |ui| self.toolbar(ui)); - } - if !self.fullscreen { - egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| self.status_bar(ui)); - } + // The control column lives on the left, always visible (even in + // fullscreen) — the VM screen sits to its right and never hides it. + egui::SidePanel::left("control_panel") + .resizable(false) + .exact_width(186.0) + .show(ctx, |ui| self.control_panel(ui, ctx)); // Config editor lives in a collapsible side panel so the emulator // screen (central panel) is never hidden by it. The toolbar's @@ -902,7 +1512,13 @@ impl eframe::App for App { egui::ScrollArea::vertical().show(ui, |ui| self.central_tabs(ui)); }); - egui::CentralPanel::default().show(ctx, |ui| { + // Zero the central panel's inner margin so the emulated display reaches + // the window edges — every reclaimed pixel makes the (tall, 5:4) picture + // a little bigger. Keep the dark panel fill so the aspect-ratio + // letterbox bars stay black. + let central_frame = egui::Frame::central_panel(&ctx.style()) + .inner_margin(egui::Margin::ZERO); + egui::CentralPanel::default().frame(central_frame).show(ctx, |ui| { // The central panel always shows the emulator screen when the // machine is running (the REX3 framebuffer), falling back to the // welcome / status summary when idle. The config editor no longer @@ -910,6 +1526,16 @@ impl eframe::App for App { if self.emu.is_running() { self.framebuffer_panel(ui); } else { + // First-ever launch: size the window to the monitor for the + // standard 1280×1024 display before the user sees it (the + // on-Start snap later re-fits to the actual guest resolution). + // Wait for the monitor size to be known before consuming the flag. + if self.pending_launcher_fit + && ui.ctx().input(|i| i.viewport().monitor_size).is_some() + { + Self::snap_window_to_fb(ui.ctx(), egui::vec2(1280.0, 1024.0), ui.available_size(), self.prefs.vm_scale); + self.pending_launcher_fit = false; + } // Emulator not running: make sure a leftover mouse capture is // released so the host cursor isn't stuck hidden/locked. input::force_release(ui.ctx(), &mut self.input_state); @@ -941,6 +1567,15 @@ impl eframe::App for App { self.toast(format!("created {path_str} and attached at scsi{}", result.scsi_id)); } + // Live host-camera test window. + self.camera_test_window(ctx); + + // In-app IRIX serial-console viewer. + self.serial_console_window(ctx); + + // Help → "How camera & networking work" explainer. + self.help_info_window(ctx); + // Safe-stop confirmation modal. let mut close_modal = false; let mut do_force = false; @@ -968,25 +1603,16 @@ impl eframe::App for App { if close_modal { self.stop_modal = None; } if do_force { self.emu.send(Cmd::Stop); } if do_halt { - // iris always opens 127.0.0.1:8881 as the ttyd1 (IRIX serial - // console) TCP listener in non-CI mode. Connect to it, - // write "halt\n", disconnect. IRIX takes a few seconds to - // shut down cleanly; the user can hit Stop again once the - // PROM "halted" message appears. - use std::io::Write as _; - match std::net::TcpStream::connect_timeout( - &"127.0.0.1:8881".parse().unwrap(), - std::time::Duration::from_millis(500), - ) { - Ok(mut s) => { - let _ = s.write_all(b"halt\n"); - self.toast("sent 'halt' to IRIX — wait for shutdown, then Stop"); - } - Err(e) => { - self.toast(format!("halt failed: {e} — falling back to Force stop")); - self.emu.send(Cmd::Stop); - } - } + // Type "halt\n" at the IRIX serial console in-process (see + // EmulatorHandle / Machine::inject_serial_console). This used to + // open a loopback TCP client to 127.0.0.1:8881; doing it in-process + // means clean shutdown no longer depends on the serial server + // socket (which the macOS App Sandbox would otherwise gate behind + // the network.server entitlement). IRIX takes a few seconds to shut + // down cleanly; the user can hit Stop once the PROM "halted" + // message appears. + self.emu.send(Cmd::HaltIrix); + self.toast("sent 'halt' to IRIX — wait for shutdown, then Stop"); } // Confirm switching from a custom PROM back to the built-in image. @@ -1074,6 +1700,23 @@ impl eframe::App for App { self.detach_and_start(&ids); } } + + // The window starts hidden so the first frame(s) can fit it to the + // monitor (the launcher fit, when not running) before it's shown — + // avoiding a visible open-then-resize. Reveal one frame after the fit + // settles so its resize has applied, or unconditionally after a short + // grace period so a missing monitor size can't leave the window hidden. + if !self.revealed { + self.startup_frame = self.startup_frame.saturating_add(1); + let fit_settled = !self.pending_launcher_fit; + if (fit_settled && self.startup_frame >= 2) || self.startup_frame >= 10 { + ctx.send_viewport_cmd(ViewportCommand::Visible(true)); + self.revealed = true; + } else { + // Keep frames coming while hidden so we reach the reveal. + ctx.request_repaint(); + } + } } fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { diff --git a/iris-gui/src/safe_stop.rs b/iris-gui/src/safe_stop.rs index 9504450..a231ede 100644 --- a/iris-gui/src/safe_stop.rs +++ b/iris-gui/src/safe_stop.rs @@ -18,8 +18,12 @@ impl UnsafeReasons { /// Evaluate whether stopping the emulator right now is safe. /// -/// The core does not expose live dirty-sector state, so we decide purely from -/// config: an abrupt power-off only risks the on-disk image when some attached +/// If the CPU has halted (clean shutdown / soft power-off, or idle at the PROM), +/// nothing is writing and stopping is always safe — see the `cpu_halted` +/// short-circuit below. +/// +/// Otherwise the core does not expose live dirty-sector state, so we decide +/// purely from config: an abrupt power-off only risks the on-disk image when some attached /// device persists guest writes straight into its **base image** — i.e. a /// plain read-write hard disk. Everything else leaves the base image untouched /// and is safe to power off without warning: @@ -34,7 +38,14 @@ impl UnsafeReasons { /// /// So when no attached device writes through to its base image, powering off /// will NOT damage the hard disk and we skip the confirmation dialog entirely. -pub fn evaluate(_status: &Status, cfg: &MachineConfig) -> UnsafeReasons { +pub fn evaluate(status: &Status, cfg: &MachineConfig) -> UnsafeReasons { + // If the CPU has halted — a clean IRIX shutdown / soft power-off, or sitting + // idle at the PROM (0 MIPS) — nothing is writing to any disk, so stopping + // now cannot corrupt a filesystem. Skip the warning regardless of config. + if status.cpu_halted { + return UnsafeReasons::default(); + } + let mut r = UnsafeReasons::default(); for (id, dev) in &cfg.scsi { let persists_to_base = !dev.cdrom @@ -61,3 +72,29 @@ pub fn reason_lines(r: &UnsafeReasons) -> Vec { }) .collect() } + +#[cfg(test)] +mod tests { + use super::*; + + // MachineConfig::default() attaches scsi1 as a plain read-write disk image, + // so it is the "unsafe while running" case. + fn running(halted: bool) -> Status { + Status { running: true, cpu_halted: halted, ..Status::default() } + } + + #[test] + fn writable_disk_is_unsafe_while_cpu_runs() { + let r = evaluate(&running(false), &MachineConfig::default()); + assert!(!r.is_empty(), "a live rw disk should warn before force-stop"); + assert!(r.writable_disks.contains(&1)); + } + + #[test] + fn halted_cpu_is_always_safe() { + // After IRIX shuts down (0 MIPS / power-off) stopping can't corrupt a + // disk, so the same config must now evaluate as safe. + let r = evaluate(&running(true), &MachineConfig::default()); + assert!(r.is_empty(), "a halted CPU must be safe to stop"); + } +} diff --git a/iris-gui/src/serial_console.rs b/iris-gui/src/serial_console.rs new file mode 100644 index 0000000..e65c31f --- /dev/null +++ b/iris-gui/src/serial_console.rs @@ -0,0 +1,192 @@ +//! In-app IRIX serial-console viewer. +//! +//! The emulated SGI Indy exposes its serial console (ttyd1) as a loopback TCP +//! server on `127.0.0.1:8881` (see `iris::z85c30`). This viewer connects to it +//! as a client and shows the live console stream, and lets the user type back +//! into it — so the serial console works inside the app without an external +//! terminal. It is also the visible demonstration of the app's network +//! entitlements: the emulator *listens* (network.server) and this viewer +//! *connects* (network.client), both on loopback. +//! +//! A background thread owns the socket, strips inbound telnet negotiation via +//! `iris::telnet::TelnetFilter`, and parks decoded text in a shared buffer. + +use std::io::{Read, Write}; +use std::net::{Shutdown, SocketAddr, TcpStream}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::JoinHandle; +use std::time::Duration; + +use parking_lot::Mutex; + +use iris::telnet::{self, TelnetFilter}; + +/// The loopback address the emulator binds for ttyd1 (IRIX serial console). +pub const SERIAL_ADDR: &str = "127.0.0.1:8881"; +/// Cap on retained scrollback so a long boot doesn't grow the buffer forever. +const MAX_TEXT: usize = 128 * 1024; + +#[derive(Default)] +struct Shared { + /// Decoded console text (telnet stripped, bare CR dropped). + text: String, + /// True once the TCP connection is established. + connected: bool, + /// Set if the connection could not be made / was lost. + error: Option, + /// Bumped on every change so the GUI can decide whether to re-scroll. + seq: u64, +} + +pub struct SerialConsole { + shared: Arc>, + running: Arc, + /// Write half (a clone of the socket) for sending typed input. + write: Arc>>, + worker: Option>, +} + +impl SerialConsole { + /// Connect to the loopback serial console and start streaming. + pub fn connect() -> Self { + let shared = Arc::new(Mutex::new(Shared::default())); + let running = Arc::new(AtomicBool::new(true)); + let write = Arc::new(Mutex::new(None)); + let (s2, r2, w2) = (shared.clone(), running.clone(), write.clone()); + let worker = std::thread::Builder::new() + .name("iris-gui-serial".into()) + .spawn(move || run(s2, r2, w2)) + .expect("spawn serial-console worker"); + Self { shared, running, write, worker: Some(worker) } + } + + /// (text, connected, error, seq) snapshot for rendering. + pub fn snapshot(&self) -> (String, bool, Option, u64) { + let g = self.shared.lock(); + (g.text.clone(), g.connected, g.error.clone(), g.seq) + } + + pub fn clear(&self) { + let mut g = self.shared.lock(); + g.text.clear(); + g.seq = g.seq.wrapping_add(1); + } + + /// Send raw bytes to the guest console (telnet-escaping 0xFF). + pub fn send(&self, bytes: &[u8]) { + let mut esc = Vec::with_capacity(bytes.len()); + for &b in bytes { + telnet::escape_byte(b, &mut esc); + } + if let Some(s) = self.write.lock().as_mut() { + let _ = s.write_all(&esc); + let _ = s.flush(); + } + } +} + +impl Drop for SerialConsole { + fn drop(&mut self) { + self.running.store(false, Ordering::Relaxed); + // Shutting the socket down unblocks the reader's blocking read so the + // worker exits promptly. + if let Some(s) = self.write.lock().take() { + let _ = s.shutdown(Shutdown::Both); + } + if let Some(w) = self.worker.take() { + let _ = w.join(); + } + } +} + +fn run(shared: Arc>, running: Arc, write: Arc>>) { + let addr: SocketAddr = SERIAL_ADDR.parse().expect("valid loopback addr"); + let stream = match TcpStream::connect_timeout(&addr, Duration::from_millis(800)) { + Ok(s) => s, + Err(e) => { + shared.lock().error = Some(format!( + "could not connect to {SERIAL_ADDR}: {e}\nStart the emulator first, then reopen." + )); + return; + } + }; + let wclone = match stream.try_clone() { + Ok(c) => c, + Err(e) => { + shared.lock().error = Some(format!("socket clone failed: {e}")); + return; + } + }; + *write.lock() = Some(wclone); + // Short read timeout so the loop can observe `running` for shutdown. + let _ = stream.set_read_timeout(Some(Duration::from_millis(200))); + { + let mut g = shared.lock(); + g.connected = true; + g.error = None; + g.seq = g.seq.wrapping_add(1); + } + + // Client-side telnet handling: decline negotiation, strip IAC. The guest's + // tty echoes typed characters, so no telnet-layer echo is needed. + let mut filter = TelnetFilter::new_passive(); + let mut buf = [0u8; 2048]; + let mut rstream = stream; + + while running.load(Ordering::Relaxed) { + match rstream.read(&mut buf) { + Ok(0) => break, // EOF — server closed + Ok(n) => { + let mut replies = Vec::new(); + let mut data = Vec::with_capacity(n); + for &b in &buf[..n] { + if let Some(d) = filter.feed(b, &mut replies) { + data.push(d); + } + } + if !replies.is_empty() { + if let Some(s) = write.lock().as_mut() { + let _ = s.write_all(&replies); + let _ = s.flush(); + } + } + if !data.is_empty() { + append_text(&shared, &data); + } + } + Err(ref e) + if e.kind() == std::io::ErrorKind::WouldBlock + || e.kind() == std::io::ErrorKind::TimedOut => + { + continue; + } + Err(_) => break, + } + } + + *write.lock() = None; + let mut g = shared.lock(); + g.connected = false; + g.seq = g.seq.wrapping_add(1); +} + +fn append_text(shared: &Arc>, data: &[u8]) { + let chunk = String::from_utf8_lossy(data); + let mut g = shared.lock(); + for ch in chunk.chars() { + // Drop bare CR; egui handles \n line breaks. + if ch != '\r' { + g.text.push(ch); + } + } + if g.text.len() > MAX_TEXT { + let cut = g.text.len() - MAX_TEXT; + let mut idx = cut; + while !g.text.is_char_boundary(idx) { + idx += 1; + } + g.text.drain(..idx); + } + g.seq = g.seq.wrapping_add(1); +} diff --git a/iris-gui/src/settings.rs b/iris-gui/src/settings.rs index 4f6378d..1b92e6e 100644 --- a/iris-gui/src/settings.rs +++ b/iris-gui/src/settings.rs @@ -1,7 +1,7 @@ use iris::config::MachineConfig; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// GUI-only persisted state. Lives at `~/.config/iris/gui.json`. /// @@ -16,6 +16,12 @@ pub struct GuiSettings { /// egui UI scale (1.0 = default). #[serde(default = "default_ui_scale")] pub ui_scale: f32, + /// Emulated-display (VM screen) magnification: 1.0 = native (1 emulated + /// pixel : 1 logical point). Driven by the View-menu slider (0.5×–3× in 0.5 + /// steps), **independent of `ui_scale`** — scaling the controls doesn't + /// resize the picture, and vice-versa. + #[serde(default = "default_vm_scale")] + pub vm_scale: f32, /// Was the app left in fullscreen mode at last close? #[serde(default)] pub fullscreen: bool, @@ -47,6 +53,89 @@ pub struct GuiSettings { pub bookmarks: BTreeMap>, } +/// Byte offset of the Indy's 6-byte Ethernet MAC inside the NVRAM. The PROM +/// reads the MAC from these *raw bytes* — it is NOT the colon-separated ASCII +/// you type at `setenv` (that's just the human entry form). Reverse-engineered +/// from firmware-written NVRAMs (the SGI OUI 08:00:69 lands exactly here, with +/// zero bytes around it and no adjacent checksum). Like the `console` byte the +/// headless path patches, this is a fixed, PROM-specific offset. +pub const NVRAM_MAC_OFFSET: usize = 0x13a; + +/// The 6 raw MAC bytes from an NVRAM file, if it holds a non-blank one. +pub fn nvram_mac(path: &str) -> Option<[u8; 6]> { + let b = std::fs::read(path).ok()?; + let m: [u8; 6] = b.get(NVRAM_MAC_OFFSET..NVRAM_MAC_OFFSET + 6)?.try_into().ok()?; + let blank = m.iter().all(|&x| x == 0x00) || m.iter().all(|&x| x == 0xff); + (!blank).then_some(m) +} + +/// Whether the NVRAM already has an Ethernet MAC (so IRIX can attach `ec0`). +pub fn nvram_has_mac(path: &str) -> bool { + nvram_mac(path).is_some() +} + +/// Deterministic SGI-OUI MAC bytes (`08:00:69:xx:xx:xx`) from `seed` (machine +/// name) — stable per machine. Uniqueness across instances doesn't matter; each +/// runs on its own isolated NAT. +pub fn generate_mac_bytes(seed: &str) -> [u8; 6] { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + seed.hash(&mut h); + let v = h.finish(); + [0x08, 0x00, 0x69, (v >> 16) as u8, (v >> 8) as u8, v as u8] +} + +/// Human form `08:00:69:xx:xx:xx` for display/logging. +pub fn mac_to_string(m: [u8; 6]) -> String { + m.iter().map(|b| format!("{b:02x}")).collect::>().join(":") +} + +/// Write 6 MAC bytes into an existing NVRAM file at [`NVRAM_MAC_OFFSET`], +/// touching only those 6 bytes so the boot env is preserved. Backs the file up +/// to `.bak` first. Returns Ok(false) if there's no NVRAM file yet (a +/// bare MAC with no DS1386 structure would be useless) or it's too small. +pub fn write_nvram_mac(path: &str, mac: [u8; 6]) -> std::io::Result { + let Ok(mut bytes) = std::fs::read(path) else { return Ok(false); }; + if bytes.len() < NVRAM_MAC_OFFSET + 6 { + return Ok(false); + } + let _ = std::fs::copy(path, format!("{path}.bak")); // best-effort backup + bytes[NVRAM_MAC_OFFSET..NVRAM_MAC_OFFSET + 6].copy_from_slice(&mac); + std::fs::write(path, &bytes)?; + Ok(true) +} + +/// Default NVRAM image baked into the binary: the repo's known-good NVRAM (boot +/// env present) with the MAC zeroed. Lets a fresh install — especially the +/// bundled `.app`, which has nothing in its working dir to migrate — boot with +/// proper PROM env, while the auto-write fills in a per-machine MAC. +pub const DEFAULT_NVRAM: &[u8] = include_bytes!("../assets/nvram-default.bin"); + +/// Write the embedded default NVRAM to `path` if there's no (non-empty) file +/// there yet. Returns true if it seeded one. Creates the parent dir as needed. +pub fn ensure_nvram_seeded(path: &str) -> bool { + if std::fs::metadata(path).map(|m| m.len() > 0).unwrap_or(false) { + return false; + } + if let Some(parent) = Path::new(path).parent() { + let _ = std::fs::create_dir_all(parent); + } + std::fs::write(path, DEFAULT_NVRAM).is_ok() +} + +/// Overwrite the NVRAM at `path` with the embedded default (boot env, blank +/// MAC) — backs the current file up to `.bak` first. Used by the +/// "Reset NVRAM / fresh PRAM" menu action. +pub fn reset_nvram(path: &str) -> std::io::Result<()> { + if std::fs::metadata(path).map(|m| m.len() > 0).unwrap_or(false) { + let _ = std::fs::copy(path, format!("{path}.bak")); + } + if let Some(parent) = Path::new(path).parent() { + let _ = std::fs::create_dir_all(parent); + } + std::fs::write(path, DEFAULT_NVRAM) +} + /// Allowed UI-scale range, shared by the View-menu slider, the Ctrl +/-/0 /// keyboard zoom, and the load-time clamp so a stale persisted value can never /// put the UI into a state the slider can't represent (which egui would then @@ -55,11 +144,89 @@ pub const UI_SCALE_MIN: f32 = 1.0; pub const UI_SCALE_MAX: f32 = 3.0; pub const UI_SCALE_DEFAULT: f32 = 1.25; +/// Allowed VM-screen scale range and step for the View-menu slider. ¼× steps +/// (0.5, 0.75, 1.0, 1.25, …) give finer control; on a HiDPI (2×) display the +/// half-integer steps (0.5, 1.0, 1.5, …) are pixel-crisp and the ¼ steps in +/// between are bilinear-smoothed — the footer readout tags which is which. +pub const VM_SCALE_MIN: f32 = 0.5; +pub const VM_SCALE_MAX: f32 = 3.0; +pub const VM_SCALE_STEP: f64 = 0.25; +pub const VM_SCALE_DEFAULT: f32 = 1.0; + +/// First-launch window size in logical points. Sized to match the *running* +/// window for the standard 1280×1024 display so the picture doesn't visibly +/// jump when you press Start: with the left control column (~186 pt) and no +/// top/bottom chrome, the running size at the default UI scale is ≈ the native +/// 1280×1024 display plus the column width. The launcher fit (see `main`) and +/// the on-Start snap still refine this — clamping to the monitor on smaller +/// screens — so it's only the initial size and the fallback when the monitor +/// size is unknown. Once a real size is persisted to `gui.json`, that's used. +pub const WINDOW_DEFAULT_SIZE: [f32; 2] = [1512.0, 1024.0]; + fn default_ui_scale() -> f32 { UI_SCALE_DEFAULT } +fn default_vm_scale() -> f32 { VM_SCALE_DEFAULT } impl GuiSettings { pub fn config_path() -> Option { - dirs::config_dir().map(|d| d.join("iris").join("gui.json")) + Self::data_dir().map(|d| d.join("gui.json")) + } + + /// Stable per-user directory for GUI state (gui.json, nvram.bin, …). The OS + /// maps this into the sandbox container automatically on the App Store + /// build, so the *same* code resolves the right place for `cargo run` and + /// the bundled app alike. + pub fn data_dir() -> Option { + dirs::config_dir().map(|d| d.join("iris")) + } + + /// Default absolute NVRAM path: `/nvram.bin`. Absolute on purpose + /// — a relative `nvram.bin` resolves against the process's working + /// directory, which differs between `cargo run` (repo root) and a bundled + /// `.app`, silently loading different (often blank, MAC-less) NVRAMs. Anchor + /// it once and every launch shares one NVRAM. + pub fn default_nvram_path() -> String { + Self::data_dir() + .map(|d| d.join("nvram.bin").to_string_lossy().into_owned()) + .unwrap_or_else(|| "nvram.bin".to_string()) + } + + /// Managed directory for newly-created disk images: `/disks`. + /// Absolute and writable in every launch context — the OS maps it into the + /// sandbox container on the App Store build, so creating a disk here needs + /// no permission prompt. Users can still pick another location. + pub fn disks_dir() -> Option { + Self::data_dir().map(|d| d.join("disks")) + } + + /// Default absolute path for a new SCSI disk image: `/scsiN.raw`. + pub fn default_disk_path(scsi_id: u8) -> String { + Self::disks_dir() + .map(|d| d.join(format!("scsi{scsi_id}.raw")).to_string_lossy().into_owned()) + .unwrap_or_else(|| format!("scsi{scsi_id}.raw")) + } + + /// Anchor a machine's NVRAM path to [`data_dir`] if it's relative (the + /// legacy default was a bare `"nvram.bin"`). Best-effort: if the anchored + /// file doesn't exist yet but the old cwd-relative one does, copy it over so + /// the PROM env (boot settings, any MAC) carries forward instead of starting + /// blank. Idempotent — absolute paths are left untouched. + pub fn migrate_nvram_path(nvram: &mut String) { + if !nvram.is_empty() && Path::new(&nvram).is_absolute() { + return; + } + let Some(dir) = Self::data_dir() else { return; }; + let _ = std::fs::create_dir_all(&dir); + let leaf = Path::new(nvram.as_str()) + .file_name() + .and_then(|s| s.to_str()) + .filter(|s| !s.is_empty()) + .unwrap_or("nvram.bin"); + let dst = dir.join(leaf); + let src = PathBuf::from(nvram.as_str()); // relative to cwd + if !dst.exists() && !nvram.is_empty() && src.exists() { + let _ = std::fs::copy(&src, &dst); + } + *nvram = dst.to_string_lossy().into_owned(); } pub fn load() -> Self { @@ -76,6 +243,17 @@ impl GuiSettings { } else { s.ui_scale.min(UI_SCALE_MAX) }; + s.vm_scale = if !s.vm_scale.is_finite() || s.vm_scale < VM_SCALE_MIN { + VM_SCALE_DEFAULT + } else { + s.vm_scale.min(VM_SCALE_MAX) + }; + // Anchor every machine's NVRAM to the stable data dir so all launch + // methods share one file (the persisted path becomes absolute on the + // next save). + for m in s.machines.values_mut() { + Self::migrate_nvram_path(&mut m.nvram); + } s } diff --git a/rules/gui/gui_mouse_integration.md b/rules/gui/gui_mouse_integration.md index b84a2be..16e4669 100644 --- a/rules/gui/gui_mouse_integration.md +++ b/rules/gui/gui_mouse_integration.md @@ -1,9 +1,9 @@ -# GUI mouse integration — current approach, and the Snow absolute-mouse pattern +# GUI mouse integration — current approach, and the classic-Mac absolute-mouse pattern Status: reference / design analysis. Captures why iris-gui uses pointer **capture** for the framebuffer, and why the seamless absolute-mouse trick used -by the `snow` Macintosh emulator does **not** port to IRIX without significant -new machinery. Read this before proposing "make the mouse seamless like snow." +by some classic Macintosh emulators does **not** port to IRIX without +significant new machinery. Read this before proposing "make the mouse seamless." ## Current iris-gui approach: capture (grab + hide) @@ -36,25 +36,22 @@ host cursor can't get stuck hidden. > warp-to-center + relative deltas (`src/ui.rs:532`), *not* absolute > positioning. There is no hidden absolute backend to tap. -## What `snow` does (the absolute pattern) +## The absolute pattern (classic Mac OS) -`snow` (sibling repo `../snow`, a classic Macintosh emulator) gets seamless, -capture-free, 1:1 mouse alignment via an **absolute** mode that bypasses the -emulated mouse hardware entirely: +Some classic Macintosh emulators get seamless, capture-free, 1:1 mouse +alignment via an **absolute** mode that bypasses the emulated mouse hardware +entirely. It works because **classic Mac OS exposes a stable, documented, +memory-mapped cursor position you may overwrite, and cooperatively re-reads +it**: -- `mouse_update_abs(x, y)` writes the host cursor position directly into classic - Mac OS **low-memory globals**: `MTemp` (MouseTemp) and `RawMouse`, then sets - the `CrsrNew` flag. See `core/src/mac/compact/bus.rs:476` (and the Mac II - variant in `core/src/mac/macii/bus.rs`). +- The host cursor position is written directly into classic Mac OS **low-memory + globals** — `MTemp` (MouseTemp) and `RawMouse` — and the `CrsrNew` flag is + set. - Mac OS polls those globals every tick and "jumps" its cursor to the new - position. The ADB mouse (`core/src/mac/adb/mouse.rs`) stays relative but is - sidestepped in absolute mode. + position. The emulated ADB mouse stays relative but is sidestepped in + absolute mode. - The frontend exposes a `MouseMode { Absolute, RelativeHw, Disabled }` seam and - calls `update_mouse(abs_p, rel_p)` (`frontend_egui/src/emulator.rs:434`), - dispatching `MouseUpdateAbsolute { x, y }` vs `MouseUpdateRelative { .. }`. - -It works because **classic Mac OS exposes a stable, documented, memory-mapped -cursor position you may overwrite, and cooperatively re-reads it.** + dispatches an absolute vs. relative update per event. ## Why it does not port to IRIS/IRIX cheaply @@ -70,7 +67,7 @@ IRIX has no equivalent of that mechanism: paths are the input protocol (absolute valuators / XInput) or `XWarpPointer`/XTEST — none of which the emulated SGI PS/2-style mouse exposes. -## What a Snow-like absolute mode would actually require here +## What an absolute mode would actually require here One of: @@ -88,8 +85,9 @@ None of these is a small port. ## Recommendation Capture is the correct, standard approach for an X11 guest — it is what -SGI/Unix emulators do, and what snow itself falls back to (`RelativeHw`). The -one piece genuinely worth borrowing from snow is its clean frontend seam: a -`MouseMode` enum + `update_mouse(abs, rel)`. Adopting that abstraction now -(even with only relative/capture wired up) would make options 1 or 2 drop-in -later, without committing to the absolute backend today. +SGI/Unix emulators do, and what the classic-Mac absolute approach itself falls +back to (a relative-hardware mode) when absolute isn't available. The one piece +genuinely worth borrowing is the clean frontend seam: a `MouseMode` enum + +`update_mouse(abs, rel)`. Adopting that abstraction now (even with only +relative/capture wired up) would make options 1 or 2 drop-in later, without +committing to the absolute backend today. diff --git a/rules/macos/appstore-private-api.md b/rules/macos/appstore-private-api.md new file mode 100644 index 0000000..eb92367 --- /dev/null +++ b/rules/macos/appstore-private-api.md @@ -0,0 +1,64 @@ +# Mac App Store rejects winit's private SkyLight blur API (`CGSSetWindowBackgroundBlurRadius`) + +**Symptom.** App Store review rejects the `iris-gui` binary under **Guideline +2.5.1 (Performance — Software Requirements)**: + +> The app uses or references the following non-public or deprecated APIs: +> Contents/MacOS/iris-gui — Symbols: `_CGSSetWindowBackgroundBlurRadius` + +**Root cause.** `eframe 0.29` pulls in `winit 0.30` for window creation. winit's +macOS backend (`platform_impl/macos/window_delegate.rs::set_blur`) calls the +private SkyLight APIs `CGSSetWindowBackgroundBlurRadius` / +`CGSMainConnectionID`, declared in `platform_impl/macos/ffi.rs`. The call site +is reached unconditionally during window init (`set_blur(attrs.blur)`), so the +import lands in the linked binary **even though iris-gui never requests blur** +(`egui::ViewportBuilder` leaves `blur = false`). Apple's static binary scan +flags the imported symbol regardless of whether it's called at runtime. + +Confirm with: + +``` +nm -u target/release/iris-gui | grep -i CGSSetWindowBackgroundBlurRadius +``` + +`U _CGSSetWindowBackgroundBlurRadius` = present (rejected). No output = clean. +(`_CGShieldingWindowLevel` also shows up but is a **public** CoreGraphics API — +Apple does not flag it.) + +**Fix.** Vendor a patched winit and override it via `[patch.crates-io]`: + +- `third_party/winit-0.30.13/` — copy of the crate with: + - `set_blur` stubbed to a no-op (no `ffi::CGS…` calls), + - the two private `extern` declarations removed from `ffi.rs` (and the + now-unused `NSInteger` / `AnyObject` imports dropped). +- Root `Cargo.toml`: `[patch.crates-io] winit = { path = "third_party/winit-0.30.13" }`. + +Only the `0.30.x` requirement (eframe → egui-winit → glutin-winit) matches the +patch. `iris`'s own `winit 0.29` dependency is the keyboard `KeyCode` type only +and creates no window inside `iris-gui`, so its `set_blur` is dead-stripped — +patching just the 0.30 copy removes the symbol entirely (verified with `nm -u`). + +**Two-version gotcha.** Cargo allows only one `[patch.crates-io]` entry per +crate name, so you cannot patch both 0.29.15 and 0.30.13. That's fine here — +only the eframe (0.30) window code reaches `set_blur`. If a future change makes +`iris` create a winit-0.29 window inside the GUI process, re-check `nm -u`; +you'd then have to unify on a single winit version before patching. + +**When bumping eframe/winit:** re-vendor the matching winit version, re-apply +the two-edit patch, and re-run the `nm -u` check before submitting. + +## Upstream status (don't file a new bug — already tracked) + +- winit issue **#4205** "_CGSSetWindowBackgroundBlurRadius non-public or + deprecated API" — open, milestone **winit 0.31.0**. +- winit PR **#4541** "macOS: Feature-gate `CGSSetWindowBackgroundBlurRadius`" — + open/in-progress. Puts the call behind a `private-apple-apis` Cargo feature + (off by default → symbol absent unless opted in). Resolves #4205. +- #4574 (dup App Store rejection report) closed as duplicate; #4538 (remove it + outright) abandoned. + +**Migration:** once IRIS moves to a winit (≥0.31) that ships the feature gate — +which only happens after eframe bumps to a winit-0.31 release and we bump eframe +— delete `third_party/winit-0.30.13/` and the `[patch.crates-io]`, and just make +sure the `private-apple-apis` feature stays disabled (and that eframe doesn't +enable it). Re-run the `nm -u` check to confirm. diff --git a/src/camera.rs b/src/camera.rs index d2c1caf..dd58697 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -15,6 +15,7 @@ //! returns a solid black field so VINO DMA still gets coherent bytes. use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use std::time::Instant; @@ -47,7 +48,11 @@ pub(crate) struct Shared { pub struct CameraSource { standard: VideoStandard, shared: Arc>, - _worker: thread::JoinHandle<()>, + /// Cleared on drop to stop the capture loop so the host camera is released + /// (its indicator light turns off) rather than the worker running until the + /// process exits. The backend polls this between frames. + running: Arc, + worker: Option>, } impl CameraSource { @@ -69,13 +74,27 @@ impl CameraSource { capture_res: None, })); let s2 = shared.clone(); + let running = Arc::new(AtomicBool::new(true)); + let r2 = running.clone(); let worker = thread::Builder::new() .name("iris-camera".into()) - .spawn(move || backend::capture_loop(s2, frame_w, frame_h, camera_index)) + .spawn(move || backend::capture_loop(s2, frame_w, frame_h, camera_index, r2)) .map_err(|e| format!("camera worker spawn failed: {}", e))?; - Ok(Self { standard, shared, _worker: worker }) + Ok(Self { standard, shared, running, worker: Some(worker) }) + } +} + +impl Drop for CameraSource { + fn drop(&mut self) { + self.running.store(false, Ordering::Relaxed); + if let Some(w) = self.worker.take() { + // The backend exits within one frame interval of seeing `running` + // clear, then closes the camera stream. Join so the device is fully + // released before we return (e.g. before a re-open). + let _ = w.join(); + } } } diff --git a/src/camera_nokhwa.rs b/src/camera_nokhwa.rs index 6fa2731..a36fd09 100644 --- a/src/camera_nokhwa.rs +++ b/src/camera_nokhwa.rs @@ -1,6 +1,7 @@ //! nokhwa-based capture loop (macOS AVFoundation, Windows MediaFoundation). use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use std::time::Duration; @@ -9,7 +10,7 @@ use parking_lot::Mutex; use super::{Shared, downscale_yuyv_to_uyvy, split_fields}; pub(super) fn capture_loop(shared: Arc>, frame_w: u32, frame_h: u32, - camera_index: u32) { + camera_index: u32, running: Arc) { use nokhwa::pixel_format::YuyvFormat; use nokhwa::utils::{ CameraFormat, CameraIndex, FrameFormat, RequestedFormat, RequestedFormatType, Resolution, @@ -49,7 +50,7 @@ pub(super) fn capture_loop(shared: Arc>, frame_w: u32, frame_h: u3 eprintln!("camera: streaming at {}×{} → downscale to {}×{}", sw, sh, frame_w, frame_h); shared.lock().capture_res = Some((sw, sh)); - loop { + while running.load(Ordering::Relaxed) { let buf = match cam.frame() { Ok(b) => b, Err(e) => { diff --git a/src/camera_v4l.rs b/src/camera_v4l.rs index 1494752..38b460a 100644 --- a/src/camera_v4l.rs +++ b/src/camera_v4l.rs @@ -13,6 +13,7 @@ //! start an mmap stream, and pull frames directly. use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use std::time::Duration; @@ -81,12 +82,13 @@ fn best_yuyv_resolution(dev: &Device) -> Option<(u32, u32)> { } pub(super) fn capture_loop(shared: Arc>, frame_w: u32, frame_h: u32, - camera_index: u32) { + camera_index: u32, running: Arc) { // Outer retry loop: reopens the device from scratch after stream loss or // hot-plug. Re-resolves the device index each time so a swap (e.g. Logitech // → Huddly GO) picks up the new camera's node without restarting iris. let mut open_attempts = 0u32; 'outer: loop { + if !running.load(Ordering::Relaxed) { return; } let cur_idx = match resolve_device_index(camera_index) { Some(i) => i, None => { @@ -150,6 +152,7 @@ pub(super) fn capture_loop(shared: Arc>, frame_w: u32, frame_h: u3 open_attempts = 0; loop { + if !running.load(Ordering::Relaxed) { return; } let (data, _meta) = match stream.next() { Ok(f) => f, Err(e) => { diff --git a/src/config.rs b/src/config.rs index 6383173..85564dc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -269,6 +269,13 @@ pub struct MachineConfig { #[serde(default = "default_scroll_pixels_per_line")] pub mouse_scroll_pixels_per_line: f64, + /// Lock the window's aspect ratio to the emulated display (picture + + /// status bar) while resizing, so it fills the window without letterbox + /// bars. Set to false if you have a non-standard monitor and prefer free + /// resizing — the display is then letterboxed to fit. Default: true. + #[serde(default = "default_lock_aspect_ratio")] + pub lock_aspect_ratio: bool, + /// Optional file path that will receive every byte emitted on ttyd1 /// (the IRIX serial console) in `--ci` mode. Append-only. Useful for /// keeping a continuously-updated transcript of the install or test run. @@ -296,6 +303,7 @@ pub struct MachineConfig { fn default_ci_socket() -> String { "/tmp/iris.sock".to_string() } fn default_scroll_pixels_per_line() -> f64 { 40.0 } +fn default_lock_aspect_ratio() -> bool { true } fn default_prom() -> String { "prom.bin".to_string() @@ -352,6 +360,7 @@ impl Default for MachineConfig { serial_log: None, vino: VinoConfig::default(), mouse_scroll_pixels_per_line: default_scroll_pixels_per_line(), + lock_aspect_ratio: default_lock_aspect_ratio(), } } } diff --git a/src/jit/compiler.rs b/src/jit/compiler.rs index 90fd898..400ecac 100644 --- a/src/jit/compiler.rs +++ b/src/jit/compiler.rs @@ -275,7 +275,6 @@ impl BlockCompiler { let old_gpr = gpr; let old_hi = hi; let old_lo = lo; - let old_modified = modified_gprs; let (_, delay_d) = &instrs[idx]; let delay_pc = block_pc.wrapping_add(idx as u64 * 4); let delay_result = emit_instruction( @@ -297,7 +296,9 @@ impl BlockCompiler { gpr = old_gpr; hi = old_hi; lo = old_lo; - modified_gprs = old_modified; + // modified_gprs intentionally not restored: the + // post-loop flush stores every GPR (all_modified + // = 0xFFFFFFFE), so its value is dead past here. compiled_count -= 1; break; } diff --git a/src/machine.rs b/src/machine.rs index df1eaa0..a00fbad 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -694,6 +694,13 @@ impl Machine { self.ci_serial.clone() } + /// Type bytes at the IRIX serial console (tty1) in-process, without any + /// loopback TCP client. Used by the GUI to send `halt\n` for a clean + /// shutdown so the feature doesn't depend on the serial server socket. + pub fn inject_serial_console(&self, bytes: &[u8]) { + self.hpc3.ioc().scc().inject_b(bytes); + } + /// CPU thread, started explicitly by the CI `start` command or by /// `ci_restore`. In `--ci` mode the CPU is not autostarted in `start()` /// — the harness drives startup via `restore`. @@ -701,6 +708,14 @@ impl Machine { self.cpu.start(); } + /// Whether the CPU thread is currently executing. Goes false when the CPU + /// is stopped — including the soft power-off path (a guest `poweroff` makes + /// the machine-events thread call `stop()`), so an embedder can tell the + /// guest has shut down without subscribing to machine events. + pub fn cpu_is_running(&self) -> bool { + self.cpu.is_running() + } + /// Step the CPU `n` instructions in-line on the calling thread, with all /// peripheral threads stopped so the CPU sees no external interrupts. /// Used by Phase 3.3 snapshot determinism validator. diff --git a/src/main.rs b/src/main.rs index 8bcc31b..7c41153 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ fn main() { let (mut cfg, scale) = load_config(); let scroll_pixels_per_line = cfg.mouse_scroll_pixels_per_line; + let lock_aspect_ratio = cfg.lock_aspect_ratio; let headless = cfg.headless; let gdb_port = cfg.gdb_port; let ci_enabled = cfg.ci; @@ -104,7 +105,7 @@ fn main() { use winit::event_loop::EventLoop; let event_loop = EventLoop::new().unwrap(); let rex3 = machine.get_rex3().expect("rex3 must be present in non-headless mode"); - let ui = Ui::new(machine.get_ps2(), rex3, machine.get_timer_manager(), &event_loop, scale, scroll_pixels_per_line); + let ui = Ui::new(machine.get_ps2(), rex3, machine.get_timer_manager(), &event_loop, scale, scroll_pixels_per_line, lock_aspect_ratio); ui.run(event_loop); } diff --git a/src/ui.rs b/src/ui.rs index 3359754..2ef35fb 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -55,6 +55,9 @@ struct GlRenderer { gl_config: glutin::config::Config, window_size: Arc>>, scale_snap: Arc>>, + // Current emulated display resolution (width, height), published for the + // event thread so it can lock the window to the right aspect ratio. + display_res: Arc>, state: Option, compositor: Box, use_gl_compositor: bool, @@ -402,6 +405,8 @@ impl Renderer for GlRenderer { self.current_h = height; self.current_win_w = win_w; self.current_win_h = win_h; + // Publish the display resolution for the event thread's aspect lock. + *self.display_res.lock() = (width as u32, height as u32); // UV coords into 2048×1024 texture let max_u = width as f32 / 2048.0; let max_v_main = height as f32 / 1024.0; @@ -481,6 +486,10 @@ impl Renderer for GlRenderer { } fn resize(&mut self, width: usize, height: usize) { + // Publish the new resolution *before* the snap below, so the event + // thread's aspect lock sees it when it handles the resulting Resized + // event (otherwise it would re-fit the window to the stale aspect). + *self.display_res.lock() = (width as u32, height as u32); // On display resolution change, snap window to 1x of the new resolution. let _ = self.window.request_inner_size(winit::dpi::PhysicalSize::new( width as u32, @@ -535,15 +544,20 @@ pub struct Ui { window: Arc, window_size: Arc>>, scale_snap: Arc>>, + display_res: Arc>, timer_manager: Arc, initial_scale: u32, scroll_pixels_per_line: f64, + lock_aspect_ratio: bool, } impl Ui { - pub fn new(ps2: Arc, rex3: Arc, timer_manager: Arc, event_loop: &EventLoop<()>, scale: u32, scroll_pixels_per_line: f64) -> Self { - let w = 1024 * scale; - let h = (768 + STATUS_BAR_HEIGHT as u32) * scale; + pub fn new(ps2: Arc, rex3: Arc, timer_manager: Arc, event_loop: &EventLoop<()>, scale: u32, scroll_pixels_per_line: f64, lock_aspect_ratio: bool) -> Self { + // The Indy's default video mode is 1280×1024; open the window at that + // size (plus the status bar). The renderer snaps to the real resolution + // via resize() once the PROM/IRIX programs its actual mode. + let w = 1280 * scale; + let h = (1024 + STATUS_BAR_HEIGHT as u32) * scale; let window_builder = WindowBuilder::new() .with_title(crate::machine::emulator_name()) .with_resizable(true) @@ -568,12 +582,16 @@ impl Ui { let window = Arc::new(window.unwrap()); let window_size = Arc::new(Mutex::new(None)); let scale_snap = Arc::new(Mutex::new(None)); + // Seed with the Indy's default 1280×1024; the render thread republishes + // the real resolution on the first frame and on any mode change. + let display_res = Arc::new(Mutex::new((1280u32, 1024u32))); let renderer = GlRenderer { window: window.clone(), gl_config, window_size: window_size.clone(), scale_snap: scale_snap.clone(), + display_res: display_res.clone(), state: None, compositor: Box::new(GlCompositor::new()), use_gl_compositor: true, @@ -585,16 +603,22 @@ impl Ui { *rex3.renderer.lock() = Some(Box::new(renderer)); - Self { ps2, rex3, window, window_size, scale_snap, timer_manager, initial_scale: scale, scroll_pixels_per_line } + Self { ps2, rex3, window, window_size, scale_snap, display_res, timer_manager, initial_scale: scale, scroll_pixels_per_line, lock_aspect_ratio } } /// Run the UI event loop (blocks the current thread) pub fn run(self, event_loop: EventLoop<()>) { - let Ui { ps2, rex3, window, window_size, scale_snap, timer_manager, initial_scale, scroll_pixels_per_line } = self; + let Ui { ps2, rex3, window, window_size, scale_snap, display_res, timer_manager, initial_scale, scroll_pixels_per_line, lock_aspect_ratio } = self; let scale = initial_scale; let mut mouse_grabbed = false; let mut rctrl_held = false; + // Last window size we accepted, used to tell which edge the user is + // dragging when locking the aspect ratio. + let mut last_win_size = { + let s = window.inner_size(); + (s.width, s.height) + }; let mouse_delta = Arc::new(Mutex::new(MouseDelta { accum: (0.0, 0.0), wheel: 0.0, buttons: 0 })); { @@ -618,7 +642,31 @@ impl Ui { WindowEvent::CloseRequested => { elwt.exit() }, WindowEvent::Resized(size) => { if size.width != 0 && size.height != 0 { - *window_size.lock() = Some((size.width, size.height)); + let mut new_size = (size.width, size.height); + // Lock the window to the display's aspect ratio so the + // picture fills it without letterbox bars. Skipped when + // fullscreen or maximized (aspect can't be honoured there) + // and when disabled by config. + if lock_aspect_ratio + && window.fullscreen().is_none() + && !window.is_maximized() + { + let (dw, dh) = *display_res.lock(); + if let Some(fixed) = Self::aspect_fit( + size.width, size.height, last_win_size, dw, dh) + { + new_size = match window.request_inner_size( + winit::dpi::PhysicalSize::new(fixed.0, fixed.1)) + { + // Some => applied synchronously, no further + // Resized event; use the actual granted size. + Some(actual) => (actual.width, actual.height), + None => fixed, + }; + } + } + last_win_size = new_size; + *window_size.lock() = Some(new_size); } } WindowEvent::KeyboardInput { event, .. } => { @@ -695,6 +743,33 @@ impl Ui { ps2.push_mouse_input(buttons, dx, dy, dz); } + /// Adjust an incoming window size to match the emulated display's aspect + /// ratio (display width : display height + status bar). Whichever axis the + /// user is actively dragging — the one that moved most from `prev` — is + /// kept, and the other is derived from it. Returns `None` when the size is + /// already within 1 px of the target (no correction needed), which keeps + /// the follow-up resize from oscillating. + fn aspect_fit(win_w: u32, win_h: u32, prev: (u32, u32), disp_w: u32, disp_h: u32) + -> Option<(u32, u32)> + { + if disp_w == 0 || disp_h == 0 { return None; } + let content_h = disp_h + STATUS_BAR_HEIGHT as u32; + // round(a * b / c) in u64 to avoid overflow/bias. + let muldiv = |a: u32, b: u32, c: u32| -> u32 { + ((a as u64 * b as u64 + c as u64 / 2) / c as u64) as u32 + }; + let (pw, ph) = prev; + if win_w.abs_diff(pw) >= win_h.abs_diff(ph) { + // Width is the driven axis: derive height from it. + let target_h = muldiv(win_w, content_h, disp_w).max(1); + if target_h.abs_diff(win_h) <= 1 { None } else { Some((win_w, target_h)) } + } else { + // Height is the driven axis: derive width from it. + let target_w = muldiv(win_h, disp_w, content_h).max(1); + if target_w.abs_diff(win_w) <= 1 { None } else { Some((target_w, win_h)) } + } + } + fn handle_keyboard(ps2: &Ps2Controller, rex3: &Rex3, scale_snap: &Mutex>, input: KeyEvent, grabbed: &mut bool, rctrl_held: &mut bool, window: &Window) { @@ -744,3 +819,49 @@ impl Ui { } } } + +#[cfg(test)] +mod tests { + use super::*; + + // Indy default mode. Content aspect = 1280 : (1024 + 16) = 1280 : 1040. + const DW: u32 = 1280; + const DH: u32 = 1024; + + #[test] + fn dragging_width_derives_height() { + // Width grows from 1280 to 2560; height untouched. Expect height locked + // to the content ratio: 2560 * 1040 / 1280 = 2080. + let fixed = Ui::aspect_fit(2560, 1040, (1280, 1040), DW, DH); + assert_eq!(fixed, Some((2560, 2080))); + } + + #[test] + fn dragging_height_derives_width() { + // Height grows from 1040 to 2080; width untouched. Expect width locked: + // 2080 * 1280 / 1040 = 2560. + let fixed = Ui::aspect_fit(1280, 2080, (1280, 1040), DW, DH); + assert_eq!(fixed, Some((2560, 2080))); + } + + #[test] + fn already_locked_is_noop() { + // A size already on-ratio needs no correction (prevents oscillation on + // the follow-up Resized event after we apply a fix). + assert_eq!(Ui::aspect_fit(2560, 2080, (2560, 2080), DW, DH), None); + assert_eq!(Ui::aspect_fit(1280, 1040, (1280, 1040), DW, DH), None); + } + + #[test] + fn within_one_pixel_tolerance() { + // 1 px off the exact ratio is accepted as-is (no visible letterbox). + assert_eq!(Ui::aspect_fit(2560, 2079, (2560, 2079), DW, DH), None); + assert_eq!(Ui::aspect_fit(2560, 2081, (2560, 2081), DW, DH), None); + } + + #[test] + fn zero_resolution_is_noop() { + // Guard against a divide-by-zero before the first frame publishes a res. + assert_eq!(Ui::aspect_fit(800, 600, (800, 600), 0, 0), None); + } +} diff --git a/src/z85c30.rs b/src/z85c30.rs index ef4c0d4..0032100 100644 --- a/src/z85c30.rs +++ b/src/z85c30.rs @@ -584,6 +584,12 @@ pub struct Z85c30 { // so `Z85c30` stays `Clone` and the swap is thread-safe. backend_a: Arc>>, backend_b: Arc>>, + // In-process injection queue for channel B (tty1, the IRIX serial console). + // Bytes queued via `inject_b` are delivered to the guest by the channel-B + // RX thread ahead of any socket input, so a host action (e.g. the GUI's + // "Send IRIX halt") can type at the console without opening a loopback TCP + // client. Independent of whichever backend is installed. + inject_b: Arc>>, running: Arc, threads: Arc>>>, } @@ -628,6 +634,7 @@ impl Z85c30 { channel_b: Arc::new((Mutex::new(Channel::new("B", ip_b, ip_a, callback)), Condvar::new())), backend_a: Arc::new(Mutex::new(backend_a)), backend_b: Arc::new(Mutex::new(backend_b)), + inject_b: Arc::new(Mutex::new(VecDeque::new())), running: Arc::new(AtomicBool::new(false)), threads: Arc::new(Mutex::new(Vec::new())), } @@ -654,6 +661,16 @@ impl Z85c30 { self.backend_b.lock().clone() } + /// Queue host bytes to be delivered to channel B (tty1, the IRIX serial + /// console) as if typed at the console — entirely in-process, with no + /// loopback TCP client. The bytes ride the same RX path as socket input + /// (FIFO backpressure + baud pacing), so the guest sees them identically. + /// Used by the GUI's "Send IRIX halt" for a clean shutdown that doesn't + /// depend on the serial server socket. + pub fn inject_b(&self, data: &[u8]) { + self.inject_b.lock().extend(data.iter().copied()); + } + pub fn read_a_control(&self) -> u8 { let mut a = self.channel_a.0.lock(); if a.reg_ptr == 2 { @@ -871,6 +888,9 @@ impl Device for Z85c30 { let rx_channel = channel_arc.clone(); let rx_backend = backend.clone(); let running = self.running.clone(); + // Only channel B (the IRIX serial console) accepts in-process + // injection; channel A has no queue. + let rx_inject = if i == 1 { Some(self.inject_b.clone()) } else { None }; threads.push(thread::Builder::new().name(format!("SCC-RX-{}", ch_name)).spawn(move || { let mut last_rx_time = Instant::now(); @@ -886,12 +906,18 @@ impl Device for Z85c30 { while running.load(Ordering::Relaxed) { let mut byte = match pending.take() { Some(b) => b, - None => match rx_backend.recv_byte() { - Ok(b) => b, - Err(_) => { - thread::sleep(Duration::from_millis(10)); - continue; - } + // In-process injection (channel B) takes priority over + // socket input, so a queued "halt\n" is delivered even + // when no TCP client is attached. + None => match rx_inject.as_ref().and_then(|q| q.lock().pop_front()) { + Some(b) => b, + None => match rx_backend.recv_byte() { + Ok(b) => b, + Err(_) => { + thread::sleep(Duration::from_millis(10)); + continue; + } + }, }, }; if byte == 0x05 { diff --git a/third_party/winit-0.30.13/.cargo-ok b/third_party/winit-0.30.13/.cargo-ok new file mode 100644 index 0000000..5f8b795 --- /dev/null +++ b/third_party/winit-0.30.13/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/third_party/winit-0.30.13/.cargo_vcs_info.json b/third_party/winit-0.30.13/.cargo_vcs_info.json new file mode 100644 index 0000000..6a63283 --- /dev/null +++ b/third_party/winit-0.30.13/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "e9809ef54b18499bb4f2cac945719ecc2a61061b" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/third_party/winit-0.30.13/Cargo.toml b/third_party/winit-0.30.13/Cargo.toml new file mode 100644 index 0000000..4bfded8 --- /dev/null +++ b/third_party/winit-0.30.13/Cargo.toml @@ -0,0 +1,553 @@ +# 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.70.0" +name = "winit" +version = "0.30.13" +authors = [ + "The winit contributors", + "Pierre Krieger ", +] +build = "build.rs" +include = [ + "/build.rs", + "/docs", + "/examples", + "/FEATURES.md", + "/LICENSE", + "/src", + "!/src/platform_impl/web/script", + "/src/platform_impl/web/script/**/*.min.js", + "/tests", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Cross-platform window creation library." +documentation = "https://docs.rs/winit" +readme = "README.md" +keywords = ["windowing"] +categories = ["gui"] +license = "Apache-2.0" +repository = "https://github.com/rust-windowing/winit" + +[package.metadata.docs.rs] +features = [ + "rwh_04", + "rwh_05", + "rwh_06", + "serde", + "mint", + "android-native-activity", +] +targets = [ + "i686-pc-windows-msvc", + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "i686-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + "x86_64-apple-ios", + "aarch64-linux-android", + "wasm32-unknown-unknown", +] +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[features] +android-game-activity = ["android-activity/game-activity"] +android-native-activity = ["android-activity/native-activity"] +default = [ + "rwh_06", + "x11", + "wayland", + "wayland-dlopen", + "wayland-csd-adwaita", +] +mint = ["dpi/mint"] +rwh_04 = [ + "dep:rwh_04", + "ndk/rwh_04", +] +rwh_05 = [ + "dep:rwh_05", + "ndk/rwh_05", +] +rwh_06 = [ + "dep:rwh_06", + "ndk/rwh_06", +] +serde = [ + "dep:serde", + "cursor-icon/serde", + "smol_str/serde", + "dpi/serde", +] +wayland = [ + "wayland-client", + "wayland-backend", + "wayland-protocols", + "wayland-protocols-plasma", + "sctk", + "ahash", + "memmap2", +] +wayland-csd-adwaita = [ + "sctk-adwaita", + "sctk-adwaita/ab_glyph", +] +wayland-csd-adwaita-crossfont = [ + "sctk-adwaita", + "sctk-adwaita/crossfont", +] +wayland-csd-adwaita-notitle = ["sctk-adwaita"] +wayland-dlopen = ["wayland-backend/dlopen"] +x11 = [ + "x11-dl", + "bytemuck", + "percent-encoding", + "xkbcommon-dl/x11", + "x11rb", +] + +[lib] +name = "winit" +path = "src/lib.rs" + +[[example]] +name = "child_window" +path = "examples/child_window.rs" + +[[example]] +name = "control_flow" +path = "examples/control_flow.rs" + +[[example]] +name = "pump_events" +path = "examples/pump_events.rs" + +[[example]] +name = "run_on_demand" +path = "examples/run_on_demand.rs" + +[[example]] +name = "window" +path = "examples/window.rs" +doc-scrape-examples = true + +[[example]] +name = "x11_embed" +path = "examples/x11_embed.rs" + +[[test]] +name = "send_objects" +path = "tests/send_objects.rs" + +[[test]] +name = "serde_objects" +path = "tests/serde_objects.rs" + +[[test]] +name = "sync_object" +path = "tests/sync_object.rs" + +[dependencies.bitflags] +version = "2" + +[dependencies.cursor-icon] +version = "1.1.0" + +[dependencies.dpi] +version = "0.1.1" + +[dependencies.rwh_04] +version = "0.4" +optional = true +package = "raw-window-handle" + +[dependencies.rwh_05] +version = "0.5.2" +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.smol_str] +version = "0.2.0" + +[dependencies.tracing] +version = "0.1.40" +default-features = false + +[dev-dependencies.image] +version = "0.25.0" +features = ["png"] +default-features = false + +[dev-dependencies.tracing] +version = "0.1.40" +features = ["log"] +default-features = false + +[dev-dependencies.tracing-subscriber] +version = "0.3.18" +features = ["env-filter"] + +[build-dependencies.cfg_aliases] +version = "0.2.1" + +[target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies.atomic-waker] +version = "1" + +[target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies.concurrent-queue] +version = "2" +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.ahash] +version = "0.8.7" +features = ["no-rng"] +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.bytemuck] +version = "1.13.1" +optional = true +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.calloop] +version = "0.13.0" + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.libc] +version = "0.2.64" + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.memmap2] +version = "0.9.0" +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.percent-encoding] +version = "2.0" +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.rustix] +version = "0.38.4" +features = [ + "std", + "system", + "thread", + "process", +] +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.sctk] +version = "0.19.2" +features = ["calloop"] +optional = true +default-features = false +package = "smithay-client-toolkit" + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.sctk-adwaita] +version = "0.10.1" +optional = true +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.wayland-backend] +version = "0.3.10" +features = ["client_system"] +optional = true +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.wayland-client] +version = "0.31.10" +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.wayland-protocols] +version = "0.32.8" +features = ["staging"] +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.wayland-protocols-plasma] +version = "0.3.8" +features = ["client"] +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.x11-dl] +version = "2.19.1" +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.x11rb] +version = "0.13.0" +features = [ + "allow-unsafe-code", + "dl-libxcb", + "randr", + "resource_manager", + "xinput", + "xkb", +] +optional = true +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.xkbcommon-dl] +version = "0.4.2" + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies.block2] +version = "0.5.1" + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies.core-foundation] +version = "0.9.3" + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies.objc2] +version = "0.5.2" +features = ["relax-sign-encoding"] + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dev-dependencies.softbuffer] +version = "0.4.0" +features = [ + "x11", + "x11-dlopen", + "wayland", + "wayland-dlopen", +] +default-features = false + +[target.'cfg(target_family = "wasm")'.dependencies.js-sys] +version = "0.3.70" + +[target.'cfg(target_family = "wasm")'.dependencies.pin-project] +version = "1" + +[target.'cfg(target_family = "wasm")'.dependencies.wasm-bindgen] +version = "0.2.93" + +[target.'cfg(target_family = "wasm")'.dependencies.wasm-bindgen-futures] +version = "0.4.43" + +[target.'cfg(target_family = "wasm")'.dependencies.web-time] +version = "1" + +[target.'cfg(target_family = "wasm")'.dependencies.web_sys] +version = "0.3.70" +features = [ + "AbortController", + "AbortSignal", + "Blob", + "BlobPropertyBag", + "console", + "CssStyleDeclaration", + "Document", + "DomException", + "DomRect", + "DomRectReadOnly", + "Element", + "Event", + "EventTarget", + "FocusEvent", + "HtmlCanvasElement", + "HtmlElement", + "HtmlImageElement", + "ImageBitmap", + "ImageBitmapOptions", + "ImageBitmapRenderingContext", + "ImageData", + "IntersectionObserver", + "IntersectionObserverEntry", + "KeyboardEvent", + "MediaQueryList", + "MessageChannel", + "MessagePort", + "Navigator", + "Node", + "OrientationLockType", + "OrientationType", + "PageTransitionEvent", + "Permissions", + "PermissionState", + "PermissionStatus", + "PointerEvent", + "PremultiplyAlpha", + "ResizeObserver", + "ResizeObserverBoxOptions", + "ResizeObserverEntry", + "ResizeObserverOptions", + "ResizeObserverSize", + "Screen", + "ScreenOrientation", + "Url", + "VisibilityState", + "WheelEvent", + "Window", + "Worker", +] +package = "web-sys" + +[target.'cfg(target_family = "wasm")'.dev-dependencies.console_error_panic_hook] +version = "0.1" + +[target.'cfg(target_family = "wasm")'.dev-dependencies.tracing-web] +version = "0.1" + +[target.'cfg(target_os = "android")'.dependencies.android-activity] +version = "0.6.0" + +[target.'cfg(target_os = "android")'.dependencies.ndk] +version = "0.9.0" +default-features = false + +[target.'cfg(target_os = "ios")'.dependencies.objc2-foundation] +version = "0.2.2" +features = [ + "block2", + "dispatch", + "NSArray", + "NSEnumerator", + "NSGeometry", + "NSObjCRuntime", + "NSOperation", + "NSString", + "NSProcessInfo", + "NSThread", + "NSSet", +] + +[target.'cfg(target_os = "ios")'.dependencies.objc2-ui-kit] +version = "0.2.2" +features = [ + "UIApplication", + "UIDevice", + "UIEvent", + "UIGeometry", + "UIGestureRecognizer", + "UITextInput", + "UITextInputTraits", + "UIOrientation", + "UIPanGestureRecognizer", + "UIPinchGestureRecognizer", + "UIResponder", + "UIRotationGestureRecognizer", + "UIScreen", + "UIScreenMode", + "UITapGestureRecognizer", + "UITouch", + "UITraitCollection", + "UIView", + "UIViewController", + "UIWindow", +] + +[target.'cfg(target_os = "macos")'.dependencies.core-graphics] +version = "0.23.1" + +[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit] +version = "0.2.2" +features = [ + "NSAppearance", + "NSApplication", + "NSBitmapImageRep", + "NSButton", + "NSColor", + "NSControl", + "NSCursor", + "NSDragging", + "NSEvent", + "NSGraphics", + "NSGraphicsContext", + "NSImage", + "NSImageRep", + "NSMenu", + "NSMenuItem", + "NSOpenGLView", + "NSPasteboard", + "NSResponder", + "NSRunningApplication", + "NSScreen", + "NSTextInputClient", + "NSTextInputContext", + "NSView", + "NSWindow", + "NSWindowScripting", + "NSWindowTabGroup", +] + +[target.'cfg(target_os = "macos")'.dependencies.objc2-foundation] +version = "0.2.2" +features = [ + "block2", + "dispatch", + "NSArray", + "NSAttributedString", + "NSData", + "NSDictionary", + "NSDistributedNotificationCenter", + "NSEnumerator", + "NSKeyValueObserving", + "NSNotification", + "NSObjCRuntime", + "NSPathUtilities", + "NSProcessInfo", + "NSRunLoop", + "NSString", + "NSThread", + "NSValue", +] + +[target.'cfg(target_os = "redox")'.dependencies.orbclient] +version = "0.3.47" +default-features = false + +[target.'cfg(target_os = "redox")'.dependencies.redox_syscall] +version = "0.4.1" + +[target.'cfg(target_os = "windows")'.dependencies.unicode-segmentation] +version = "1.7.1" + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +version = "0.52.0" +features = [ + "Win32_Devices_HumanInterfaceDevice", + "Win32_Foundation", + "Win32_Globalization", + "Win32_Graphics_Dwm", + "Win32_Graphics_Gdi", + "Win32_Media", + "Win32_System_Com_StructuredStorage", + "Win32_System_Com", + "Win32_System_LibraryLoader", + "Win32_System_Ole", + "Win32_Security", + "Win32_System_SystemInformation", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", + "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", +] diff --git a/third_party/winit-0.30.13/FEATURES.md b/third_party/winit-0.30.13/FEATURES.md new file mode 100644 index 0000000..2ae9477 --- /dev/null +++ b/third_party/winit-0.30.13/FEATURES.md @@ -0,0 +1,248 @@ +# Winit Scope + +Winit aims to expose an interface that abstracts over window creation and input handling and can +be used to create both games and applications. It supports the following main graphical platforms: +- Desktop + - Windows + - macOS + - Unix + - via X11 + - via Wayland + - Redox OS, via Orbital +- Mobile + - iOS + - Android +- Web + +Most platforms expose capabilities that cannot be meaningfully transposed onto others. Winit does not +aim to support every single feature of every platform, but rather to abstract over the common features +available everywhere. In this context, APIs exposed in winit can be split into different "support tiers": + +- **Core:** Features that are essential to providing a well-formed abstraction over each platform's + windowing and input APIs. +- **Platform:** Platform-specific features that can't be meaningfully exposed through a common API and + cannot be implemented outside of Winit without exposing a significant amount of Winit's internals + or interfering with Winit's abstractions. +- **Usability:** Features that are not strictly essential to Winit's functionality, but provide meaningful + usability improvements and cannot be reasonably implemented in an external crate. These are + generally optional and exposed through Cargo features. + +Core features are taken care of by the core Winit maintainers. Platform features are not. +When a platform feature is submitted, the submitter is considered the expert in the +feature and may be asked to support the feature should it break in the future. + +Winit ***does not*** directly expose functionality for drawing inside windows or creating native +menus, but ***does*** commit to providing APIs that higher-level crates can use to implement that +functionality. + +## `1.0` and stability + +When all core features are implemented to the satisfaction of the Winit maintainers, Winit 1.0 will +be released and the library will enter maintenance mode. For the most part, new core features will not +be added past this point. New platform features may be accepted and exposed through point releases. + +### Tier upgrades +Some platform features could, in theory, be exposed across multiple platforms, but have not gone +through the implementation work necessary to function on all platforms. When one of these features +gets implemented across all platforms, a PR can be opened to upgrade the feature to a core feature. +If that gets accepted, the platform-specific functions get deprecated and become permanently +exposed through the core, cross-platform API. + +# Features + +## Extending this section + +If your PR makes notable changes to Winit's features, please update this section as follows: + +- If your PR adds a new feature, add a brief description to the relevant section. If the feature is a core + feature, add a row to the feature matrix and describe what platforms the feature has been implemented on. + +- If your PR begins a new API rework, add a row to the `Pending API Reworks` table. If the PR implements the + API rework on all relevant platforms, please move it to the `Completed API Reworks` table. + +- If your PR implements an already-existing feature on a new platform, either mark the feature as *completed*, + or mark it as *mostly completed* and link to an issue describing the problems with the implementation. + +## Core + +### Windowing +- **Window initialization**: Winit allows the creation of a window +- **Providing pointer to init OpenGL**: Winit provides the necessary pointers to initialize a working opengl context +- **Providing pointer to init Vulkan**: Same as OpenGL but for Vulkan +- **Window decorations**: The windows created by winit are properly decorated, and the decorations can + be deactivated +- **Window decorations toggle**: Decorations can be turned on or off after window creation +- **Window resizing**: The windows created by winit can be resized and generate the appropriate events + when they are. The application can precisely control its window size if desired. +- **Window resize increments**: When the window gets resized, the application can choose to snap the window's + size to specific values. +- **Window transparency**: Winit allows the creation of windows with a transparent background. +- **Window maximization**: The windows created by winit can be maximized upon creation. +- **Window maximization toggle**: The windows created by winit can be maximized and unmaximized after + creation. +- **Window minimization**: The windows created by winit can be minimized after creation. +- **Fullscreen**: The windows created by winit can be put into fullscreen mode. +- **Fullscreen toggle**: The windows created by winit can be switched to and from fullscreen after + creation. +- **Exclusive fullscreen**: Winit allows changing the video mode of the monitor + for fullscreen windows and, if applicable, captures the monitor for exclusive + use by this application. +- **HiDPI support**: Winit assists developers in appropriately scaling HiDPI content. +- **Popup / modal windows**: Windows can be created relative to the client area of other windows, and parent + windows can be disabled in favor of popup windows. This feature also guarantees that popup windows + get drawn above their owner. + + +### System Information +- **Monitor list**: Retrieve the list of monitors and their metadata, including which one is primary. +- **Video mode query**: Monitors can be queried for their supported fullscreen video modes (consisting of resolution, refresh rate, and bit depth). + +### Input Handling +- **Mouse events**: Generating mouse events associated with pointer motion, click, and scrolling events. +- **Mouse set location**: Forcibly changing the location of the pointer. +- **Cursor locking**: Locking the cursor inside the window so it cannot move. +- **Cursor confining**: Confining the cursor to the window bounds so it cannot leave them. +- **Cursor icon**: Changing the cursor icon or hiding the cursor. +- **Cursor image**: Changing the cursor to your own image. +- **Cursor hittest**: Handle or ignore mouse events for a window. +- **Touch events**: Single-touch events. +- **Touch pressure**: Touch events contain information about the amount of force being applied. +- **Multitouch**: Multi-touch events, including cancellation of a gesture. +- **Keyboard events**: Properly processing keyboard events using the user-specified keymap and + translating keypresses into UTF-8 characters, handling dead keys and IMEs. +- **Drag & Drop**: Dragging content into winit, detecting when content enters, drops, or if the drop is cancelled. +- **Raw Device Events**: Capturing input from input devices without any OS filtering. +- **Gamepad/Joystick events**: Capturing input from gamepads and joysticks. +- **Device movement events**: Capturing input from the device gyroscope and accelerometer. + +## Platform +### Windows +* Setting the name of the internal window class +* Setting the taskbar icon +* Setting the parent window +* Setting a menu bar +* `WS_EX_NOREDIRECTIONBITMAP` support +* Theme the title bar according to Windows 10 Dark Mode setting or set a preferred theme +* Changing a system-drawn backdrop +* Setting the window border color +* Setting the title bar background color +* Setting the title color +* Setting the corner rounding preference + +### macOS +* Window activation policy +* Window movable by background +* Transparent titlebar +* Hidden titlebar +* Hidden titlebar buttons +* Full-size content view +* Accepts first mouse +* Set a preferred theme and get current theme. + +### Unix +* Window urgency +* X11 Window Class +* X11 Override Redirect Flag +* GTK Theme Variant +* Base window size +* Setting the X11 parent window + +### iOS +* Get the `UIScreen` object pointer +* Setting the `UIView` hidpi factor +* Valid orientations +* Home indicator visibility +* Status bar visibility and style +* Deferring system gestures +* Getting the device idiom +* Getting the preferred video mode + +### Web +* Get if the systems preferred color scheme is "dark" + +## Compatibility Matrix + +Legend: + +- ✔️: Works as intended +- ▢: Mostly works, but some bugs are known +- ❌: Missing feature or large bugs making it unusable +- **N/A**: Not applicable for this platform +- ❓: Unknown status + +### Windowing +|Feature |Windows|MacOS |Linux x11 |Linux Wayland |Android|iOS |Web |Redox OS| +|-------------------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | +|Window initialization |✔️ |✔️ |▢[#5] |✔️ |▢[#33]|▢[#33] |✔️ |✔️ | +|Providing pointer to init OpenGL |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ | +|Providing pointer to init Vulkan |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |**N/A**|**N/A** | +|Window decorations |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|✔️ | +|Window decorations toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|**N/A** | +|Window resizing |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |✔️ | +|Window resize increments |✔️ |✔️ |✔️ |❌ |**N/A**|**N/A**|**N/A**|**N/A** | +|Window transparency |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|N/A |✔️ | +|Window blur |❌ |❌ |❌ |✔️ |**N/A**|**N/A**|N/A |❌ | +|Window maximization |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|**N/A** | +|Window maximization toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|**N/A** | +|Window minimization |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|**N/A** | +|Fullscreen |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ |**N/A** | +|Fullscreen toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ |**N/A** | +|Exclusive fullscreen |✔️ |✔️ |✔️ |**N/A** |❌ |✔️ |**N/A**|**N/A** | +|HiDPI support |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |❌ | +|Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |**N/A**|**N/A** | + +### System information +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| +|---------------- | ----- | ---- | ------- | ----------- | ----- | ------- | -------- | ------ | +|Monitor list |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**|❌ | +|Video mode query |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**|❌ | + +### Input handling +|Feature |Windows |MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| +|----------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | +|Mouse events |✔️ |▢[#63] |✔️ |✔️ |**N/A**|**N/A**|✔️ |✔️ | +|Mouse set location |✔️ |✔️ |✔️ |✔️(when locked) |**N/A**|**N/A**|**N/A**|**N/A** | +|Cursor locking |❌ |✔️ |❌ |✔️ |**N/A**|**N/A**|✔️ |❌ | +|Cursor confining |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ | +|Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |**N/A** | +|Cursor image |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |**N/A** | +|Cursor hittest |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ | +|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A** | +|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |✔️ |**N/A** | +|Multitouch |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ |**N/A** | +|Keyboard events |✔️ |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |✔️ | +|Drag & Drop |▢[#720] |▢[#720] |▢[#720] |▢[#720] |**N/A**|**N/A**|❓ |**N/A** | +|Raw Device Events |▢[#750] |▢[#750] |▢[#750] |❌ |❌ |❌ |❓ |**N/A** | +|Gamepad/Joystick events |❌[#804] |❌ |❌ |❌ |❌ |❌ |❓ |**N/A** | +|Device movement events |❓ |❓ |❓ |❓ |❌ |❌ |❓ |**N/A** | +|Drag window with cursor |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** |**N/A** | +|Resize with cursor |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|**N/A** |**N/A** | + +### Pending API Reworks +Changes in the API that have been agreed upon but aren't implemented across all platforms. + +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| +|------------------------------ | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | +|New API for HiDPI ([#315] [#319]) |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |❓ | +|Event Loop 2.0 ([#459]) |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |✔️ | +|Keyboard Input 2.0 ([#753]) |✔️ |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |✔️ | + +### Completed API Reworks +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| +|------------------------------ | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | + +[#165]: https://github.com/rust-windowing/winit/issues/165 +[#219]: https://github.com/rust-windowing/winit/issues/219 +[#242]: https://github.com/rust-windowing/winit/issues/242 +[#306]: https://github.com/rust-windowing/winit/issues/306 +[#315]: https://github.com/rust-windowing/winit/issues/315 +[#319]: https://github.com/rust-windowing/winit/issues/319 +[#33]: https://github.com/rust-windowing/winit/issues/33 +[#459]: https://github.com/rust-windowing/winit/issues/459 +[#5]: https://github.com/rust-windowing/winit/issues/5 +[#63]: https://github.com/rust-windowing/winit/issues/63 +[#720]: https://github.com/rust-windowing/winit/issues/720 +[#721]: https://github.com/rust-windowing/winit/issues/721 +[#750]: https://github.com/rust-windowing/winit/issues/750 +[#753]: https://github.com/rust-windowing/winit/issues/753 +[#804]: https://github.com/rust-windowing/winit/issues/804 diff --git a/third_party/winit-0.30.13/LICENSE b/third_party/winit-0.30.13/LICENSE new file mode 100644 index 0000000..ad410e1 --- /dev/null +++ b/third_party/winit-0.30.13/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/third_party/winit-0.30.13/README.md b/third_party/winit-0.30.13/README.md new file mode 100644 index 0000000..ab95887 --- /dev/null +++ b/third_party/winit-0.30.13/README.md @@ -0,0 +1,70 @@ +# winit - Cross-platform window creation and management in Rust + +[![Crates.io](https://img.shields.io/crates/v/winit.svg)](https://crates.io/crates/winit) +[![Docs.rs](https://docs.rs/winit/badge.svg)](https://docs.rs/winit) +[![Master Docs](https://img.shields.io/github/actions/workflow/status/rust-windowing/winit/docs.yml?branch=master&label=master%20docs +)](https://rust-windowing.github.io/winit/winit/index.html) +[![CI Status](https://github.com/rust-windowing/winit/workflows/CI/badge.svg)](https://github.com/rust-windowing/winit/actions) + +```toml +[dependencies] +winit = "0.30.13" +``` + +## [Documentation](https://docs.rs/winit) + +For features _within_ the scope of winit, see [FEATURES.md](FEATURES.md). + +For features _outside_ the scope of winit, see [Are we GUI Yet?](https://areweguiyet.com/) and [Are we game yet?](https://arewegameyet.rs/), depending on what kind of project you're looking to do. + +## Contact Us + +Join us in our [![Matrix](https://img.shields.io/badge/Matrix-%23rust--windowing%3Amatrix.org-blueviolet.svg)](https://matrix.to/#/#rust-windowing:matrix.org) room. + +The maintainers have a meeting every friday at UTC 15. The meeting notes can be found [here](https://hackmd.io/@winit-meetings). + +## Usage + +Winit is a window creation and management library. It can create windows and lets you handle +events (for example: the window being resized, a key being pressed, a mouse movement, etc.) +produced by the window. + +Winit is designed to be a low-level brick in a hierarchy of libraries. Consequently, in order to +show something on the window you need to use the platform-specific getters provided by winit, or +another library. + +## CONTRIBUTING + +For contributing guidelines see [CONTRIBUTING.md](./CONTRIBUTING.md). + +## MSRV Policy + +This crate's Minimum Supported Rust Version (MSRV) is **1.70**. Changes to +the MSRV will be accompanied by a minor version bump. + +As a **tentative** policy, the upper bound of the MSRV is given by the following +formula: + +``` +min(sid, stable - 3) +``` + +Where `sid` is the current version of `rustc` provided by [Debian Sid], and +`stable` is the latest stable version of Rust. This bound may be broken in case of a major ecosystem shift or a security vulnerability. + +[Debian Sid]: https://packages.debian.org/sid/rustc + +The exception is for the Android platform, where a higher Rust version +must be used for certain Android features. In this case, the MSRV will be +capped at the latest stable version of Rust minus three. This inconsistency is +not reflected in Cargo metadata, as it is not powerful enough to expose this +restriction. + +All crates in the [`rust-windowing`] organizations have the +same MSRV policy. + +[`rust-windowing`]: https://github.com/rust-windowing + +### Platform-specific usage + +Check out the [`winit::platform`](https://rust-windowing.github.io/winit/winit/platform/index.html) module for platform-specific usage. diff --git a/third_party/winit-0.30.13/build.rs b/third_party/winit-0.30.13/build.rs new file mode 100644 index 0000000..6a4528b --- /dev/null +++ b/third_party/winit-0.30.13/build.rs @@ -0,0 +1,27 @@ +use cfg_aliases::cfg_aliases; + +fn main() { + // The script doesn't depend on our code. + println!("cargo:rerun-if-changed=build.rs"); + + // Setup cfg aliases. + cfg_aliases! { + // Systems. + android_platform: { target_os = "android" }, + web_platform: { all(target_family = "wasm", target_os = "unknown") }, + macos_platform: { target_os = "macos" }, + ios_platform: { target_os = "ios" }, + windows_platform: { target_os = "windows" }, + apple: { any(target_os = "ios", target_os = "macos") }, + free_unix: { all(unix, not(apple), not(android_platform), not(target_os = "emscripten")) }, + redox: { target_os = "redox" }, + + // Native displays. + x11_platform: { all(feature = "x11", free_unix, not(redox)) }, + wayland_platform: { all(feature = "wayland", free_unix, not(redox)) }, + orbital_platform: { redox }, + } + + // Winit defined cfgs. + println!("cargo:rustc-check-cfg=cfg(unreleased_changelogs)"); +} diff --git a/third_party/winit-0.30.13/docs/res/ATTRIBUTION.md b/third_party/winit-0.30.13/docs/res/ATTRIBUTION.md new file mode 100644 index 0000000..268316f --- /dev/null +++ b/third_party/winit-0.30.13/docs/res/ATTRIBUTION.md @@ -0,0 +1,11 @@ +# Image Attribution + +These images are used in the documentation of `winit`. + +## keyboard_*.svg + +These files are a modified version of "[ANSI US QWERTY (Windows)](https://commons.wikimedia.org/wiki/File:ANSI_US_QWERTY_(Windows).svg)" +by [Tomiĉo] (https://commons.wikimedia.org/wiki/User:Tomi%C4%89o). It was +originally released under the [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en) +License. Minor modifications have been made by [John Nunley](https://github.com/notgull), +which have been released under the same license as a derivative work. diff --git a/third_party/winit-0.30.13/docs/res/keyboard_left_shift_key.svg b/third_party/winit-0.30.13/docs/res/keyboard_left_shift_key.svg new file mode 100644 index 0000000..bae6f9a --- /dev/null +++ b/third_party/winit-0.30.13/docs/res/keyboard_left_shift_key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/third_party/winit-0.30.13/docs/res/keyboard_numpad_1_key.svg b/third_party/winit-0.30.13/docs/res/keyboard_numpad_1_key.svg new file mode 100644 index 0000000..d595758 --- /dev/null +++ b/third_party/winit-0.30.13/docs/res/keyboard_numpad_1_key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/third_party/winit-0.30.13/docs/res/keyboard_right_shift_key.svg b/third_party/winit-0.30.13/docs/res/keyboard_right_shift_key.svg new file mode 100644 index 0000000..dc016f0 --- /dev/null +++ b/third_party/winit-0.30.13/docs/res/keyboard_right_shift_key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/third_party/winit-0.30.13/docs/res/keyboard_standard_1_key.svg b/third_party/winit-0.30.13/docs/res/keyboard_standard_1_key.svg new file mode 100644 index 0000000..3520d55 --- /dev/null +++ b/third_party/winit-0.30.13/docs/res/keyboard_standard_1_key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/third_party/winit-0.30.13/examples/child_window.rs b/third_party/winit-0.30.13/examples/child_window.rs new file mode 100644 index 0000000..715996e --- /dev/null +++ b/third_party/winit-0.30.13/examples/child_window.rs @@ -0,0 +1,88 @@ +#[cfg(all(feature = "rwh_06", any(x11_platform, macos_platform, windows_platform)))] +#[allow(deprecated)] +fn main() -> Result<(), impl std::error::Error> { + use std::collections::HashMap; + + use winit::dpi::{LogicalPosition, LogicalSize, Position}; + use winit::event::{ElementState, Event, KeyEvent, WindowEvent}; + use winit::event_loop::{ActiveEventLoop, EventLoop}; + use winit::raw_window_handle::HasRawWindowHandle; + use winit::window::Window; + + #[path = "util/fill.rs"] + mod fill; + + fn spawn_child_window(parent: &Window, event_loop: &ActiveEventLoop) -> Window { + let parent = parent.raw_window_handle().unwrap(); + let mut window_attributes = Window::default_attributes() + .with_title("child window") + .with_inner_size(LogicalSize::new(200.0f32, 200.0f32)) + .with_position(Position::Logical(LogicalPosition::new(0.0, 0.0))) + .with_visible(true); + // `with_parent_window` is unsafe. Parent window must be a valid window. + window_attributes = unsafe { window_attributes.with_parent_window(Some(parent)) }; + + event_loop.create_window(window_attributes).unwrap() + } + + let mut windows = HashMap::new(); + + let event_loop: EventLoop<()> = EventLoop::new().unwrap(); + let mut parent_window_id = None; + + event_loop.run(move |event: Event<()>, event_loop| { + match event { + Event::Resumed => { + let attributes = Window::default_attributes() + .with_title("parent window") + .with_position(Position::Logical(LogicalPosition::new(0.0, 0.0))) + .with_inner_size(LogicalSize::new(640.0f32, 480.0f32)); + let window = event_loop.create_window(attributes).unwrap(); + + parent_window_id = Some(window.id()); + + println!("Parent window id: {parent_window_id:?})"); + windows.insert(window.id(), window); + }, + Event::WindowEvent { window_id, event } => match event { + WindowEvent::CloseRequested => { + windows.clear(); + event_loop.exit(); + }, + WindowEvent::CursorEntered { device_id: _ } => { + // On x11, println when the cursor entered in a window even if the child window + // is created by some key inputs. + // the child windows are always placed at (0, 0) with size (200, 200) in the + // parent window, so we also can see this log when we move + // the cursor around (200, 200) in parent window. + println!("cursor entered in the window {window_id:?}"); + }, + WindowEvent::KeyboardInput { + event: KeyEvent { state: ElementState::Pressed, .. }, + .. + } => { + let parent_window = windows.get(&parent_window_id.unwrap()).unwrap(); + let child_window = spawn_child_window(parent_window, event_loop); + let child_id = child_window.id(); + println!("Child window created with id: {child_id:?}"); + windows.insert(child_id, child_window); + }, + WindowEvent::RedrawRequested => { + if let Some(window) = windows.get(&window_id) { + fill::fill_window(window); + } + }, + _ => (), + }, + _ => (), + } + }) +} + +#[cfg(all(feature = "rwh_06", not(any(x11_platform, macos_platform, windows_platform))))] +fn main() { + panic!( + "This example is supported only on x11, macOS, and Windows, with the `rwh_06` feature \ + enabled." + ); +} diff --git a/third_party/winit-0.30.13/examples/control_flow.rs b/third_party/winit-0.30.13/examples/control_flow.rs new file mode 100644 index 0000000..13cc947 --- /dev/null +++ b/third_party/winit-0.30.13/examples/control_flow.rs @@ -0,0 +1,148 @@ +#![allow(clippy::single_match)] + +use std::thread; +#[cfg(not(web_platform))] +use std::time; + +use ::tracing::{info, warn}; +#[cfg(web_platform)] +use web_time as time; + +use winit::application::ApplicationHandler; +use winit::event::{ElementState, KeyEvent, StartCause, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::keyboard::{Key, NamedKey}; +use winit::window::{Window, WindowId}; + +#[path = "util/fill.rs"] +mod fill; +#[path = "util/tracing.rs"] +mod tracing; + +const WAIT_TIME: time::Duration = time::Duration::from_millis(100); +const POLL_SLEEP_TIME: time::Duration = time::Duration::from_millis(100); + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +enum Mode { + #[default] + Wait, + WaitUntil, + Poll, +} + +fn main() -> Result<(), impl std::error::Error> { + #[cfg(web_platform)] + console_error_panic_hook::set_once(); + + tracing::init(); + + info!("Press '1' to switch to Wait mode."); + info!("Press '2' to switch to WaitUntil mode."); + info!("Press '3' to switch to Poll mode."); + info!("Press 'R' to toggle request_redraw() calls."); + info!("Press 'Esc' to close the window."); + + let event_loop = EventLoop::new().unwrap(); + + let mut app = ControlFlowDemo::default(); + event_loop.run_app(&mut app) +} + +#[derive(Default)] +struct ControlFlowDemo { + mode: Mode, + request_redraw: bool, + wait_cancelled: bool, + close_requested: bool, + window: Option, +} + +impl ApplicationHandler for ControlFlowDemo { + fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) { + info!("new_events: {cause:?}"); + + self.wait_cancelled = match cause { + StartCause::WaitCancelled { .. } => self.mode == Mode::WaitUntil, + _ => false, + } + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes().with_title( + "Press 1, 2, 3 to change control flow mode. Press R to toggle redraw requests.", + ); + self.window = Some(event_loop.create_window(window_attributes).unwrap()); + } + + fn window_event( + &mut self, + _event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + info!("{event:?}"); + + match event { + WindowEvent::CloseRequested => { + self.close_requested = true; + }, + WindowEvent::KeyboardInput { + event: KeyEvent { logical_key: key, state: ElementState::Pressed, .. }, + .. + } => match key.as_ref() { + // WARNING: Consider using `key_without_modifiers()` if available on your platform. + // See the `key_binding` example + Key::Character("1") => { + self.mode = Mode::Wait; + warn!("mode: {:?}", self.mode); + }, + Key::Character("2") => { + self.mode = Mode::WaitUntil; + warn!("mode: {:?}", self.mode); + }, + Key::Character("3") => { + self.mode = Mode::Poll; + warn!("mode: {:?}", self.mode); + }, + Key::Character("r") => { + self.request_redraw = !self.request_redraw; + warn!("request_redraw: {}", self.request_redraw); + }, + Key::Named(NamedKey::Escape) => { + self.close_requested = true; + }, + _ => (), + }, + WindowEvent::RedrawRequested => { + let window = self.window.as_ref().unwrap(); + window.pre_present_notify(); + fill::fill_window(window); + }, + _ => (), + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if self.request_redraw && !self.wait_cancelled && !self.close_requested { + self.window.as_ref().unwrap().request_redraw(); + } + + match self.mode { + Mode::Wait => event_loop.set_control_flow(ControlFlow::Wait), + Mode::WaitUntil => { + if !self.wait_cancelled { + event_loop + .set_control_flow(ControlFlow::WaitUntil(time::Instant::now() + WAIT_TIME)); + } + }, + Mode::Poll => { + thread::sleep(POLL_SLEEP_TIME); + event_loop.set_control_flow(ControlFlow::Poll); + }, + }; + + if self.close_requested { + event_loop.exit(); + } + } +} diff --git a/third_party/winit-0.30.13/examples/data/cross.png b/third_party/winit-0.30.13/examples/data/cross.png new file mode 100644 index 0000000..9bfdf36 Binary files /dev/null and b/third_party/winit-0.30.13/examples/data/cross.png differ diff --git a/third_party/winit-0.30.13/examples/data/cross2.png b/third_party/winit-0.30.13/examples/data/cross2.png new file mode 100644 index 0000000..b9f7a48 Binary files /dev/null and b/third_party/winit-0.30.13/examples/data/cross2.png differ diff --git a/third_party/winit-0.30.13/examples/data/gradient.png b/third_party/winit-0.30.13/examples/data/gradient.png new file mode 100644 index 0000000..41ce610 Binary files /dev/null and b/third_party/winit-0.30.13/examples/data/gradient.png differ diff --git a/third_party/winit-0.30.13/examples/data/icon.png b/third_party/winit-0.30.13/examples/data/icon.png new file mode 100644 index 0000000..aa3fbf3 Binary files /dev/null and b/third_party/winit-0.30.13/examples/data/icon.png differ diff --git a/third_party/winit-0.30.13/examples/pump_events.rs b/third_party/winit-0.30.13/examples/pump_events.rs new file mode 100644 index 0000000..ad198cf --- /dev/null +++ b/third_party/winit-0.30.13/examples/pump_events.rs @@ -0,0 +1,80 @@ +#![allow(clippy::single_match)] + +// Limit this example to only compatible platforms. +#[cfg(any(windows_platform, macos_platform, x11_platform, wayland_platform, android_platform,))] +fn main() -> std::process::ExitCode { + use std::process::ExitCode; + use std::thread::sleep; + use std::time::Duration; + + use winit::application::ApplicationHandler; + use winit::event::WindowEvent; + use winit::event_loop::{ActiveEventLoop, EventLoop}; + use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus}; + use winit::window::{Window, WindowId}; + + #[path = "util/fill.rs"] + mod fill; + + #[derive(Default)] + struct PumpDemo { + window: Option, + } + + impl ApplicationHandler for PumpDemo { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes().with_title("A fantastic window!"); + self.window = Some(event_loop.create_window(window_attributes).unwrap()); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + println!("{event:?}"); + + let window = match self.window.as_ref() { + Some(window) => window, + None => return, + }; + + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::RedrawRequested => { + fill::fill_window(window); + window.request_redraw(); + }, + _ => (), + } + } + } + + let mut event_loop = EventLoop::new().unwrap(); + + tracing_subscriber::fmt::init(); + + let mut app = PumpDemo::default(); + + loop { + let timeout = Some(Duration::ZERO); + let status = event_loop.pump_app_events(timeout, &mut app); + + if let PumpStatus::Exit(exit_code) = status { + break ExitCode::from(exit_code as u8); + } + + // Sleep for 1/60 second to simulate application work + // + // Since `pump_events` doesn't block it will be important to + // throttle the loop in the app somehow. + println!("Update()"); + sleep(Duration::from_millis(16)); + } +} + +#[cfg(any(ios_platform, web_platform, orbital_platform))] +fn main() { + println!("This platform doesn't support pump_events."); +} diff --git a/third_party/winit-0.30.13/examples/run_on_demand.rs b/third_party/winit-0.30.13/examples/run_on_demand.rs new file mode 100644 index 0000000..5a277de --- /dev/null +++ b/third_party/winit-0.30.13/examples/run_on_demand.rs @@ -0,0 +1,99 @@ +#![allow(clippy::single_match)] + +// Limit this example to only compatible platforms. +#[cfg(any(windows_platform, macos_platform, x11_platform, wayland_platform,))] +fn main() -> Result<(), Box> { + use std::time::Duration; + + use winit::application::ApplicationHandler; + use winit::event::WindowEvent; + use winit::event_loop::{ActiveEventLoop, EventLoop}; + use winit::platform::run_on_demand::EventLoopExtRunOnDemand; + use winit::window::{Window, WindowId}; + + #[path = "util/fill.rs"] + mod fill; + + #[derive(Default)] + struct App { + idx: usize, + window_id: Option, + window: Option, + } + + impl ApplicationHandler for App { + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + if let Some(window) = self.window.as_ref() { + window.request_redraw(); + } + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes() + .with_title("Fantastic window number one!") + .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0)); + let window = event_loop.create_window(window_attributes).unwrap(); + self.window_id = Some(window.id()); + self.window = Some(window); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + if event == WindowEvent::Destroyed && self.window_id == Some(window_id) { + println!( + "--------------------------------------------------------- Window {} Destroyed", + self.idx + ); + self.window_id = None; + event_loop.exit(); + return; + } + + let window = match self.window.as_mut() { + Some(window) => window, + None => return, + }; + + match event { + WindowEvent::CloseRequested => { + println!( + "--------------------------------------------------------- Window {} \ + CloseRequested", + self.idx + ); + fill::cleanup_window(window); + self.window = None; + }, + WindowEvent::RedrawRequested => { + fill::fill_window(window); + }, + _ => (), + } + } + } + + tracing_subscriber::fmt::init(); + + let mut event_loop = EventLoop::new().unwrap(); + + let mut app = App { idx: 1, ..Default::default() }; + event_loop.run_app_on_demand(&mut app)?; + + println!("--------------------------------------------------------- Finished first loop"); + println!("--------------------------------------------------------- Waiting 5 seconds"); + std::thread::sleep(Duration::from_secs(5)); + + app.idx += 1; + event_loop.run_app_on_demand(&mut app)?; + println!("--------------------------------------------------------- Finished second loop"); + Ok(()) +} + +#[cfg(not(any(windows_platform, macos_platform, x11_platform, wayland_platform,)))] +fn main() { + println!("This example is not supported on this platform"); +} diff --git a/third_party/winit-0.30.13/examples/util/fill.rs b/third_party/winit-0.30.13/examples/util/fill.rs new file mode 100644 index 0000000..31540c0 --- /dev/null +++ b/third_party/winit-0.30.13/examples/util/fill.rs @@ -0,0 +1,117 @@ +//! Fill the window buffer with a solid color. +//! +//! Launching a window without drawing to it has unpredictable results varying from platform to +//! platform. In order to have well-defined examples, this module provides an easy way to +//! fill the window buffer with a solid color. +//! +//! The `softbuffer` crate is used, largely because of its ease of use. `glutin` or `wgpu` could +//! also be used to fill the window buffer, but they are more complicated to use. + +#[allow(unused_imports)] +pub use platform::cleanup_window; +pub use platform::fill_window; + +#[cfg(all(feature = "rwh_05", not(any(target_os = "android", target_os = "ios"))))] +mod platform { + use std::cell::RefCell; + use std::collections::HashMap; + use std::mem; + use std::mem::ManuallyDrop; + use std::num::NonZeroU32; + + use softbuffer::{Context, Surface}; + use winit::window::{Window, WindowId}; + + thread_local! { + // NOTE: You should never do things like that, create context and drop it before + // you drop the event loop. We do this for brevity to not blow up examples. We use + // ManuallyDrop to prevent destructors from running. + // + // A static, thread-local map of graphics contexts to open windows. + static GC: ManuallyDrop>> = const { ManuallyDrop::new(RefCell::new(None)) }; + } + + /// The graphics context used to draw to a window. + struct GraphicsContext { + /// The global softbuffer context. + context: RefCell>, + + /// The hash map of window IDs to surfaces. + surfaces: HashMap>, + } + + impl GraphicsContext { + fn new(w: &Window) -> Self { + Self { + context: RefCell::new( + Context::new(unsafe { mem::transmute::<&'_ Window, &'static Window>(w) }) + .expect("Failed to create a softbuffer context"), + ), + surfaces: HashMap::new(), + } + } + + fn create_surface( + &mut self, + window: &Window, + ) -> &mut Surface<&'static Window, &'static Window> { + self.surfaces.entry(window.id()).or_insert_with(|| { + Surface::new(&self.context.borrow(), unsafe { + mem::transmute::<&'_ Window, &'static Window>(window) + }) + .expect("Failed to create a softbuffer surface") + }) + } + + fn destroy_surface(&mut self, window: &Window) { + self.surfaces.remove(&window.id()); + } + } + + pub fn fill_window(window: &Window) { + GC.with(|gc| { + let size = window.inner_size(); + let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + else { + return; + }; + + // Either get the last context used or create a new one. + let mut gc = gc.borrow_mut(); + let surface = + gc.get_or_insert_with(|| GraphicsContext::new(window)).create_surface(window); + + // Fill a buffer with a solid color. + const DARK_GRAY: u32 = 0xff181818; + + surface.resize(width, height).expect("Failed to resize the softbuffer surface"); + + let mut buffer = surface.buffer_mut().expect("Failed to get the softbuffer buffer"); + buffer.fill(DARK_GRAY); + buffer.present().expect("Failed to present the softbuffer buffer"); + }) + } + + #[allow(dead_code)] + pub fn cleanup_window(window: &Window) { + GC.with(|gc| { + let mut gc = gc.borrow_mut(); + if let Some(context) = gc.as_mut() { + context.destroy_surface(window); + } + }); + } +} + +#[cfg(not(all(feature = "rwh_05", not(any(target_os = "android", target_os = "ios")))))] +mod platform { + pub fn fill_window(_window: &winit::window::Window) { + // No-op on mobile platforms. + } + + #[allow(dead_code)] + pub fn cleanup_window(_window: &winit::window::Window) { + // No-op on mobile platforms. + } +} diff --git a/third_party/winit-0.30.13/examples/util/tracing.rs b/third_party/winit-0.30.13/examples/util/tracing.rs new file mode 100644 index 0000000..bab7ced --- /dev/null +++ b/third_party/winit-0.30.13/examples/util/tracing.rs @@ -0,0 +1,25 @@ +#[cfg(not(web_platform))] +pub fn init() { + use tracing_subscriber::filter::{EnvFilter, LevelFilter}; + + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::builder().with_default_directive(LevelFilter::INFO.into()).from_env_lossy(), + ) + .init(); +} + +#[cfg(web_platform)] +pub fn init() { + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer() + .with_ansi(false) + .without_time() + .with_writer(tracing_web::MakeWebConsoleWriter::new()), + ) + .init(); +} diff --git a/third_party/winit-0.30.13/examples/window.rs b/third_party/winit-0.30.13/examples/window.rs new file mode 100644 index 0000000..48afadf --- /dev/null +++ b/third_party/winit-0.30.13/examples/window.rs @@ -0,0 +1,1110 @@ +//! Simple winit application. + +use std::collections::HashMap; +use std::error::Error; +use std::fmt::Debug; +#[cfg(not(any(android_platform, ios_platform)))] +use std::num::NonZeroU32; +use std::sync::Arc; +use std::{fmt, mem}; + +use ::tracing::{error, info}; +use cursor_icon::CursorIcon; +#[cfg(not(any(android_platform, ios_platform)))] +use rwh_06::{DisplayHandle, HasDisplayHandle}; +#[cfg(not(any(android_platform, ios_platform)))] +use softbuffer::{Context, Surface}; + +use winit::application::ApplicationHandler; +use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize}; +use winit::event::{DeviceEvent, DeviceId, Ime, MouseButton, MouseScrollDelta, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::keyboard::{Key, ModifiersState}; +use winit::window::{ + Cursor, CursorGrabMode, CustomCursor, CustomCursorSource, Fullscreen, Icon, ResizeDirection, + Theme, Window, WindowId, +}; + +#[cfg(macos_platform)] +use winit::platform::macos::{OptionAsAlt, WindowAttributesExtMacOS, WindowExtMacOS}; +#[cfg(any(x11_platform, wayland_platform))] +use winit::platform::startup_notify::{ + self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, WindowExtStartupNotify, +}; +#[cfg(x11_platform)] +use winit::platform::x11::WindowAttributesExtX11; + +#[path = "util/tracing.rs"] +mod tracing; + +/// The amount of points to around the window for drag resize direction calculations. +const BORDER_SIZE: f64 = 20.; + +fn main() -> Result<(), Box> { + #[cfg(web_platform)] + console_error_panic_hook::set_once(); + + tracing::init(); + + let event_loop = EventLoop::::with_user_event().build()?; + let _event_loop_proxy = event_loop.create_proxy(); + + // Wire the user event from another thread. + #[cfg(not(web_platform))] + std::thread::spawn(move || { + // Wake up the `event_loop` once every second and dispatch a custom event + // from a different thread. + info!("Starting to send user event every second"); + loop { + let _ = _event_loop_proxy.send_event(UserEvent::WakeUp); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + }); + + let mut state = Application::new(&event_loop); + + event_loop.run_app(&mut state).map_err(Into::into) +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy)] +enum UserEvent { + WakeUp, +} + +/// Application state and event handling. +struct Application { + /// Custom cursors assets. + custom_cursors: Vec, + /// Application icon. + icon: Icon, + windows: HashMap, + /// Drawing context. + /// + /// With OpenGL it could be EGLDisplay. + #[cfg(not(any(android_platform, ios_platform)))] + context: Option>>, +} + +impl Application { + fn new(event_loop: &EventLoop) -> Self { + // SAFETY: we drop the context right before the event loop is stopped, thus making it safe. + #[cfg(not(any(android_platform, ios_platform)))] + let context = Some( + Context::new(unsafe { + std::mem::transmute::, DisplayHandle<'static>>( + event_loop.display_handle().unwrap(), + ) + }) + .unwrap(), + ); + + // You'll have to choose an icon size at your own discretion. On X11, the desired size + // varies by WM, and on Windows, you still 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 icon = load_icon(include_bytes!("data/icon.png")); + + info!("Loading cursor assets"); + let custom_cursors = vec![ + event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/cross.png"))), + event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/cross2.png"))), + event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/gradient.png"))), + ]; + + Self { + #[cfg(not(any(android_platform, ios_platform)))] + context, + custom_cursors, + icon, + windows: Default::default(), + } + } + + fn create_window( + &mut self, + event_loop: &ActiveEventLoop, + _tab_id: Option, + ) -> Result> { + // TODO read-out activation token. + + #[allow(unused_mut)] + let mut window_attributes = Window::default_attributes() + .with_title("Winit window") + .with_transparent(true) + .with_window_icon(Some(self.icon.clone())); + + #[cfg(any(x11_platform, wayland_platform))] + if let Some(token) = event_loop.read_token_from_env() { + startup_notify::reset_activation_token_env(); + info!("Using token {:?} to activate a window", token); + window_attributes = window_attributes.with_activation_token(token); + } + + #[cfg(x11_platform)] + match std::env::var("X11_VISUAL_ID") { + Ok(visual_id_str) => { + info!("Using X11 visual id {visual_id_str}"); + let visual_id = visual_id_str.parse()?; + window_attributes = window_attributes.with_x11_visual(visual_id); + }, + Err(_) => info!("Set the X11_VISUAL_ID env variable to request specific X11 visual"), + } + + #[cfg(x11_platform)] + match std::env::var("X11_SCREEN_ID") { + Ok(screen_id_str) => { + info!("Placing the window on X11 screen {screen_id_str}"); + let screen_id = screen_id_str.parse()?; + window_attributes = window_attributes.with_x11_screen(screen_id); + }, + Err(_) => info!( + "Set the X11_SCREEN_ID env variable to place the window on non-default screen" + ), + } + + #[cfg(macos_platform)] + if let Some(tab_id) = _tab_id { + window_attributes = window_attributes.with_tabbing_identifier(&tab_id); + } + + #[cfg(web_platform)] + { + use winit::platform::web::WindowAttributesExtWebSys; + window_attributes = window_attributes.with_append(true); + } + + let window = event_loop.create_window(window_attributes)?; + + #[cfg(ios_platform)] + { + use winit::platform::ios::WindowExtIOS; + window.recognize_doubletap_gesture(true); + window.recognize_pinch_gesture(true); + window.recognize_rotation_gesture(true); + window.recognize_pan_gesture(true, 2, 2); + } + + let window_state = WindowState::new(self, window)?; + let window_id = window_state.window.id(); + info!("Created new window with id={window_id:?}"); + self.windows.insert(window_id, window_state); + Ok(window_id) + } + + fn handle_action(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, action: Action) { + // let cursor_position = self.cursor_position; + let window = self.windows.get_mut(&window_id).unwrap(); + info!("Executing action: {action:?}"); + match action { + Action::CloseWindow => { + let _ = self.windows.remove(&window_id); + }, + Action::CreateNewWindow => { + #[cfg(any(x11_platform, wayland_platform))] + if let Err(err) = window.window.request_activation_token() { + info!("Failed to get activation token: {err}"); + } else { + return; + } + + if let Err(err) = self.create_window(event_loop, None) { + error!("Error creating new window: {err}"); + } + }, + Action::ToggleResizeIncrements => window.toggle_resize_increments(), + Action::ToggleCursorVisibility => window.toggle_cursor_visibility(), + Action::ToggleResizable => window.toggle_resizable(), + Action::ToggleDecorations => window.toggle_decorations(), + Action::ToggleFullscreen => window.toggle_fullscreen(), + Action::ToggleMaximize => window.toggle_maximize(), + Action::ToggleImeInput => window.toggle_ime(), + Action::Minimize => window.minimize(), + Action::NextCursor => window.next_cursor(), + Action::NextCustomCursor => window.next_custom_cursor(&self.custom_cursors), + #[cfg(web_platform)] + Action::UrlCustomCursor => window.url_custom_cursor(event_loop), + #[cfg(web_platform)] + Action::AnimationCustomCursor => { + window.animation_custom_cursor(event_loop, &self.custom_cursors) + }, + Action::CycleCursorGrab => window.cycle_cursor_grab(), + Action::DragWindow => window.drag_window(), + Action::DragResizeWindow => window.drag_resize_window(), + Action::ShowWindowMenu => window.show_menu(), + Action::PrintHelp => self.print_help(), + #[cfg(macos_platform)] + Action::CycleOptionAsAlt => window.cycle_option_as_alt(), + Action::SetTheme(theme) => { + window.window.set_theme(theme); + // Get the resulting current theme to draw with + let actual_theme = theme.or_else(|| window.window.theme()).unwrap_or(Theme::Dark); + window.set_draw_theme(actual_theme); + }, + #[cfg(macos_platform)] + Action::CreateNewTab => { + let tab_id = window.window.tabbing_identifier(); + if let Err(err) = self.create_window(event_loop, Some(tab_id)) { + error!("Error creating new window: {err}"); + } + }, + Action::RequestResize => window.swap_dimensions(), + } + } + + fn dump_monitors(&self, event_loop: &ActiveEventLoop) { + info!("Monitors information"); + let primary_monitor = event_loop.primary_monitor(); + for monitor in event_loop.available_monitors() { + let intro = if primary_monitor.as_ref() == Some(&monitor) { + "Primary monitor" + } else { + "Monitor" + }; + + if let Some(name) = monitor.name() { + info!("{intro}: {name}"); + } else { + info!("{intro}: [no name]"); + } + + let PhysicalSize { width, height } = monitor.size(); + info!( + " Current mode: {width}x{height}{}", + if let Some(m_hz) = monitor.refresh_rate_millihertz() { + format!(" @ {}.{} Hz", m_hz / 1000, m_hz % 1000) + } else { + String::new() + } + ); + + let PhysicalPosition { x, y } = monitor.position(); + info!(" Position: {x},{y}"); + + info!(" Scale factor: {}", monitor.scale_factor()); + + info!(" Available modes (width x height x bit-depth):"); + for mode in monitor.video_modes() { + let PhysicalSize { width, height } = mode.size(); + let bits = mode.bit_depth(); + let m_hz = mode.refresh_rate_millihertz(); + info!(" {width}x{height}x{bits} @ {}.{} Hz", m_hz / 1000, m_hz % 1000); + } + } + } + + /// Process the key binding. + fn process_key_binding(key: &str, mods: &ModifiersState) -> Option { + KEY_BINDINGS + .iter() + .find_map(|binding| binding.is_triggered_by(&key, mods).then_some(binding.action)) + } + + /// Process mouse binding. + fn process_mouse_binding(button: MouseButton, mods: &ModifiersState) -> Option { + MOUSE_BINDINGS + .iter() + .find_map(|binding| binding.is_triggered_by(&button, mods).then_some(binding.action)) + } + + fn print_help(&self) { + info!("Keyboard bindings:"); + for binding in KEY_BINDINGS { + info!( + "{}{:<10} - {} ({})", + modifiers_to_string(binding.mods), + binding.trigger, + binding.action, + binding.action.help(), + ); + } + info!("Mouse bindings:"); + for binding in MOUSE_BINDINGS { + info!( + "{}{:<10} - {} ({})", + modifiers_to_string(binding.mods), + mouse_button_to_string(binding.trigger), + binding.action, + binding.action.help(), + ); + } + } +} + +impl ApplicationHandler for Application { + fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) { + info!("User event: {event:?}"); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + let window = match self.windows.get_mut(&window_id) { + Some(window) => window, + None => return, + }; + + match event { + WindowEvent::Resized(size) => { + window.resize(size); + }, + WindowEvent::Focused(focused) => { + if focused { + info!("Window={window_id:?} focused"); + } else { + info!("Window={window_id:?} unfocused"); + } + }, + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + info!("Window={window_id:?} changed scale to {scale_factor}"); + }, + WindowEvent::ThemeChanged(theme) => { + info!("Theme changed to {theme:?}"); + window.set_draw_theme(theme); + }, + WindowEvent::RedrawRequested => { + if let Err(err) = window.draw() { + error!("Error drawing window: {err}"); + } + }, + WindowEvent::Occluded(occluded) => { + window.set_occluded(occluded); + }, + WindowEvent::CloseRequested => { + info!("Closing Window={window_id:?}"); + self.windows.remove(&window_id); + }, + WindowEvent::ModifiersChanged(modifiers) => { + window.modifiers = modifiers.state(); + info!("Modifiers changed to {:?}", window.modifiers); + }, + WindowEvent::MouseWheel { delta, .. } => match delta { + MouseScrollDelta::LineDelta(x, y) => { + info!("Mouse wheel Line Delta: ({x},{y})"); + }, + MouseScrollDelta::PixelDelta(px) => { + info!("Mouse wheel Pixel Delta: ({},{})", px.x, px.y); + }, + }, + WindowEvent::KeyboardInput { event, is_synthetic: false, .. } => { + let mods = window.modifiers; + + // Dispatch actions only on press. + if event.state.is_pressed() { + let action = if let Key::Character(ch) = event.logical_key.as_ref() { + Self::process_key_binding(&ch.to_uppercase(), &mods) + } else { + None + }; + + if let Some(action) = action { + self.handle_action(event_loop, window_id, action); + } + } + }, + WindowEvent::MouseInput { button, state, .. } => { + let mods = window.modifiers; + if let Some(action) = + state.is_pressed().then(|| Self::process_mouse_binding(button, &mods)).flatten() + { + self.handle_action(event_loop, window_id, action); + } + }, + WindowEvent::CursorLeft { .. } => { + info!("Cursor left Window={window_id:?}"); + window.cursor_left(); + }, + WindowEvent::CursorMoved { position, .. } => { + info!("Moved cursor to {position:?}"); + window.cursor_moved(position); + }, + WindowEvent::ActivationTokenDone { token: _token, .. } => { + #[cfg(any(x11_platform, wayland_platform))] + { + startup_notify::set_activation_token_env(_token); + if let Err(err) = self.create_window(event_loop, None) { + error!("Error creating new window: {err}"); + } + } + }, + WindowEvent::Ime(event) => match event { + Ime::Enabled => info!("IME enabled for Window={window_id:?}"), + Ime::Preedit(text, caret_pos) => { + info!("Preedit: {}, with caret at {:?}", text, caret_pos); + }, + Ime::Commit(text) => { + info!("Committed: {}", text); + }, + Ime::Disabled => info!("IME disabled for Window={window_id:?}"), + }, + WindowEvent::PinchGesture { delta, .. } => { + window.zoom += delta; + let zoom = window.zoom; + if delta > 0.0 { + info!("Zoomed in {delta:.5} (now: {zoom:.5})"); + } else { + info!("Zoomed out {delta:.5} (now: {zoom:.5})"); + } + }, + WindowEvent::RotationGesture { delta, .. } => { + window.rotated += delta; + let rotated = window.rotated; + if delta > 0.0 { + info!("Rotated counterclockwise {delta:.5} (now: {rotated:.5})"); + } else { + info!("Rotated clockwise {delta:.5} (now: {rotated:.5})"); + } + }, + WindowEvent::PanGesture { delta, phase, .. } => { + window.panned.x += delta.x; + window.panned.y += delta.y; + info!("Panned ({delta:?})) (now: {:?}), {phase:?}", window.panned); + }, + WindowEvent::DoubleTapGesture { .. } => { + info!("Smart zoom"); + }, + WindowEvent::TouchpadPressure { .. } + | WindowEvent::HoveredFileCancelled + | WindowEvent::KeyboardInput { .. } + | WindowEvent::CursorEntered { .. } + | WindowEvent::AxisMotion { .. } + | WindowEvent::DroppedFile(_) + | WindowEvent::HoveredFile(_) + | WindowEvent::Destroyed + | WindowEvent::Touch(_) + | WindowEvent::Moved(_) => (), + } + } + + fn device_event( + &mut self, + _event_loop: &ActiveEventLoop, + device_id: DeviceId, + event: DeviceEvent, + ) { + info!("Device {device_id:?} event: {event:?}"); + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + info!("Resumed the event loop"); + self.dump_monitors(event_loop); + + // Create initial window. + self.create_window(event_loop, None).expect("failed to create initial window"); + + self.print_help(); + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if self.windows.is_empty() { + info!("No windows left, exiting..."); + event_loop.exit(); + } + } + + #[cfg(not(any(android_platform, ios_platform)))] + fn exiting(&mut self, _event_loop: &ActiveEventLoop) { + // We must drop the context here. + self.context = None; + } +} + +/// State of the window. +struct WindowState { + /// IME input. + ime: bool, + /// Render surface. + /// + /// NOTE: This surface must be dropped before the `Window`. + #[cfg(not(any(android_platform, ios_platform)))] + surface: Surface, Arc>, + /// The actual winit Window. + window: Arc, + /// The window theme we're drawing with. + theme: Theme, + /// Cursor position over the window. + cursor_position: Option>, + /// Window modifiers state. + modifiers: ModifiersState, + /// Occlusion state of the window. + occluded: bool, + /// Current cursor grab mode. + cursor_grab: CursorGrabMode, + /// The amount of zoom into window. + zoom: f64, + /// The amount of rotation of the window. + rotated: f32, + /// The amount of pan of the window. + panned: PhysicalPosition, + + #[cfg(macos_platform)] + option_as_alt: OptionAsAlt, + + // Cursor states. + named_idx: usize, + custom_idx: usize, + cursor_hidden: bool, +} + +impl WindowState { + fn new(app: &Application, window: Window) -> Result> { + let window = Arc::new(window); + + // SAFETY: the surface is dropped before the `window` which provided it with handle, thus + // it doesn't outlive it. + #[cfg(not(any(android_platform, ios_platform)))] + let surface = Surface::new(app.context.as_ref().unwrap(), Arc::clone(&window))?; + + let theme = window.theme().unwrap_or(Theme::Dark); + info!("Theme: {theme:?}"); + let named_idx = 0; + window.set_cursor(CURSORS[named_idx]); + + // Allow IME out of the box. + let ime = true; + window.set_ime_allowed(ime); + + let size = window.inner_size(); + let mut state = Self { + #[cfg(macos_platform)] + option_as_alt: window.option_as_alt(), + custom_idx: app.custom_cursors.len() - 1, + cursor_grab: CursorGrabMode::None, + named_idx, + #[cfg(not(any(android_platform, ios_platform)))] + surface, + window, + theme, + ime, + cursor_position: Default::default(), + cursor_hidden: Default::default(), + modifiers: Default::default(), + occluded: Default::default(), + rotated: Default::default(), + panned: Default::default(), + zoom: Default::default(), + }; + + state.resize(size); + Ok(state) + } + + pub fn toggle_ime(&mut self) { + self.ime = !self.ime; + self.window.set_ime_allowed(self.ime); + if let Some(position) = self.ime.then_some(self.cursor_position).flatten() { + self.window.set_ime_cursor_area(position, PhysicalSize::new(20, 20)); + } + } + + pub fn minimize(&mut self) { + self.window.set_minimized(true); + } + + pub fn cursor_moved(&mut self, position: PhysicalPosition) { + self.cursor_position = Some(position); + if self.ime { + self.window.set_ime_cursor_area(position, PhysicalSize::new(20, 20)); + } + } + + pub fn cursor_left(&mut self) { + self.cursor_position = None; + } + + /// Toggle maximized. + fn toggle_maximize(&self) { + let maximized = self.window.is_maximized(); + self.window.set_maximized(!maximized); + } + + /// Toggle window decorations. + fn toggle_decorations(&self) { + let decorated = self.window.is_decorated(); + self.window.set_decorations(!decorated); + } + + /// Toggle window resizable state. + fn toggle_resizable(&self) { + let resizable = self.window.is_resizable(); + self.window.set_resizable(!resizable); + } + + /// Toggle cursor visibility + fn toggle_cursor_visibility(&mut self) { + self.cursor_hidden = !self.cursor_hidden; + self.window.set_cursor_visible(!self.cursor_hidden); + } + + /// Toggle resize increments on a window. + fn toggle_resize_increments(&mut self) { + let new_increments = match self.window.resize_increments() { + Some(_) => None, + None => Some(LogicalSize::new(25.0, 25.0)), + }; + info!("Had increments: {}", new_increments.is_none()); + self.window.set_resize_increments(new_increments); + } + + /// Toggle fullscreen. + fn toggle_fullscreen(&self) { + let fullscreen = if self.window.fullscreen().is_some() { + None + } else { + Some(Fullscreen::Borderless(None)) + }; + + self.window.set_fullscreen(fullscreen); + } + + /// Cycle through the grab modes ignoring errors. + fn cycle_cursor_grab(&mut self) { + self.cursor_grab = match self.cursor_grab { + CursorGrabMode::None => CursorGrabMode::Confined, + CursorGrabMode::Confined => CursorGrabMode::Locked, + CursorGrabMode::Locked => CursorGrabMode::None, + }; + info!("Changing cursor grab mode to {:?}", self.cursor_grab); + if let Err(err) = self.window.set_cursor_grab(self.cursor_grab) { + error!("Error setting cursor grab: {err}"); + } + } + + #[cfg(macos_platform)] + fn cycle_option_as_alt(&mut self) { + self.option_as_alt = match self.option_as_alt { + OptionAsAlt::None => OptionAsAlt::OnlyLeft, + OptionAsAlt::OnlyLeft => OptionAsAlt::OnlyRight, + OptionAsAlt::OnlyRight => OptionAsAlt::Both, + OptionAsAlt::Both => OptionAsAlt::None, + }; + info!("Setting option as alt {:?}", self.option_as_alt); + self.window.set_option_as_alt(self.option_as_alt); + } + + /// Swap the window dimensions with `request_inner_size`. + fn swap_dimensions(&mut self) { + let old_inner_size = self.window.inner_size(); + let mut inner_size = old_inner_size; + + mem::swap(&mut inner_size.width, &mut inner_size.height); + info!("Requesting resize from {old_inner_size:?} to {inner_size:?}"); + + if let Some(new_inner_size) = self.window.request_inner_size(inner_size) { + if old_inner_size == new_inner_size { + info!("Inner size change got ignored"); + } else { + self.resize(new_inner_size); + } + } else { + info!("Request inner size is asynchronous"); + } + } + + /// Pick the next cursor. + fn next_cursor(&mut self) { + self.named_idx = (self.named_idx + 1) % CURSORS.len(); + info!("Setting cursor to \"{:?}\"", CURSORS[self.named_idx]); + self.window.set_cursor(Cursor::Icon(CURSORS[self.named_idx])); + } + + /// Pick the next custom cursor. + fn next_custom_cursor(&mut self, custom_cursors: &[CustomCursor]) { + self.custom_idx = (self.custom_idx + 1) % custom_cursors.len(); + let cursor = Cursor::Custom(custom_cursors[self.custom_idx].clone()); + self.window.set_cursor(cursor); + } + + /// Custom cursor from an URL. + #[cfg(web_platform)] + fn url_custom_cursor(&mut self, event_loop: &ActiveEventLoop) { + let cursor = event_loop.create_custom_cursor(url_custom_cursor()); + + self.window.set_cursor(cursor); + } + + /// Custom cursor from a URL. + #[cfg(web_platform)] + fn animation_custom_cursor( + &mut self, + event_loop: &ActiveEventLoop, + custom_cursors: &[CustomCursor], + ) { + use std::time::Duration; + use winit::platform::web::CustomCursorExtWebSys; + + let cursors = vec![ + custom_cursors[0].clone(), + custom_cursors[1].clone(), + event_loop.create_custom_cursor(url_custom_cursor()), + ]; + let cursor = CustomCursor::from_animation(Duration::from_secs(3), cursors).unwrap(); + let cursor = event_loop.create_custom_cursor(cursor); + + self.window.set_cursor(cursor); + } + + /// Resize the window to the new size. + fn resize(&mut self, size: PhysicalSize) { + info!("Resized to {size:?}"); + #[cfg(not(any(android_platform, ios_platform)))] + { + let (width, height) = match (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + { + (Some(width), Some(height)) => (width, height), + _ => return, + }; + self.surface.resize(width, height).expect("failed to resize inner buffer"); + } + self.window.request_redraw(); + } + + /// Change the theme that things are drawn in. + fn set_draw_theme(&mut self, theme: Theme) { + self.theme = theme; + self.window.request_redraw(); + } + + /// Show window menu. + fn show_menu(&self) { + if let Some(position) = self.cursor_position { + self.window.show_window_menu(position); + } + } + + /// Drag the window. + fn drag_window(&self) { + if let Err(err) = self.window.drag_window() { + info!("Error starting window drag: {err}"); + } else { + info!("Dragging window Window={:?}", self.window.id()); + } + } + + /// Drag-resize the window. + fn drag_resize_window(&self) { + let position = match self.cursor_position { + Some(position) => position, + None => { + info!("Drag-resize requires cursor to be inside the window"); + return; + }, + }; + + let win_size = self.window.inner_size(); + let border_size = BORDER_SIZE * self.window.scale_factor(); + + let x_direction = if position.x < border_size { + ResizeDirection::West + } else if position.x > (win_size.width as f64 - border_size) { + ResizeDirection::East + } else { + // Use arbitrary direction instead of None for simplicity. + ResizeDirection::SouthEast + }; + + let y_direction = if position.y < border_size { + ResizeDirection::North + } else if position.y > (win_size.height as f64 - border_size) { + ResizeDirection::South + } else { + // Use arbitrary direction instead of None for simplicity. + ResizeDirection::SouthEast + }; + + let direction = match (x_direction, y_direction) { + (ResizeDirection::West, ResizeDirection::North) => ResizeDirection::NorthWest, + (ResizeDirection::West, ResizeDirection::South) => ResizeDirection::SouthWest, + (ResizeDirection::West, _) => ResizeDirection::West, + (ResizeDirection::East, ResizeDirection::North) => ResizeDirection::NorthEast, + (ResizeDirection::East, ResizeDirection::South) => ResizeDirection::SouthEast, + (ResizeDirection::East, _) => ResizeDirection::East, + (_, ResizeDirection::South) => ResizeDirection::South, + (_, ResizeDirection::North) => ResizeDirection::North, + _ => return, + }; + + if let Err(err) = self.window.drag_resize_window(direction) { + info!("Error starting window drag-resize: {err}"); + } else { + info!("Drag-resizing window Window={:?}", self.window.id()); + } + } + + /// Change window occlusion state. + fn set_occluded(&mut self, occluded: bool) { + self.occluded = occluded; + if !occluded { + self.window.request_redraw(); + } + } + + /// Draw the window contents. + #[cfg(not(any(android_platform, ios_platform)))] + fn draw(&mut self) -> Result<(), Box> { + if self.occluded { + info!("Skipping drawing occluded window={:?}", self.window.id()); + return Ok(()); + } + + const WHITE: u32 = 0xffffffff; + const DARK_GRAY: u32 = 0xff181818; + + let color = match self.theme { + Theme::Light => WHITE, + Theme::Dark => DARK_GRAY, + }; + + let mut buffer = self.surface.buffer_mut()?; + buffer.fill(color); + self.window.pre_present_notify(); + buffer.present()?; + Ok(()) + } + + #[cfg(any(android_platform, ios_platform))] + fn draw(&mut self) -> Result<(), Box> { + info!("Drawing but without rendering..."); + Ok(()) + } +} + +struct Binding { + trigger: T, + mods: ModifiersState, + action: Action, +} + +impl Binding { + const fn new(trigger: T, mods: ModifiersState, action: Action) -> Self { + Self { trigger, mods, action } + } + + fn is_triggered_by(&self, trigger: &T, mods: &ModifiersState) -> bool { + &self.trigger == trigger && &self.mods == mods + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Action { + CloseWindow, + ToggleCursorVisibility, + CreateNewWindow, + ToggleResizeIncrements, + ToggleImeInput, + ToggleDecorations, + ToggleResizable, + ToggleFullscreen, + ToggleMaximize, + Minimize, + NextCursor, + NextCustomCursor, + #[cfg(web_platform)] + UrlCustomCursor, + #[cfg(web_platform)] + AnimationCustomCursor, + CycleCursorGrab, + PrintHelp, + DragWindow, + DragResizeWindow, + ShowWindowMenu, + #[cfg(macos_platform)] + CycleOptionAsAlt, + SetTheme(Option), + #[cfg(macos_platform)] + CreateNewTab, + RequestResize, +} + +impl Action { + fn help(&self) -> &'static str { + match self { + Action::CloseWindow => "Close window", + Action::ToggleCursorVisibility => "Hide cursor", + Action::CreateNewWindow => "Create new window", + Action::ToggleImeInput => "Toggle IME input", + Action::ToggleDecorations => "Toggle decorations", + Action::ToggleResizable => "Toggle window resizable state", + Action::ToggleFullscreen => "Toggle fullscreen", + Action::ToggleMaximize => "Maximize", + Action::Minimize => "Minimize", + Action::ToggleResizeIncrements => "Use resize increments when resizing window", + Action::NextCursor => "Advance the cursor to the next value", + Action::NextCustomCursor => "Advance custom cursor to the next value", + #[cfg(web_platform)] + Action::UrlCustomCursor => "Custom cursor from an URL", + #[cfg(web_platform)] + Action::AnimationCustomCursor => "Custom cursor from an animation", + Action::CycleCursorGrab => "Cycle through cursor grab mode", + Action::PrintHelp => "Print help", + Action::DragWindow => "Start window drag", + Action::DragResizeWindow => "Start window drag-resize", + Action::ShowWindowMenu => "Show window menu", + #[cfg(macos_platform)] + Action::CycleOptionAsAlt => "Cycle option as alt mode", + Action::SetTheme(None) => "Change to the system theme", + Action::SetTheme(Some(Theme::Light)) => "Change to a light theme", + Action::SetTheme(Some(Theme::Dark)) => "Change to a dark theme", + #[cfg(macos_platform)] + Action::CreateNewTab => "Create new tab", + Action::RequestResize => "Request a resize", + } + } +} + +impl fmt::Display for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&self, f) + } +} + +fn decode_cursor(bytes: &[u8]) -> CustomCursorSource { + let img = image::load_from_memory(bytes).unwrap().to_rgba8(); + let samples = img.into_flat_samples(); + let (_, w, h) = samples.extents(); + let (w, h) = (w as u16, h as u16); + CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap() +} + +#[cfg(web_platform)] +fn url_custom_cursor() -> CustomCursorSource { + use std::sync::atomic::{AtomicU64, Ordering}; + + use winit::platform::web::CustomCursorExtWebSys; + + static URL_COUNTER: AtomicU64 = AtomicU64::new(0); + + CustomCursor::from_url( + format!("https://picsum.photos/128?random={}", URL_COUNTER.fetch_add(1, Ordering::Relaxed)), + 64, + 64, + ) +} + +fn load_icon(bytes: &[u8]) -> Icon { + let (icon_rgba, icon_width, icon_height) = { + let image = image::load_from_memory(bytes).unwrap().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") +} + +fn modifiers_to_string(mods: ModifiersState) -> String { + let mut mods_line = String::new(); + // Always add + since it's printed as a part of the bindings. + for (modifier, desc) in [ + (ModifiersState::SUPER, "Super+"), + (ModifiersState::ALT, "Alt+"), + (ModifiersState::CONTROL, "Ctrl+"), + (ModifiersState::SHIFT, "Shift+"), + ] { + if !mods.contains(modifier) { + continue; + } + + mods_line.push_str(desc); + } + mods_line +} + +fn mouse_button_to_string(button: MouseButton) -> &'static str { + match button { + MouseButton::Left => "LMB", + MouseButton::Right => "RMB", + MouseButton::Middle => "MMB", + MouseButton::Back => "Back", + MouseButton::Forward => "Forward", + MouseButton::Other(_) => "", + } +} + +/// Cursor list to cycle through. +const CURSORS: &[CursorIcon] = &[ + CursorIcon::Default, + CursorIcon::Crosshair, + CursorIcon::Pointer, + 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, +]; + +const KEY_BINDINGS: &[Binding<&'static str>] = &[ + Binding::new("Q", ModifiersState::CONTROL, Action::CloseWindow), + Binding::new("H", ModifiersState::CONTROL, Action::PrintHelp), + Binding::new("F", ModifiersState::CONTROL, Action::ToggleFullscreen), + Binding::new("D", ModifiersState::CONTROL, Action::ToggleDecorations), + Binding::new("I", ModifiersState::CONTROL, Action::ToggleImeInput), + Binding::new("L", ModifiersState::CONTROL, Action::CycleCursorGrab), + Binding::new("P", ModifiersState::CONTROL, Action::ToggleResizeIncrements), + Binding::new("R", ModifiersState::CONTROL, Action::ToggleResizable), + Binding::new("R", ModifiersState::ALT, Action::RequestResize), + // M. + Binding::new("M", ModifiersState::CONTROL, Action::ToggleMaximize), + Binding::new("M", ModifiersState::ALT, Action::Minimize), + // N. + Binding::new("N", ModifiersState::CONTROL, Action::CreateNewWindow), + // C. + Binding::new("C", ModifiersState::CONTROL, Action::NextCursor), + Binding::new("C", ModifiersState::ALT, Action::NextCustomCursor), + #[cfg(web_platform)] + Binding::new( + "C", + ModifiersState::CONTROL.union(ModifiersState::SHIFT), + Action::UrlCustomCursor, + ), + #[cfg(web_platform)] + Binding::new( + "C", + ModifiersState::ALT.union(ModifiersState::SHIFT), + Action::AnimationCustomCursor, + ), + Binding::new("Z", ModifiersState::CONTROL, Action::ToggleCursorVisibility), + // K. + Binding::new("K", ModifiersState::empty(), Action::SetTheme(None)), + Binding::new("K", ModifiersState::SUPER, Action::SetTheme(Some(Theme::Light))), + Binding::new("K", ModifiersState::CONTROL, Action::SetTheme(Some(Theme::Dark))), + #[cfg(macos_platform)] + Binding::new("T", ModifiersState::SUPER, Action::CreateNewTab), + #[cfg(macos_platform)] + Binding::new("O", ModifiersState::CONTROL, Action::CycleOptionAsAlt), +]; + +const MOUSE_BINDINGS: &[Binding] = &[ + Binding::new(MouseButton::Left, ModifiersState::ALT, Action::DragResizeWindow), + Binding::new(MouseButton::Left, ModifiersState::CONTROL, Action::DragWindow), + Binding::new(MouseButton::Right, ModifiersState::CONTROL, Action::ShowWindowMenu), +]; diff --git a/third_party/winit-0.30.13/examples/x11_embed.rs b/third_party/winit-0.30.13/examples/x11_embed.rs new file mode 100644 index 0000000..9db55e5 --- /dev/null +++ b/third_party/winit-0.30.13/examples/x11_embed.rs @@ -0,0 +1,69 @@ +//! A demonstration of embedding a winit window in an existing X11 application. +use std::error::Error; + +#[cfg(x11_platform)] +fn main() -> Result<(), Box> { + use winit::application::ApplicationHandler; + use winit::event::WindowEvent; + use winit::event_loop::{ActiveEventLoop, EventLoop}; + use winit::platform::x11::WindowAttributesExtX11; + use winit::window::{Window, WindowId}; + + #[path = "util/fill.rs"] + mod fill; + + pub struct XEmbedDemo { + parent_window_id: u32, + window: Option, + } + + impl ApplicationHandler for XEmbedDemo { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes() + .with_title("An embedded window!") + .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0)) + .with_embed_parent_window(self.parent_window_id); + + self.window = Some(event_loop.create_window(window_attributes).unwrap()); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + let window = self.window.as_ref().unwrap(); + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::RedrawRequested => { + window.pre_present_notify(); + fill::fill_window(window); + }, + _ => (), + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + self.window.as_ref().unwrap().request_redraw(); + } + } + + // First argument should be a 32-bit X11 window ID. + let parent_window_id = std::env::args() + .nth(1) + .ok_or("Expected a 32-bit X11 window ID as the first argument.")? + .parse::()?; + + tracing_subscriber::fmt::init(); + let event_loop = EventLoop::new()?; + + let mut app = XEmbedDemo { parent_window_id, window: None }; + event_loop.run_app(&mut app).map_err(Into::into) +} + +#[cfg(not(x11_platform))] +fn main() -> Result<(), Box> { + println!("This example is only supported on X11 platforms."); + Ok(()) +} diff --git a/third_party/winit-0.30.13/src/application.rs b/third_party/winit-0.30.13/src/application.rs new file mode 100644 index 0000000..977a7c7 --- /dev/null +++ b/third_party/winit-0.30.13/src/application.rs @@ -0,0 +1,339 @@ +//! End user application handling. + +use crate::event::{DeviceEvent, DeviceId, StartCause, WindowEvent}; +use crate::event_loop::ActiveEventLoop; +use crate::window::WindowId; + +/// The handler of the application events. +pub trait ApplicationHandler { + /// Emitted when new events arrive from the OS to be processed. + /// + /// This is a useful 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`] to see if a timer set by + /// [`ControlFlow::WaitUntil`][crate::event_loop::ControlFlow::WaitUntil] has elapsed. + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { + let _ = (event_loop, cause); + } + + /// Emitted when the application has been resumed. + /// + /// For consistency, all platforms emit a `Resumed` event even if they don't themselves have a + /// formal suspend/resume lifecycle. For systems without a formal suspend/resume lifecycle + /// the `Resumed` event is always emitted after the + /// [`NewEvents(StartCause::Init)`][StartCause::Init] event. + /// + /// # Portability + /// + /// It's recommended that applications should only initialize their graphics context and create + /// a window after they have received their first `Resumed` event. Some systems + /// (specifically Android) won't allow applications to create a render surface until they are + /// resumed. + /// + /// Considering that the implementation of [`Suspended`] and `Resumed` events may be internally + /// driven by multiple platform-specific events, and that there may be subtle differences across + /// platforms with how these internal events are delivered, it's recommended that applications + /// be able to gracefully handle redundant (i.e. back-to-back) [`Suspended`] or `Resumed` + /// events. + /// + /// Also see [`Suspended`] notes. + /// + /// ## Android + /// + /// On Android, the `Resumed` event is sent when a new [`SurfaceView`] has been created. This is + /// expected to closely correlate with the [`onResume`] lifecycle event but there may + /// technically be a discrepancy. + /// + /// [`onResume`]: https://developer.android.com/reference/android/app/Activity#onResume() + /// + /// Applications that need to run on Android must wait until they have been `Resumed` + /// before they will be able to create a render surface (such as an `EGLSurface`, + /// [`VkSurfaceKHR`] or [`wgpu::Surface`]) which depend on having a + /// [`SurfaceView`]. Applications must also assume that if they are [`Suspended`], then their + /// render surfaces are invalid and should be dropped. + /// + /// Also see [`Suspended`] notes. + /// + /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView + /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle + /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html + /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html + /// + /// ## iOS + /// + /// On iOS, the `Resumed` event is emitted in response to an [`applicationDidBecomeActive`] + /// callback which means the application is "active" (according to the + /// [iOS application lifecycle]). + /// + /// [`applicationDidBecomeActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622956-applicationdidbecomeactive + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// ## Web + /// + /// On Web, the `Resumed` event is emitted in response to a [`pageshow`] event + /// with the property [`persisted`] being true, which means that the page is being + /// restored from the [`bfcache`] (back/forward cache) - an in-memory cache that + /// stores a complete snapshot of a page (including the JavaScript heap) as the + /// user is navigating away. + /// + /// [`pageshow`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/pageshow_event + /// [`persisted`]: https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted + /// [`bfcache`]: https://web.dev/bfcache/ + /// [`Suspended`]: Self::suspended + fn resumed(&mut self, event_loop: &ActiveEventLoop); + + /// Emitted when an event is sent from [`EventLoopProxy::send_event`]. + /// + /// [`EventLoopProxy::send_event`]: crate::event_loop::EventLoopProxy::send_event + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: T) { + let _ = (event_loop, event); + } + + /// Emitted when the OS sends an event to a winit window. + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ); + + /// Emitted when the OS sends an event to a device. + fn device_event( + &mut self, + event_loop: &ActiveEventLoop, + device_id: DeviceId, + event: DeviceEvent, + ) { + let _ = (event_loop, device_id, event); + } + + /// Emitted when the event loop is about to block and wait for new events. + /// + /// Most applications shouldn't need to hook into this event since there is no real relationship + /// between how often the event loop needs to wake up and the dispatching of any specific + /// events. + /// + /// High frequency event sources, such as input devices could potentially lead to lots of wake + /// ups and also lots of corresponding `AboutToWait` events. + /// + /// This is not an ideal event to drive application rendering from and instead applications + /// should render in response to [`WindowEvent::RedrawRequested`] events. + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } + + /// Emitted when the application has been suspended. + /// + /// # Portability + /// + /// Not all platforms support the notion of suspending applications, and there may be no + /// technical way to guarantee being able to emit a `Suspended` event if the OS has + /// no formal application lifecycle (currently only Android, iOS, and Web do). For this reason, + /// Winit does not currently try to emit pseudo `Suspended` events before the application + /// quits on platforms without an application lifecycle. + /// + /// Considering that the implementation of `Suspended` and [`Resumed`] events may be internally + /// driven by multiple platform-specific events, and that there may be subtle differences across + /// platforms with how these internal events are delivered, it's recommended that applications + /// be able to gracefully handle redundant (i.e. back-to-back) `Suspended` or [`Resumed`] + /// events. + /// + /// Also see [`Resumed`] notes. + /// + /// ## Android + /// + /// On Android, the `Suspended` event is only sent when the application's associated + /// [`SurfaceView`] is destroyed. This is expected to closely correlate with the [`onPause`] + /// lifecycle event but there may technically be a discrepancy. + /// + /// [`onPause`]: https://developer.android.com/reference/android/app/Activity#onPause() + /// + /// Applications that need to run on Android should assume their [`SurfaceView`] has been + /// destroyed, which indirectly invalidates any existing render surfaces that may have been + /// created outside of Winit (such as an `EGLSurface`, [`VkSurfaceKHR`] or [`wgpu::Surface`]). + /// + /// After being `Suspended` on Android applications must drop all render surfaces before + /// the event callback completes, which may be re-created when the application is next + /// [`Resumed`]. + /// + /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView + /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle + /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html + /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html + /// + /// ## iOS + /// + /// On iOS, the `Suspended` event is currently emitted in response to an + /// [`applicationWillResignActive`] callback which means that the application is + /// about to transition from the active to inactive state (according to the + /// [iOS application lifecycle]). + /// + /// [`applicationWillResignActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622950-applicationwillresignactive + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// ## Web + /// + /// On Web, the `Suspended` event is emitted in response to a [`pagehide`] event + /// with the property [`persisted`] being true, which means that the page is being + /// put in the [`bfcache`] (back/forward cache) - an in-memory cache that stores a + /// complete snapshot of a page (including the JavaScript heap) as the user is + /// navigating away. + /// + /// [`pagehide`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event + /// [`persisted`]: https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted + /// [`bfcache`]: https://web.dev/bfcache/ + /// [`Resumed`]: Self::resumed + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } + + /// Emitted when the event loop is being shut down. + /// + /// This is irreversible - if this method is called, it is guaranteed that the event loop + /// will exit right after. + fn exiting(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } + + /// Emitted when the application has received a memory warning. + /// + /// ## Platform-specific + /// + /// ### Android + /// + /// On Android, the `MemoryWarning` event is sent when [`onLowMemory`] was called. The + /// application must [release memory] or risk being killed. + /// + /// [`onLowMemory`]: https://developer.android.com/reference/android/app/Application.html#onLowMemory() + /// [release memory]: https://developer.android.com/topic/performance/memory#release + /// + /// ### iOS + /// + /// On iOS, the `MemoryWarning` event is emitted in response to an + /// [`applicationDidReceiveMemoryWarning`] callback. The application must free as much + /// memory as possible or risk being terminated, see [how to respond to memory warnings]. + /// + /// [`applicationDidReceiveMemoryWarning`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623063-applicationdidreceivememorywarni + /// [how to respond to memory warnings]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle/responding_to_memory_warnings + /// + /// ### Others + /// + /// - **macOS / Orbital / Wayland / Web / Windows:** Unsupported. + fn memory_warning(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } +} + +impl, T: 'static> ApplicationHandler for &mut A { + #[inline] + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { + (**self).new_events(event_loop, cause); + } + + #[inline] + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + (**self).resumed(event_loop); + } + + #[inline] + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: T) { + (**self).user_event(event_loop, event); + } + + #[inline] + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + (**self).window_event(event_loop, window_id, event); + } + + #[inline] + fn device_event( + &mut self, + event_loop: &ActiveEventLoop, + device_id: DeviceId, + event: DeviceEvent, + ) { + (**self).device_event(event_loop, device_id, event); + } + + #[inline] + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + (**self).about_to_wait(event_loop); + } + + #[inline] + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + (**self).suspended(event_loop); + } + + #[inline] + fn exiting(&mut self, event_loop: &ActiveEventLoop) { + (**self).exiting(event_loop); + } + + #[inline] + fn memory_warning(&mut self, event_loop: &ActiveEventLoop) { + (**self).memory_warning(event_loop); + } +} + +impl, T: 'static> ApplicationHandler for Box { + #[inline] + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { + (**self).new_events(event_loop, cause); + } + + #[inline] + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + (**self).resumed(event_loop); + } + + #[inline] + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: T) { + (**self).user_event(event_loop, event); + } + + #[inline] + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + (**self).window_event(event_loop, window_id, event); + } + + #[inline] + fn device_event( + &mut self, + event_loop: &ActiveEventLoop, + device_id: DeviceId, + event: DeviceEvent, + ) { + (**self).device_event(event_loop, device_id, event); + } + + #[inline] + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + (**self).about_to_wait(event_loop); + } + + #[inline] + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + (**self).suspended(event_loop); + } + + #[inline] + fn exiting(&mut self, event_loop: &ActiveEventLoop) { + (**self).exiting(event_loop); + } + + #[inline] + fn memory_warning(&mut self, event_loop: &ActiveEventLoop) { + (**self).memory_warning(event_loop); + } +} diff --git a/third_party/winit-0.30.13/src/changelog/mod.rs b/third_party/winit-0.30.13/src/changelog/mod.rs new file mode 100644 index 0000000..a05ea9c --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/mod.rs @@ -0,0 +1,77 @@ +//! # Changelog and migrations +//! +//! All notable changes to this project will be documented in this module, +//! along with migration instructions for larger changes. +// Put the current entry at the top of this page, for discoverability. +// See `.cargo/config.toml` for details about `unreleased_changelogs`. +#![cfg_attr(unreleased_changelogs, doc = include_str!("unreleased.md"))] +#![cfg_attr(not(unreleased_changelogs), doc = include_str!("v0.30.md"))] + +#[doc = include_str!("v0.30.md")] +pub mod v0_30 {} + +#[doc = include_str!("v0.29.md")] +pub mod v0_29 {} + +#[doc = include_str!("v0.28.md")] +pub mod v0_28 {} + +#[doc = include_str!("v0.27.md")] +pub mod v0_27 {} + +#[doc = include_str!("v0.26.md")] +pub mod v0_26 {} + +#[doc = include_str!("v0.25.md")] +pub mod v0_25 {} + +#[doc = include_str!("v0.24.md")] +pub mod v0_24 {} + +#[doc = include_str!("v0.23.md")] +pub mod v0_23 {} + +#[doc = include_str!("v0.22.md")] +pub mod v0_22 {} + +#[doc = include_str!("v0.21.md")] +pub mod v0_21 {} + +#[doc = include_str!("v0.20.md")] +pub mod v0_20 {} + +#[doc = include_str!("v0.19.md")] +pub mod v0_19 {} + +#[doc = include_str!("v0.18.md")] +pub mod v0_18 {} + +#[doc = include_str!("v0.17.md")] +pub mod v0_17 {} + +#[doc = include_str!("v0.16.md")] +pub mod v0_16 {} + +#[doc = include_str!("v0.15.md")] +pub mod v0_15 {} + +#[doc = include_str!("v0.14.md")] +pub mod v0_14 {} + +#[doc = include_str!("v0.13.md")] +pub mod v0_13 {} + +#[doc = include_str!("v0.12.md")] +pub mod v0_12 {} + +#[doc = include_str!("v0.11.md")] +pub mod v0_11 {} + +#[doc = include_str!("v0.10.md")] +pub mod v0_10 {} + +#[doc = include_str!("v0.9.md")] +pub mod v0_9 {} + +#[doc = include_str!("v0.8.md")] +pub mod v0_8 {} diff --git a/third_party/winit-0.30.13/src/changelog/unreleased.md b/third_party/winit-0.30.13/src/changelog/unreleased.md new file mode 100644 index 0000000..f3a0f6d --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/unreleased.md @@ -0,0 +1,41 @@ +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +The sections should follow the order `Added`, `Changed`, `Deprecated`, +`Removed`, and `Fixed`. + +Platform specific changed should be added to the end of the section and grouped +by platform name. Common API additions should have `, implemented` at the end +for platforms where the API was initially implemented. See the following example +on how to add them: + +```md +### Added + +- Add `Window::turbo()`, implemented on X11, Wayland, and Web. +- On X11, add `Window::some_rare_api`. +- On X11, add `Window::even_more_rare_api`. +- On Wayland, add `Window::common_api`. +- On Windows, add `Window::some_rare_api`. +``` + +When the change requires non-trivial amount of work for users to comply +with it, the migration guide should be added below the entry, like: + +```md +- Deprecate `Window` creation outside of `EventLoop::run` + + This was done to simply migration in the future. Consider the + following code: + + // Code snippet. + + To migrate it we should do X, Y, and then Z, for example: + + // Code snippet. + +``` + +The migration guide could reference other migration examples in the current +changelog entry. + +## Unreleased diff --git a/third_party/winit-0.30.13/src/changelog/v0.10.md b/third_party/winit-0.30.13/src/changelog/v0.10.md new file mode 100644 index 0000000..9e47c0b --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.10.md @@ -0,0 +1,13 @@ +## 0.10.1 + +_Yanked_ + +## 0.10.0 + +- Add support for `Touch` for emscripten backend. +- Added support for `DroppedFile`, `HoveredFile`, and `HoveredFileCancelled` to X11 backend. +- **Breaking:** `unix::WindowExt` no longer returns pointers for things that aren't actually pointers; `get_xlib_window` now returns `Option` and `get_xlib_screen_id` returns `Option`. Additionally, methods that previously returned `libc::c_void` have been changed to return `std::os::raw::c_void`, which are not interchangeable types, so users wanting the former will need to explicitly cast. +- Added `set_decorations` method to `Window` to allow decorations to be toggled after the window is built. Presently only implemented on X11. +- Raised the minimum supported version of Rust to 1.20 on MacOS due to usage of associated constants in new versions of cocoa and core-graphics. +- Added `modifiers` field to `MouseInput`, `MouseWheel`, and `CursorMoved` events to track the modifiers state (`ModifiersState`). +- Fixed the emscripten backend to return the size of the canvas instead of the size of the window. diff --git a/third_party/winit-0.30.13/src/changelog/v0.11.md b/third_party/winit-0.30.13/src/changelog/v0.11.md new file mode 100644 index 0000000..31fb1d3 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.11.md @@ -0,0 +1,27 @@ +## 0.11.3 + +- Added `set_min_dimensions` and `set_max_dimensions` methods to `Window`, and implemented on Windows, X11, Wayland, and OSX. +- On X11, dropping a `Window` actually closes it now, and clicking the window's × button (or otherwise having the WM signal to close it) will result in the window closing. +- Added `WindowBuilderExt` methods for macos: `with_titlebar_transparent`, + `with_title_hidden`, `with_titlebar_buttons_hidden`, + `with_fullsize_content_view`. +- Mapped X11 numpad keycodes (arrows, Home, End, PageUp, PageDown, Insert and Delete) to corresponding virtual keycodes + +## 0.11.2 + +- Impl `Hash`, `PartialEq`, and `Eq` for `events::ModifiersState`. +- Implement `MonitorId::get_hidpi_factor` for MacOS. +- Added method `os::macos::MonitorIdExt::get_nsscreen() -> *mut c_void` that gets a `NSScreen` object matching the monitor ID. +- Send `Awakened` event on Android when event loop is woken up. + +## 0.11.1 + +- Fixed windows not receiving mouse events when click-dragging the mouse outside the client area of a window, on Windows platforms. +- Added method `os::android::EventsLoopExt:set_suspend_callback(Option ()>>)` that allows glutin to register a callback when a suspend event happens + +## 0.11.0 + +- Implement `MonitorId::get_dimensions` for Android. +- Added method `os::macos::WindowBuilderExt::with_movable_by_window_background(bool)` that allows to move a window without a titlebar - `with_decorations(false)` +- Implement `Window::set_fullscreen`, `Window::set_maximized` and `Window::set_decorations` for Wayland. +- Added `Caret` as VirtualKeyCode and support OSX ^-Key with german input. diff --git a/third_party/winit-0.30.13/src/changelog/v0.12.md b/third_party/winit-0.30.13/src/changelog/v0.12.md new file mode 100644 index 0000000..3f9c83d --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.12.md @@ -0,0 +1,8 @@ +## 0.12.0 + +- Added subclass to macos windows so they can be made resizable even with no decorations. +- Dead keys now work properly on X11, no longer resulting in a panic. +- On X11, input method creation first tries to use the value from the user's `XMODIFIERS` environment variable, so application developers should no longer need to manually call `XSetLocaleModifiers`. If that fails, fallbacks are tried, which should prevent input method initialization from ever outright failing. +- Fixed thread safety issues with input methods on X11. +- Add support for `Touch` for win32 backend. +- Fixed `Window::get_inner_size` and friends to return the size in pixels instead of points when using HIDPI displays on OSX. diff --git a/third_party/winit-0.30.13/src/changelog/v0.13.md b/third_party/winit-0.30.13/src/changelog/v0.13.md new file mode 100644 index 0000000..fd45665 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.13.md @@ -0,0 +1,20 @@ +## 0.13.1 + +- Ensure necessary `x11-dl` version is used. + +## 0.13.0 + +- Implement `WindowBuilder::with_maximized`, `Window::set_fullscreen`, `Window::set_maximized` and `Window::set_decorations` for MacOS. +- Implement `WindowBuilder::with_maximized`, `Window::set_fullscreen`, `Window::set_maximized` and `Window::set_decorations` for Windows. +- On Windows, `WindowBuilder::with_fullscreen` no longer changing monitor display resolution. +- Overhauled X11 window geometry calculations. `get_position` and `set_position` are more universally accurate across different window managers, and `get_outer_size` actually works now. +- Fixed SIGSEGV/SIGILL crashes on macOS caused by stabilization of the `!` (never) type. +- Implement `WindowEvent::HiDPIFactorChanged` for macOS +- On X11, input methods now work completely out of the box, no longer requiring application developers to manually call `setlocale`. Additionally, when input methods are started, stopped, or restarted on the server end, it's correctly handled. +- Implemented `Refresh` event on Windows. +- Properly calculate the minimum and maximum window size on Windows, including window decorations. +- Map more `MouseCursor` variants to cursor icons on Windows. +- Corrected `get_position` on macOS to return outer frame position, not content area position. +- Corrected `set_position` on macOS to set outer frame position, not content area position. +- Added `get_inner_position` method to `Window`, which gets the position of the window's client area. This is implemented on all applicable platforms (all desktop platforms other than Wayland, where this isn't possible). +- **Breaking:** the `Closed` event has been replaced by `CloseRequested` and `Destroyed`. To migrate, you typically just need to replace all usages of `Closed` with `CloseRequested`; see example programs for more info. The exception is iOS, where `Closed` must be replaced by `Destroyed`. diff --git a/third_party/winit-0.30.13/src/changelog/v0.14.md b/third_party/winit-0.30.13/src/changelog/v0.14.md new file mode 100644 index 0000000..822ae20 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.14.md @@ -0,0 +1,21 @@ +## 0.14.0 + +- Created the `Copy`, `Paste` and `Cut` `VirtualKeyCode`s and added support for them on X11 and Wayland +- Fix `.with_decorations(false)` in macOS +- On Mac, `NSWindow` and supporting objects might be alive long after they were `closed` which resulted in apps consuming more heap then needed. Mainly it was affecting multi window applications. Not expecting any user visible change of behaviour after the fix. +- Fix regression of Window platform extensions for macOS where `NSFullSizeContentViewWindowMask` was not being correctly applied to `.fullsize_content_view`. +- Corrected `get_position` on Windows to be relative to the screen rather than to the taskbar. +- Corrected `Moved` event on Windows to use position values equivalent to those returned by `get_position`. It previously supplied client area positions instead of window positions, and would additionally interpret negative values as being very large (around `u16::MAX`). +- Implemented `Moved` event on macOS. +- On X11, the `Moved` event correctly use window positions rather than client area positions. Additionally, a stray `Moved` that unconditionally accompanied `Resized` with the client area position relative to the parent has been eliminated; `Moved` is still received alongside `Resized`, but now only once and always correctly. +- On Windows, implemented all variants of `DeviceEvent` other than `Text`. Mouse `DeviceEvent`s are now received even if the window isn't in the foreground. +- `DeviceId` on Windows is no longer a unit struct, and now contains a `u32`. For `WindowEvent`s, this will always be 0, but on `DeviceEvent`s it will be the handle to that device. `DeviceIdExt::get_persistent_identifier` can be used to acquire a unique identifier for that device that persists across replugs/reboots/etc. +- Corrected `run_forever` on X11 to stop discarding `Awakened` events. +- Various safety and correctness improvements to the X11 backend internals. +- Fixed memory leak on X11 every time the mouse entered the window. +- On X11, drag and drop now works reliably in release mode. +- Added `WindowBuilderExt::with_resize_increments` and `WindowBuilderExt::with_base_size` to X11, allowing for more optional hints to be set. +- Rework of the wayland backend, migrating it to use [Smithay's Client Toolkit](https://github.com/Smithay/client-toolkit). +- Added `WindowBuilder::with_window_icon` and `Window::set_window_icon`, finally making it possible to set the window icon on Windows and X11. The `icon_loading` feature can be enabled to allow for icons to be easily loaded; see example program `window_icon.rs` for usage. +- Windows additionally has `WindowBuilderExt::with_taskbar_icon` and `WindowExt::set_taskbar_icon`. +- On Windows, fix panic when trying to call `set_fullscreen(None)` on a window that has not been fullscreened prior. diff --git a/third_party/winit-0.30.13/src/changelog/v0.15.md b/third_party/winit-0.30.13/src/changelog/v0.15.md new file mode 100644 index 0000000..f5fc485 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.15.md @@ -0,0 +1,42 @@ +## 0.15.1 + +- On X11, the `Moved` event is no longer sent when the window is resized without changing position. +- `MouseCursor` and `CursorState` now implement `Default`. +- `WindowBuilder::with_resizable` implemented for Windows, X11, Wayland, and macOS. +- `Window::set_resizable` implemented for Windows, X11, Wayland, and macOS. +- On X11, if the monitor's width or height in millimeters is reported as 0, the DPI is now 1.0 instead of +inf. +- On X11, the environment variable `WINIT_HIDPI_FACTOR` has been added for overriding DPI factor. +- On X11, enabling transparency no longer causes the window contents to flicker when resizing. +- On X11, `with_override_redirect` now actually enables override redirect. +- macOS now generates `VirtualKeyCode::LAlt` and `VirtualKeyCode::RAlt` instead of `None` for both. +- On macOS, `VirtualKeyCode::RWin` and `VirtualKeyCode::LWin` are no longer switched. +- On macOS, windows without decorations can once again be resized. +- Fixed race conditions when creating an `EventsLoop` on X11, most commonly manifesting as `"[xcb] Unknown sequence number while processing queue"`. +- On macOS, `CursorMoved` and `MouseInput` events are only generated if they occurs within the window's client area. +- On macOS, resizing the window no longer generates a spurious `MouseInput` event. + +## 0.15.0 + +- `Icon::to_cardinals` is no longer public, since it was never supposed to be. +- Wayland: improve diagnostics if initialization fails +- Fix some system event key doesn't work when focused, do not block keyevent forward to system on macOS +- On X11, the scroll wheel position is now correctly reset on i3 and other WMs that have the same quirk. +- On X11, `Window::get_current_monitor` now reliably returns the correct monitor. +- On X11, `Window::hidpi_factor` returns values from XRandR rather than the inaccurate values previously queried from the core protocol. +- On X11, the primary monitor is detected correctly even when using versions of XRandR less than 1.5. +- `MonitorId` now implements `Debug`. +- Fixed bug on macOS where using `with_decorations(false)` would cause `set_decorations(true)` to produce a transparent titlebar with no title. +- Implemented `MonitorId::get_position` on macOS. +- On macOS, `Window::get_current_monitor` now returns accurate values. +- Added `WindowBuilderExt::with_resize_increments` to macOS. +- **Breaking:** On X11, `WindowBuilderExt::with_resize_increments` and `WindowBuilderExt::with_base_size` now take `u32` values rather than `i32`. +- macOS keyboard handling has been overhauled, allowing for the use of dead keys, IME, etc. Right modifier keys are also no longer reported as being left. +- Added the `Window::set_ime_spot(x: i32, y: i32)` method, which is implemented on X11 and macOS. +- **Breaking**: `os::unix::WindowExt::send_xim_spot(x: i16, y: i16)` no longer exists. Switch to the new `Window::set_ime_spot(x: i32, y: i32)`, which has equivalent functionality. +- Fixed detection of `Pause` and `Scroll` keys on Windows. +- On Windows, alt-tabbing while the cursor is grabbed no longer makes it impossible to re-grab the cursor. +- On Windows, using `CursorState::Hide` when the cursor is grabbed now ungrabs the cursor first. +- Implemented `MouseCursor::NoneCursor` on Windows. +- Added `WindowBuilder::with_always_on_top` and `Window::set_always_on_top`. Implemented on Windows, macOS, and X11. +- On X11, `WindowBuilderExt` now has `with_class`, `with_override_redirect`, and `with_x11_window_type` to allow for more control over window creation. `WindowExt` additionally has `set_urgent`. +- More hints are set by default on X11, including `_NET_WM_PID` and `WM_CLIENT_MACHINE`. Note that prior to this, the `WM_CLASS` hint was automatically set to whatever value was passed to `with_title`. It's now set to the executable name to better conform to expectations and the specification; if this is undesirable, you must explicitly use `WindowBuilderExt::with_class`. diff --git a/third_party/winit-0.30.13/src/changelog/v0.16.md b/third_party/winit-0.30.13/src/changelog/v0.16.md new file mode 100644 index 0000000..126ad9f --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.16.md @@ -0,0 +1,32 @@ +## 0.16.2 + +- On Windows, non-resizable windows now have the maximization button disabled. This is consistent with behavior on macOS and popular X11 WMs. +- Corrected incorrect `unreachable!` usage when guessing the DPI factor with no detected monitors. + +## 0.16.1 + +- Added logging through `log`. Logging will become more extensive over time. +- On X11 and Windows, the window's DPI factor is guessed before creating the window. This _greatly_ cuts back on unsightly auto-resizing that would occur immediately after window creation. +- Fixed X11 backend compilation for environments where `c_char` is unsigned. + +## 0.16.0 + +- Windows additionally has `WindowBuilderExt::with_no_redirection_bitmap`. +- **Breaking:** Removed `VirtualKeyCode::LMenu` and `VirtualKeyCode::RMenu`; Windows now generates `VirtualKeyCode::LAlt` and `VirtualKeyCode::RAlt` instead. +- On X11, exiting fullscreen no longer leaves the window in the monitor's top left corner. +- **Breaking:** `Window::hidpi_factor` has been renamed to `Window::get_hidpi_factor` for better consistency. `WindowEvent::HiDPIFactorChanged` has been renamed to `WindowEvent::HiDpiFactorChanged`. DPI factors are always represented as `f64` instead of `f32` now. +- The Windows backend is now DPI aware. `WindowEvent::HiDpiFactorChanged` is implemented, and `MonitorId::get_hidpi_factor` and `Window::hidpi_factor` return accurate values. +- Implemented `WindowEvent::HiDpiFactorChanged` on X11. +- On macOS, `Window::set_cursor_position` is now relative to the client area. +- On macOS, setting the maximum and minimum dimensions now applies to the client area dimensions rather than to the window dimensions. +- On iOS, `MonitorId::get_dimensions` has been implemented and both `MonitorId::get_hidpi_factor` and `Window::get_hidpi_factor` return accurate values. +- On Emscripten, `MonitorId::get_hidpi_factor` now returns the same value as `Window::get_hidpi_factor` (it previously would always return 1.0). +- **Breaking:** The entire API for sizes, positions, etc. has changed. In the majority of cases, winit produces and consumes positions and sizes as `LogicalPosition` and `LogicalSize`, respectively. The notable exception is `MonitorId` methods, which deal in `PhysicalPosition` and `PhysicalSize`. See the documentation for specifics and explanations of the types. Additionally, winit automatically conserves logical size when the DPI factor changes. +- **Breaking:** All deprecated methods have been removed. For `Window::platform_display` and `Window::platform_window`, switch to the appropriate platform-specific `WindowExt` methods. For `Window::get_inner_size_points` and `Window::get_inner_size_pixels`, use the `LogicalSize` returned by `Window::get_inner_size` and convert as needed. +- HiDPI support for Wayland. +- `EventsLoop::get_available_monitors` and `EventsLoop::get_primary_monitor` now have identical counterparts on `Window`, so this information can be acquired without an `EventsLoop` borrow. +- `AvailableMonitorsIter` now implements `Debug`. +- Fixed quirk on macOS where certain keys would generate characters at twice the normal rate when held down. +- On X11, all event loops now share the same `XConnection`. +- **Breaking:** `Window::set_cursor_state` and `CursorState` enum removed in favor of the more composable `Window::grab_cursor` and `Window::hide_cursor`. As a result, grabbing the cursor no longer automatically hides it; you must call both methods to retain the old behavior on Windows and macOS. `Cursor::NoneCursor` has been removed, as it's no longer useful. +- **Breaking:** `Window::set_cursor_position` now returns `Result<(), String>`, thus allowing for `Box` conversion via `?`. diff --git a/third_party/winit-0.30.13/src/changelog/v0.17.md b/third_party/winit-0.30.13/src/changelog/v0.17.md new file mode 100644 index 0000000..0a1abc8 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.17.md @@ -0,0 +1,23 @@ +## 0.17.2 + +- On macOS, fix `` so applications receive the event. +- On macOS, fix `` so applications receive the event. +- On Wayland, key press events will now be repeated. + +## 0.17.1 + +- On X11, prevent a compilation failure in release mode for versions of Rust greater than or equal to 1.30. +- Fixed deadlock that broke fullscreen mode on Windows. + +## 0.17.0 + +- Cocoa and core-graphics updates. +- Fixed thread-safety issues in several `Window` functions on Windows. +- On MacOS, the key state for modifiers key events is now properly set. +- On iOS, the view is now set correctly. This makes it possible to render things (instead of being stuck on a black screen), and touch events work again. +- Added NetBSD support. +- **Breaking:** On iOS, `UIView` is now the default root view. `WindowBuilderExt::with_root_view_class` can be used to set the root view objective-c class to `GLKView` (OpenGLES) or `MTKView` (Metal/MoltenVK). +- On iOS, the `UIApplication` is not started until `Window::new` is called. +- Fixed thread unsafety with cursor hiding on macOS. +- On iOS, fixed the size of the `JmpBuf` type used for `setjmp`/`longjmp` calls. Previously this was a buffer overflow on most architectures. +- On Windows, use cached window DPI instead of repeatedly querying the system. This fixes sporadic crashes on Windows 7. diff --git a/third_party/winit-0.30.13/src/changelog/v0.18.md b/third_party/winit-0.30.13/src/changelog/v0.18.md new file mode 100644 index 0000000..e98df5b --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.18.md @@ -0,0 +1,52 @@ +## 0.18.1 + +- On macOS, fix `Yen` (JIS) so applications receive the event. +- On X11 with a tiling WM, fixed high CPU usage when moving windows across monitors. +- On X11, fixed panic caused by dropping the window before running the event loop. +- on macOS, added `WindowExt::set_simple_fullscreen` which does not require a separate space +- Introduce `WindowBuilderExt::with_app_id` to allow setting the application ID on Wayland. +- On Windows, catch panics in event loop child thread and forward them to the parent thread. This prevents an invocation of undefined behavior due to unwinding into foreign code. +- On Windows, fix issue where resizing or moving window combined with grabbing the cursor would freeze program. +- On Windows, fix issue where resizing or moving window would eat `Awakened` events. +- On Windows, exiting fullscreen after entering fullscreen with disabled decorations no longer shrinks window. +- On X11, fixed a segfault when using virtual monitors with XRandR. +- Derive `Ord` and `PartialOrd` for `VirtualKeyCode` enum. +- On Windows, fix issue where hovering or dropping a non file item would create a panic. +- On Wayland, fix resizing and DPI calculation when a `wl_output` is removed without sending a `leave` event to the `wl_surface`, such as disconnecting a monitor from a laptop. +- On Wayland, DPI calculation is handled by smithay-client-toolkit. +- On X11, `WindowBuilder::with_min_dimensions` and `WindowBuilder::with_max_dimensions` now correctly account for DPI. +- Added support for generating dummy `DeviceId`s and `WindowId`s to better support unit testing. +- On macOS, fixed unsoundness in drag-and-drop that could result in drops being rejected. +- On macOS, implemented `WindowEvent::Refresh`. +- On macOS, all `MouseCursor` variants are now implemented and the cursor will no longer reset after unfocusing. +- Removed minimum supported Rust version guarantee. + +## 0.18.0 + +- **Breaking:** `image` crate upgraded to 0.20. This is exposed as part of the `icon_loading` API. +- On Wayland, pointer events will now provide the current modifiers state. +- On Wayland, titles will now be displayed in the window header decoration. +- On Wayland, key repetition is now ended when keyboard loses focus. +- On Wayland, windows will now use more stylish and modern client side decorations. +- On Wayland, windows will use server-side decorations when available. +- **Breaking:** Added support for F16-F24 keys (variants were added to the `VirtualKeyCode` enum). +- Fixed graphical glitches when resizing on Wayland. +- On Windows, fix freezes when performing certain actions after a window resize has been triggered. Reintroduces some visual artifacts when resizing. +- Updated window manager hints under X11 to v1.5 of [Extended Window Manager Hints](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html#idm140200472629520). +- Added `WindowBuilderExt::with_gtk_theme_variant` to X11-specific `WindowBuilder` functions. +- Fixed UTF8 handling bug in X11 `set_title` function. +- On Windows, `Window::set_cursor` now applies immediately instead of requiring specific events to occur first. +- On Windows, the `HoveredFile` and `HoveredFileCancelled` events are now implemented. +- On Windows, fix `Window::set_maximized`. +- On Windows 10, fix transparency (#260). +- On macOS, fix modifiers during key repeat. +- Implemented the `Debug` trait for `Window`, `EventsLoop`, `EventsLoopProxy` and `WindowBuilder`. +- On X11, now a `Resized` event will always be generated after a DPI change to ensure the window's logical size is consistent with the new DPI. +- Added further clarifications to the DPI docs. +- On Linux, if neither X11 nor Wayland manage to initialize, the corresponding panic now consists of a single line only. +- Add optional `serde` feature with implementations of `Serialize`/`Deserialize` for DPI types and various event types. +- Add `PartialEq`, `Eq`, and `Hash` implementations on public types that could have them but were missing them. +- On X11, drag-and-drop receiving an unsupported drop type can no longer cause the WM to freeze. +- Fix issue whereby the OpenGL context would not appear at startup on macOS Mojave (#1069). +- **Breaking:** Removed `From` impl from `ActivationPolicy` on macOS. +- On macOS, the application can request the user's attention with `WindowExt::request_user_attention`. diff --git a/third_party/winit-0.30.13/src/changelog/v0.19.md b/third_party/winit-0.30.13/src/changelog/v0.19.md new file mode 100644 index 0000000..65aefef --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.19.md @@ -0,0 +1,27 @@ +## 0.19.1 + +- On Wayland, added a `get_wayland_display` function to `EventsLoopExt`. +- On Windows, fix `CursorMoved(0, 0)` getting dispatched on window focus. +- On macOS, fix command key event left and right reverse. +- On FreeBSD, NetBSD, and OpenBSD, fix build of X11 backend. +- On Linux, the numpad's add, subtract and divide keys are now mapped to the `Add`, `Subtract` and `Divide` virtual key codes +- On macOS, the numpad's subtract key has been added to the `Subtract` mapping +- On Wayland, the numpad's home, end, page up and page down keys are now mapped to the `Home`, `End`, `PageUp` and `PageDown` virtual key codes +- On Windows, fix icon not showing up in corner of window. +- On X11, change DPI scaling factor behavior. First, winit tries to read it from "Xft.dpi" XResource, and uses DPI calculation from xrandr dimensions as fallback behavior. + +## 0.19.0 + +- On X11, we will use the faster `XRRGetScreenResourcesCurrent` function instead of `XRRGetScreenResources` when available. +- On macOS, fix keycodes being incorrect when using a non-US keyboard layout. +- On Wayland, fix `with_title()` not setting the windows title +- On Wayland, add `set_wayland_theme()` to control client decoration color theme +- Added serde serialization to `os::unix::XWindowType`. +- **Breaking:** Remove the `icon_loading` feature and the associated `image` dependency. +- On X11, make event loop thread safe by replacing XNextEvent with select(2) and XCheckIfEvent +- On Windows, fix malformed function pointer typecast that could invoke undefined behavior. +- Refactored Windows state/flag-setting code. +- On Windows, hiding the cursor no longer hides the cursor for all Winit windows - just the one `hide_cursor` was called on. +- On Windows, cursor grabs used to get perpetually canceled when the grabbing window lost focus. Now, cursor grabs automatically get re-initialized when the window regains focus and the mouse moves over the client area. +- On Windows, only vertical mouse wheel events were handled. Now, horizontal mouse wheel events are also handled. +- On Windows, ignore the AltGr key when populating the `ModifiersState` type. diff --git a/third_party/winit-0.30.13/src/changelog/v0.20.md b/third_party/winit-0.30.13/src/changelog/v0.20.md new file mode 100644 index 0000000..eb4ba6a --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.20.md @@ -0,0 +1,200 @@ +## 0.20.0 + +- On X11, fix `ModifiersChanged` emitting incorrect modifier change events +- **Breaking**: Overhaul how Winit handles DPI: + - Window functions and events now return `PhysicalSize` instead of `LogicalSize`. + - Functions that take `Size` or `Position` types can now take either `Logical` or `Physical` types. + - `hidpi_factor` has been renamed to `scale_factor`. + - `HiDpiFactorChanged` has been renamed to `ScaleFactorChanged`, and lets you control how the OS + resizes the window in response to the change. + - On X11, deprecate `WINIT_HIDPI_FACTOR` environment variable in favor of `WINIT_X11_SCALE_FACTOR`. + - `Size` and `Position` types are now generic over their exact pixel type. + +## 0.20.0-alpha6 + +- On macOS, fix `set_cursor_visible` hides cursor outside of window. +- On macOS, fix `CursorEntered` and `CursorLeft` events fired at old window size. +- On macOS, fix error when `set_fullscreen` is called during fullscreen transition. +- On all platforms except mobile and WASM, implement `Window::set_minimized`. +- On X11, fix `CursorEntered` event being generated for non-winit windows. +- On macOS, fix crash when starting maximized without decorations. +- On macOS, fix application not terminating on `run_return`. +- On Wayland, fix cursor icon updates on window borders when using CSD. +- On Wayland, under mutter(GNOME Wayland), fix CSD being behind the status bar, when starting window in maximized mode. +- On Windows, theme the title bar according to whether the system theme is "Light" or "Dark". +- Added `WindowEvent::ThemeChanged` variant to handle changes to the system theme. Currently only implemented on Windows. +- **Breaking**: Changes to the `RedrawRequested` event (#1041): + - `RedrawRequested` has been moved from `WindowEvent` to `Event`. + - `EventsCleared` has been renamed to `MainEventsCleared`. + - `RedrawRequested` is now issued only after `MainEventsCleared`. + - `RedrawEventsCleared` is issued after each set of `RedrawRequested` events. +- Implement synthetic window focus key events on Windows. +- **Breaking**: Change `ModifiersState` to a `bitflags` struct. +- On Windows, implement `VirtualKeyCode` translation for `LWin` and `RWin`. +- On Windows, fix closing the last opened window causing `DeviceEvent`s to stop getting emitted. +- On Windows, fix `Window::set_visible` not setting internal flags correctly. This resulted in some weird behavior. +- Add `DeviceEvent::ModifiersChanged`. + - Deprecate `modifiers` fields in other events in favor of `ModifiersChanged`. +- On X11, `WINIT_HIDPI_FACTOR` now dominates `Xft.dpi` when picking DPI factor for output. +- On X11, add special value `randr` for `WINIT_HIDPI_FACTOR` to make winit use self computed DPI factor instead of the one from `Xft.dpi`. + +## 0.20.0-alpha5 + +- On macOS, fix application termination on `ControlFlow::Exit` +- On Windows, fix missing `ReceivedCharacter` events when Alt is held. +- On macOS, stop emitting private corporate characters in `ReceivedCharacter` events. +- On X11, fix misreporting DPI factor at startup. +- On X11, fix events not being reported when using `run_return`. +- On X11, fix key modifiers being incorrectly reported. +- On X11, fix window creation hanging when another window is fullscreen. +- On Windows, fix focusing unfocused windows when switching from fullscreen to windowed. +- On X11, fix reporting incorrect DPI factor when waking from suspend. +- Change `EventLoopClosed` to contain the original event. +- **Breaking**: Add `is_synthetic` field to `WindowEvent` variant `KeyboardInput`, + indicating that the event is generated by winit. +- On X11, generate synthetic key events for keys held when a window gains or loses focus. +- On X11, issue a `CursorMoved` event when a `Touch` event occurs, + as X11 implicitly moves the cursor for such events. + +## 0.20.0-alpha4 + +- Add web support via the 'stdweb' or 'web-sys' features +- On Windows, implemented function to get HINSTANCE +- On macOS, implement `run_return`. +- On iOS, fix inverted parameter in `set_prefers_home_indicator_hidden`. +- On X11, performance is improved when rapidly calling `Window::set_cursor_icon`. +- On iOS, fix improper `msg_send` usage that was UB and/or would break if `!` is stabilized. +- On Windows, unset `maximized` when manually changing the window's position or size. +- On Windows, add touch pressure information for touch events. +- On macOS, differentiate between `CursorIcon::Grab` and `CursorIcon::Grabbing`. +- On Wayland, fix event processing sometimes stalling when using OpenGL with vsync. +- Officially remove the Emscripten backend. +- On Windows, fix handling of surrogate pairs when dispatching `ReceivedCharacter`. +- On macOS 10.15, fix freeze upon exiting exclusive fullscreen mode. +- On iOS, fix panic upon closing the app. +- On X11, allow setting multiple `XWindowType`s. +- On iOS, fix null window on initial `HiDpiFactorChanged` event. +- On Windows, fix fullscreen window shrinking upon getting restored to a normal window. +- On macOS, fix events not being emitted during modal loops, such as when windows are being resized + by the user. +- On Windows, fix hovering the mouse over the active window creating an endless stream of CursorMoved events. +- Always dispatch a `RedrawRequested` event after creating a new window. +- On X11, return dummy monitor data to avoid panicking when no monitors exist. +- On X11, prevent stealing input focus when creating a new window. + Only steal input focus when entering fullscreen mode. +- On Wayland, fixed DeviceEvents for relative mouse movement is not always produced +- On Wayland, add support for set_cursor_visible and set_cursor_grab. +- On Wayland, fixed DeviceEvents for relative mouse movement is not always produced. +- Removed `derivative` crate dependency. +- On Wayland, add support for set_cursor_icon. +- Use `impl Iterator` instead of `AvailableMonitorsIter` consistently. +- On macOS, fix fullscreen state being updated after entering fullscreen instead of before, + resulting in `Window::fullscreen` returning the old state in `Resized` events instead of + reflecting the new fullscreen state +- On X11, fix use-after-free during window creation +- On Windows, disable monitor change keyboard shortcut while in exclusive fullscreen. +- On Windows, ensure that changing a borderless fullscreen window's monitor via keyboard shortcuts keeps the window fullscreen on the new monitor. +- Prevent `EventLoop::new` and `EventLoop::with_user_event` from getting called outside the main thread. + - This is because some platforms cannot run the event loop outside the main thread. Preventing this + reduces the potential for cross-platform compatibility gotchyas. +- On Windows and Linux X11/Wayland, add platform-specific functions for creating an `EventLoop` outside the main thread. +- On Wayland, drop resize events identical to the current window size. +- On Windows, fix window rectangle not getting set correctly on high-DPI systems. + +## 0.20.0-alpha3 + +- On macOS, drop the run closure on exit. +- On Windows, location of `WindowEvent::Touch` are window client coordinates instead of screen coordinates. +- On X11, fix delayed events after window redraw. +- On macOS, add `WindowBuilderExt::with_disallow_hidpi` to have the option to turn off best resolution openGL surface. +- On Windows, screen saver won't start if the window is in fullscreen mode. +- Change all occurrences of the `new_user_event` method to `with_user_event`. +- On macOS, the dock and the menu bar are now hidden in fullscreen mode. +- `Window::set_fullscreen` now takes `Option` where `Fullscreen` + consists of `Fullscreen::Exclusive(VideoMode)` and + `Fullscreen::Borderless(MonitorHandle)` variants. + - Adds support for exclusive fullscreen mode. +- On iOS, add support for hiding the home indicator. +- On iOS, add support for deferring system gestures. +- On iOS, fix a crash that occurred while acquiring a monitor's name. +- On iOS, fix armv7-apple-ios compile target. +- Removed the `T: Clone` requirement from the `Clone` impl of `EventLoopProxy`. +- On iOS, disable overscan compensation for external displays (removes black + bars surrounding the image). +- On Linux, the functions `is_wayland`, `is_x11`, `xlib_xconnection` and `wayland_display` have been moved to a new `EventLoopWindowTargetExtUnix` trait. +- On iOS, add `set_prefers_status_bar_hidden` extension function instead of + hijacking `set_decorations` for this purpose. +- On macOS and iOS, corrected the auto trait impls of `EventLoopProxy`. +- On iOS, add touch pressure information for touch events. +- Implement `raw_window_handle::HasRawWindowHandle` for `Window` type on all supported platforms. +- On macOS, fix the signature of `-[NSView drawRect:]`. +- On iOS, fix the behavior of `ControlFlow::Poll`. It wasn't polling if that was the only mode ever used by the application. +- On iOS, fix DPI sent out by views on creation was `0.0` - now it gives a reasonable number. +- On iOS, RedrawRequested now works for gl/metal backed views. +- On iOS, RedrawRequested is generally ordered after EventsCleared. + +## 0.20.0-alpha2 + +- On X11, non-resizable windows now have maximize explicitly disabled. +- On Windows, support paths longer than MAX_PATH (260 characters) in `WindowEvent::DroppedFile` + and `WindowEvent::HoveredFile`. +- On Mac, implement `DeviceEvent::Button`. +- Change `Event::Suspended(true / false)` to `Event::Suspended` and `Event::Resumed`. +- On X11, fix sanity check which checks that a monitor's reported width and height (in millimeters) are non-zero when calculating the DPI factor. +- Revert the use of invisible surfaces in Wayland, which introduced graphical glitches with OpenGL (#835) +- On X11, implement `_NET_WM_PING` to allow desktop environment to kill unresponsive programs. +- On Windows, when a window is initially invisible, it won't take focus from the existing visible windows. +- On Windows, fix multiple calls to `request_redraw` during `EventsCleared` sending multiple `RedrawRequested events.` +- On Windows, fix edge case where `RedrawRequested` could be dispatched before input events in event loop iteration. +- On Windows, fix timing issue that could cause events to be improperly dispatched after `RedrawRequested` but before `EventsCleared`. +- On macOS, drop unused Metal dependency. +- On Windows, fix the trail effect happening on transparent decorated windows. Borderless (or un-decorated) windows were not affected. +- On Windows, fix `with_maximized` not properly setting window size to entire window. +- On macOS, change `WindowExtMacOS::request_user_attention()` to take an `enum` instead of a `bool`. + +## 0.20.0-alpha1 + +- Changes below are considered **breaking**. +- Change all occurrences of `EventsLoop` to `EventLoop`. +- Previously flat API is now exposed through `event`, `event_loop`, `monitor`, and `window` modules. +- `os` module changes: + - Renamed to `platform`. + - All traits now have platform-specific suffixes. + - Exposes new `desktop` module on Windows, Mac, and Linux. +- Changes to event loop types: + - `EventLoopProxy::wakeup` has been removed in favor of `send_event`. + - **Major:** New `run` method drives winit event loop. + - Returns `!` to ensure API behaves identically across all supported platforms. + - This allows `emscripten` implementation to work without lying about the API. + - `ControlFlow`'s variants have been replaced with `Wait`, `WaitUntil(Instant)`, `Poll`, and `Exit`. + - Is read after `EventsCleared` is processed. + - `Wait` waits until new events are available. + - `WaitUntil` waits until either new events are available or the provided time has been reached. + - `Poll` instantly resumes the event loop. + - `Exit` aborts the event loop. + - Takes a closure that implements `'static + FnMut(Event, &EventLoop, &mut ControlFlow)`. + - `&EventLoop` is provided to allow new `Window`s to be created. + - **Major:** `platform::desktop` module exposes `EventLoopExtDesktop` trait with `run_return` method. + - Behaves identically to `run`, but returns control flow to the calling context and can take non-`'static` closures. + - `EventLoop`'s `poll_events` and `run_forever` methods have been removed in favor of `run` and `run_return`. +- Changes to events: + - Remove `Event::Awakened` in favor of `Event::UserEvent(T)`. + - Can be sent with `EventLoopProxy::send_event`. + - Rename `WindowEvent::Refresh` to `WindowEvent::RedrawRequested`. + - `RedrawRequested` can be sent by the user with the `Window::request_redraw` method. + - `EventLoop`, `EventLoopProxy`, and `Event` are now generic over `T`, for use in `UserEvent`. + - **Major:** Add `NewEvents(StartCause)`, `EventsCleared`, and `LoopDestroyed` variants to `Event`. + - `NewEvents` is emitted when new events are ready to be processed by event loop. + - `StartCause` describes why new events are available, with `ResumeTimeReached`, `Poll`, `WaitCancelled`, and `Init` (sent once at start of loop). + - `EventsCleared` is emitted when all available events have been processed. + - Can be used to perform logic that depends on all events being processed (e.g. an iteration of a game loop). + - `LoopDestroyed` is emitted when the `run` or `run_return` method is about to exit. +- Rename `MonitorId` to `MonitorHandle`. +- Removed `serde` implementations from `ControlFlow`. +- Rename several functions to improve both internal consistency and compliance with Rust API guidelines. +- Remove `WindowBuilder::multitouch` field, since it was only implemented on a few platforms. Multitouch is always enabled now. +- **Breaking:** On macOS, change `ns` identifiers to use snake_case for consistency with iOS's `ui` identifiers. +- Add `MonitorHandle::video_modes` method for retrieving supported video modes for the given monitor. +- On Wayland, the window now exists even if nothing has been drawn. +- On Windows, fix initial dimensions of a fullscreen window. +- On Windows, Fix transparent borderless windows rendering wrong. diff --git a/third_party/winit-0.30.13/src/changelog/v0.21.md b/third_party/winit-0.30.13/src/changelog/v0.21.md new file mode 100644 index 0000000..48f3d35 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.21.md @@ -0,0 +1,16 @@ +## 0.21.0 + +- On Windows, fixed "error: linking with `link.exe` failed: exit code: 1120" error on older versions of windows. +- On macOS, fix set_minimized(true) works only with decorations. +- On macOS, add `hide_application` to `EventLoopWindowTarget` via a new `EventLoopWindowTargetExtMacOS` trait. `hide_application` will hide the entire application by calling `-[NSApplication hide: nil]`. +- On macOS, fix not sending ReceivedCharacter event for specific keys combinations. +- On macOS, fix `CursorMoved` event reporting the cursor position using logical coordinates. +- On macOS, fix issue where unbundled applications would sometimes open without being focused. +- On macOS, fix `run_return` does not return unless it receives a message. +- On Windows, fix bug where `RedrawRequested` would only get emitted every other iteration of the event loop. +- On X11, fix deadlock on window state when handling certain window events. +- `WindowBuilder` now implements `Default`. +- **Breaking:** `WindowEvent::CursorMoved` changed to `f64` units, preserving high-precision data supplied by most backends +- On Wayland, fix coordinates in mouse events when scale factor isn't 1 +- On Web, add the ability to provide a custom canvas +- **Breaking:** On Wayland, the `WaylandTheme` struct has been replaced with a `Theme` trait, allowing for extra configuration diff --git a/third_party/winit-0.30.13/src/changelog/v0.22.md b/third_party/winit-0.30.13/src/changelog/v0.22.md new file mode 100644 index 0000000..07bc1ef --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.22.md @@ -0,0 +1,37 @@ +## 0.22.2 + +- Added Clone implementation for 'static events. +- On Windows, fix window intermittently hanging when `ControlFlow` was set to `Poll`. +- On Windows, fix `WindowBuilder::with_maximized` being ignored. +- On Android, minimal platform support. +- On iOS, touch positions are now properly converted to physical pixels. +- On macOS, updated core-* dependencies and cocoa + +## 0.22.1 + +- On X11, fix `ResumeTimeReached` being fired too early. +- On Web, replaced zero timeout for `ControlFlow::Poll` with `requestAnimationFrame` +- On Web, fix a possible panic during event handling +- On macOS, fix `EventLoopProxy` leaking memory for every instance. + +## 0.22.0 + +- On Windows, fix minor timing issue in wait_until_time_or_msg +- On Windows, rework handling of request_redraw() to address panics. +- On macOS, fix `set_simple_screen` to remember frame excluding title bar. +- On Wayland, fix coordinates in touch events when scale factor isn't 1. +- On Wayland, fix color from `close_button_icon_color` not applying. +- Ignore locale if unsupported by X11 backend +- On Wayland, Add HiDPI cursor support +- On Web, add the ability to query "Light" or "Dark" system theme send `ThemeChanged` on change. +- Fix `Event::to_static` returning `None` for user events. +- On Wayland, Hide CSD for fullscreen windows. +- On Windows, ignore spurious mouse move messages. +- **Breaking:** Move `ModifiersChanged` variant from `DeviceEvent` to `WindowEvent`. +- On Windows, add `IconExtWindows` trait which exposes creating an `Icon` from an external file or embedded resource +- Add `BadIcon::OsError` variant for when OS icon functionality fails +- On Windows, fix crash at startup on systems that do not properly support Windows' Dark Mode +- Revert On macOS, fix not sending ReceivedCharacter event for specific keys combinations. +- on macOS, fix incorrect ReceivedCharacter events for some key combinations. +- **Breaking:** Use `i32` instead of `u32` for position type in `WindowEvent::Moved`. +- On macOS, a mouse motion event is now generated before every mouse click. diff --git a/third_party/winit-0.30.13/src/changelog/v0.23.md b/third_party/winit-0.30.13/src/changelog/v0.23.md new file mode 100644 index 0000000..33ea2ab --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.23.md @@ -0,0 +1,65 @@ +## 0.23.0 + +- On iOS, fixed support for the "Debug View Hierarchy" feature in Xcode. +- On all platforms, `available_monitors` and `primary_monitor` are now on `EventLoopWindowTarget` rather than `EventLoop` to list monitors event in the event loop. +- On Unix, X11 and Wayland are now optional features (enabled by default) +- On X11, fix deadlock when calling `set_fullscreen_inner`. +- On Web, prevent the webpage from scrolling when the user is focused on a winit canvas +- On Web, calling `window.set_cursor_icon` no longer breaks HiDPI scaling +- On Windows, drag and drop is now optional (enabled by default) and can be disabled with `WindowBuilderExtWindows::with_drag_and_drop(false)`. +- On Wayland, fix deadlock when calling to `set_inner_size` from a callback. +- On macOS, add `hide__other_applications` to `EventLoopWindowTarget` via existing `EventLoopWindowTargetExtMacOS` trait. `hide_other_applications` will hide other applications by calling `-[NSApplication hideOtherApplications: nil]`. +- On android added support for `run_return`. +- On MacOS, Fixed fullscreen and dialog support for `run_return`. +- On Windows, fix bug where we'd try to emit `MainEventsCleared` events during nested win32 event loops. +- On Web, use mouse events if pointer events aren't supported. This affects Safari. +- On Windows, `set_ime_position` is now a no-op instead of a runtime crash. +- On Android, `set_fullscreen` is now a no-op instead of a runtime crash. +- On iOS and Android, `set_inner_size` is now a no-op instead of a runtime crash. +- On Android, fix `ControlFlow::Poll` not polling the Android event queue. +- On macOS, add `NSWindow.hasShadow` support. +- On Web, fix vertical mouse wheel scrolling being inverted. +- On Web, implement mouse capturing for click-dragging out of the canvas. +- On Web, fix `ControlFlow::Exit` not properly handled. +- On Web (web-sys only), send `WindowEvent::ScaleFactorChanged` event when `window.devicePixelRatio` is changed. +- **Breaking:** On Web, `set_cursor_position` and `set_cursor_grab` will now always return an error. +- **Breaking:** `PixelDelta` scroll events now return a `PhysicalPosition`. +- On NetBSD, fixed crash due to incorrect detection of the main thread. +- **Breaking:** On X11, `-` key is mapped to the `Minus` virtual key code, instead of `Subtract`. +- On macOS, fix inverted horizontal scroll. +- **Breaking:** `current_monitor` now returns `Option`. +- **Breaking:** `primary_monitor` now returns `Option`. +- On macOS, updated core-* dependencies and cocoa. +- Bump `parking_lot` to 0.11 +- On Android, bump `ndk`, `ndk-sys` and `ndk-glue` to 0.2. Checkout the new ndk-glue main proc attribute. +- On iOS, fixed starting the app in landscape where the view still had portrait dimensions. +- Deprecate the stdweb backend, to be removed in a future release +- **Breaking:** Prefixed virtual key codes `Add`, `Multiply`, `Divide`, `Decimal`, and `Subtract` with `Numpad`. +- Added `Asterisk` and `Plus` virtual key codes. +- On Web (web-sys only), the `Event::LoopDestroyed` event is correctly emitted when leaving the page. +- On Web, the `WindowEvent::Destroyed` event now gets emitted when a `Window` is dropped. +- On Web (web-sys only), the event listeners are now removed when a `Window` is dropped or when the event loop is destroyed. +- On Web, the event handler closure passed to `EventLoop::run` now gets dropped after the event loop is destroyed. +- **Breaking:** On Web, the canvas element associated to a `Window` is no longer removed from the DOM when the `Window` is dropped. +- On Web, `WindowEvent::Resized` is now emitted when `Window::set_inner_size` is called. +- **Breaking:** `Fullscreen` enum now uses `Borderless(Option)` instead of `Borderless(MonitorHandle)` to allow picking the current monitor. +- On MacOS, fix `WindowEvent::Moved` ignoring the scale factor. +- On Wayland, add missing virtual keycodes. +- On Wayland, implement proper `set_cursor_grab`. +- On Wayland, the cursor will use similar icons if the requested one isn't available. +- On Wayland, right clicking on client side decorations will request application menu. +- On Wayland, fix tracking of window size after state changes. +- On Wayland, fix client side decorations not being hidden properly in fullscreen. +- On Wayland, fix incorrect size event when entering fullscreen with client side decorations. +- On Wayland, fix `resizable` attribute not being applied properly on startup. +- On Wayland, fix disabled repeat rate not being handled. +- On Wayland, fix decoration buttons not working after tty switch. +- On Wayland, fix scaling not being applied on output re-enable. +- On Wayland, fix crash when `XCURSOR_SIZE` is `0`. +- On Wayland, fix pointer getting created in some cases without pointer capability. +- On Wayland, on kwin, fix space between window and decorations on startup. +- **Breaking:** On Wayland, `Theme` trait was reworked. +- On Wayland, disable maximize button for non-resizable window. +- On Wayland, added support for `set_ime_position`. +- On Wayland, fix crash on startup since GNOME 3.37.90. +- On X11, fix incorrect modifiers state on startup. diff --git a/third_party/winit-0.30.13/src/changelog/v0.24.md b/third_party/winit-0.30.13/src/changelog/v0.24.md new file mode 100644 index 0000000..fa0a27b --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.24.md @@ -0,0 +1,28 @@ +## 0.24.0 + +- On Windows, fix applications not exiting gracefully due to thread_event_target_callback accessing corrupted memory. +- On Windows, implement `Window::set_ime_position`. +- **Breaking:** On Windows, Renamed `WindowBuilderExtWindows`'s `is_dark_mode` to `theme`. +- **Breaking:** On Windows, renamed `WindowBuilderExtWindows::is_dark_mode` to `theme`. +- On Windows, add `WindowBuilderExtWindows::with_theme` to set a preferred theme. +- On Windows, fix bug causing message boxes to appear delayed. +- On Android, calling `WindowEvent::Focused` now works properly instead of always returning false. +- On Windows, fix Alt-Tab behaviour by removing borderless fullscreen "always on top" flag. +- On Windows, fix bug preventing windows with transparency enabled from having fully-opaque regions. +- **Breaking:** On Windows, include prefix byte in scancodes. +- On Wayland, fix window not being resizeable when using `WindowBuilder::with_min_inner_size`. +- On Unix, fix cross-compiling to wasm32 without enabling X11 or Wayland. +- On Windows, fix use-after-free crash during window destruction. +- On Web, fix `WindowEvent::ReceivedCharacter` never being sent on key input. +- On macOS, fix compilation when targeting aarch64. +- On X11, fix `Window::request_redraw` not waking the event loop. +- On Wayland, the keypad arrow keys are now recognized. +- **Breaking** Rename `desktop::EventLoopExtDesktop` to `run_return::EventLoopExtRunReturn`. +- Added `request_user_attention` method to `Window`. +- **Breaking:** On macOS, removed `WindowExt::request_user_attention`, use `Window::request_user_attention`. +- **Breaking:** On X11, removed `WindowExt::set_urgent`, use `Window::request_user_attention`. +- On Wayland, default font size in CSD increased from 11 to 17. +- On Windows, fix bug causing message boxes to appear delayed. +- On Android, support multi-touch. +- On Wayland, extra mouse buttons are not dropped anymore. +- **Breaking**: `MouseButton::Other` now uses `u16`. diff --git a/third_party/winit-0.30.13/src/changelog/v0.25.md b/third_party/winit-0.30.13/src/changelog/v0.25.md new file mode 100644 index 0000000..00451c0 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.25.md @@ -0,0 +1,31 @@ +## 0.25.0 + +- **Breaking:** On macOS, replace `WindowBuilderExtMacOS::with_activation_policy` with `EventLoopExtMacOS::set_activation_policy` +- On macOS, wait with activating the application until the application has initialized. +- On macOS, fix creating new windows when the application has a main menu. +- On Windows, fix fractional deltas for mouse wheel device events. +- On macOS, fix segmentation fault after dropping the main window. +- On Android, `InputEvent::KeyEvent` is partially implemented providing the key scancode. +- Added `is_maximized` method to `Window`. +- On Windows, fix bug where clicking the decoration bar would make the cursor blink. +- On Windows, fix bug causing newly created windows to erroneously display the "wait" (spinning) cursor. +- On macOS, wake up the event loop immediately when a redraw is requested. +- On Windows, change the default window size (1024x768) to match the default on other desktop platforms (800x600). +- On Windows, fix bug causing mouse capture to not be released. +- On Windows, fix fullscreen not preserving minimized/maximized state. +- On Android, unimplemented events are marked as unhandled on the native event loop. +- On Windows, added `WindowBuilderExtWindows::with_menu` to set a custom menu at window creation time. +- On Android, bump `ndk` and `ndk-glue` to 0.3: use predefined constants for event `ident`. +- On macOS, fix objects captured by the event loop closure not being dropped on panic. +- On Windows, fixed `WindowEvent::ThemeChanged` not properly firing and fixed `Window::theme` returning the wrong theme. +- On Web, added support for `DeviceEvent::MouseMotion` to listen for relative mouse movements. +- Added `WindowBuilder::with_position` to allow setting the position of a `Window` on creation. Supported on Windows, macOS and X11. +- Added `Window::drag_window`. Implemented on Windows, macOS, X11 and Wayland. +- On X11, bump `mio` to 0.7. +- On Windows, added `WindowBuilderExtWindows::with_owner_window` to allow creating popup windows. +- On Windows, added `WindowExtWindows::set_enable` to allow creating modal popup windows. +- On macOS, emit `RedrawRequested` events immediately while the window is being resized. +- Implement `Default`, `Hash`, and `Eq` for `LogicalPosition`, `PhysicalPosition`, `LogicalSize`, and `PhysicalSize`. +- On macOS, initialize the Menu Bar with minimal defaults. (Can be prevented using `enable_default_menu_creation`) +- On macOS, change the default behavior for first click when the window was unfocused. Now the window becomes focused and then emits a `MouseInput` event on a "first mouse click". +- Implement mint (math interoperability standard types) conversions (under feature flag `mint`). diff --git a/third_party/winit-0.30.13/src/changelog/v0.26.md b/third_party/winit-0.30.13/src/changelog/v0.26.md new file mode 100644 index 0000000..d33d4a3 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.26.md @@ -0,0 +1,36 @@ +## 0.26.1 + +- Fix linking to the `ColorSync` framework on macOS 10.7, and in newer Rust versions. +- On Web, implement cursor grabbing through the pointer lock API. +- On X11, add mappings for numpad comma, numpad enter, numlock and pause. +- On macOS, fix Pinyin IME input by reverting a change that intended to improve IME. +- On Windows, fix a crash with transparent windows on Windows 11. + +## 0.26.0 + +- Update `raw-window-handle` to `v0.4`. This is _not_ a breaking change, we still implement `HasRawWindowHandle` from `v0.3`, see [rust-windowing/raw-window-handle#74](https://github.com/rust-windowing/raw-window-handle/pull/74). Note that you might have to run `cargo update -p raw-window-handle` after upgrading. +- On X11, bump `mio` to 0.8. +- On Android, fixed `WindowExtAndroid::config` initially returning an empty `Configuration`. +- On Android, fixed `Window::scale_factor` and `MonitorHandle::scale_factor` initially always returning 1.0. +- On X11, select an appropriate visual for transparency if is requested +- On Wayland and X11, fix diagonal window resize cursor orientation. +- On macOS, drop the event callback before exiting. +- On Android, implement `Window::request_redraw` +- **Breaking:** On Web, remove the `stdweb` backend. +- Added `Window::focus_window`to bring the window to the front and set input focus. +- On Wayland and X11, implement `is_maximized` method on `Window`. +- On Windows, prevent ghost window from showing up in the taskbar after either several hours of use or restarting `explorer.exe`. +- On macOS, fix issue where `ReceivedCharacter` was not being emitted during some key repeat events. +- On Wayland, load cursor icons `hand2` and `hand1` for `CursorIcon::Hand`. +- **Breaking:** On Wayland, Theme trait and its support types are dropped. +- On Wayland, bump `smithay-client-toolkit` to 0.15.1. +- On Wayland, implement `request_user_attention` with `xdg_activation_v1`. +- On X11, emit missing `WindowEvent::ScaleFactorChanged` when the only monitor gets reconnected. +- On X11, if RANDR based scale factor is higher than 20 reset it to 1 +- On Wayland, add an enabled-by-default feature called `wayland-dlopen` so users can opt out of using `dlopen` to load system libraries. +- **Breaking:** On Android, bump `ndk` and `ndk-glue` to 0.5. +- On Windows, increase wait timer resolution for more accurate timing when using `WaitUntil`. +- On macOS, fix native file dialogs hanging the event loop. +- On Wayland, implement a workaround for wrong configure size when using `xdg_decoration` in `kwin_wayland` +- On macOS, fix an issue that prevented the menu bar from showing in borderless fullscreen mode. +- On X11, EINTR while polling for events no longer causes a panic. Instead it will be treated as a spurious wakeup. diff --git a/third_party/winit-0.30.13/src/changelog/v0.27.md b/third_party/winit-0.30.13/src/changelog/v0.27.md new file mode 100644 index 0000000..5f067f5 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.27.md @@ -0,0 +1,107 @@ +## 0.27.5 + +- On Wayland, fix byte offset in `Ime::Preedit` pointing to invalid bytes. + +## 0.27.4 + +- On Windows, emit `ReceivedCharacter` events on system keybindings. +- On Windows, fixed focus event emission on minimize. +- On X11, fixed IME crashing during reload. + +## 0.27.3 + +- On Windows, added `WindowExtWindows::set_undecorated_shadow` and `WindowBuilderExtWindows::with_undecorated_shadow` to draw the drop shadow behind a borderless window. +- On Windows, fixed default window features (ie snap, animations, shake, etc.) when decorations are disabled. +- On Windows, fixed ALT+Space shortcut to open window menu. +- On Wayland, fixed `Ime::Preedit` not being sent on IME reset. +- Fixed unbound version specified for `raw-window-handle` leading to compilation failures. +- Empty `Ime::Preedit` event will be sent before `Ime::Commit` to help clearing preedit. +- On X11, fixed IME context picking by querying for supported styles beforehand. + +## 0.27.2 + +- On macOS, fixed touch phase reporting when scrolling. +- On X11, fix min, max and resize increment hints not persisting for resizable windows (e.g. on DPI change). +- On Windows, respect min/max inner sizes when creating the window. +- For backwards compatibility, `Window` now (additionally) implements the old version (`0.4`) of the `HasRawWindowHandle` trait +- On Windows, added support for `EventLoopWindowTarget::set_device_event_filter`. +- On Wayland, fix user requested `WindowEvent::RedrawRequested` being delayed by a frame. + +## 0.27.1 + +- The minimum supported Rust version was lowered to `1.57.0` and now explicitly tested. +- On X11, fix crash on start due to inability to create an IME context without any preedit. + +## 0.27.0 + +- On Windows, fix hiding a maximized window. +- On Android, `ndk-glue`'s `NativeWindow` lock is now held between `Event::Resumed` and `Event::Suspended`. +- On Web, added `EventLoopExtWebSys` with a `spawn` method to start the event loop without throwing an exception. +- Added `WindowEvent::Occluded(bool)`, currently implemented on macOS and X11. +- On X11, fix events for caps lock key not being sent +- Build docs on `docs.rs` for iOS and Android as well. +- **Breaking:** Removed the `WindowAttributes` struct, since all its functionality is accessible from `WindowBuilder`. +- Added `WindowBuilder::transparent` getter to check if the user set `transparent` attribute. +- On macOS, Fix emitting `Event::LoopDestroyed` on CMD+Q. +- On macOS, fixed an issue where having multiple windows would prevent run_return from ever returning. +- On Wayland, fix bug where the cursor wouldn't hide in GNOME. +- On macOS, Windows, and Wayland, add `set_cursor_hittest` to let the window ignore mouse events. +- On Windows, added `WindowExtWindows::set_skip_taskbar` and `WindowBuilderExtWindows::with_skip_taskbar`. +- On Windows, added `EventLoopBuilderExtWindows::with_msg_hook`. +- On Windows, remove internally unique DC per window. +- On macOS, remove the need to call `set_ime_position` after moving the window. +- Added `Window::is_visible`. +- Added `Window::is_resizable`. +- Added `Window::is_decorated`. +- On X11, fix for repeated event loop iteration when `ControlFlow` was `Wait` +- On X11, fix scale factor calculation when the only monitor is reconnected +- On Wayland, report unaccelerated mouse deltas in `DeviceEvent::MouseMotion`. +- On Web, a focused event is manually generated when a click occurs to emulate behaviour of other backends. +- **Breaking:** Bump `ndk` version to 0.6, ndk-sys to `v0.3`, `ndk-glue` to `0.6`. +- Remove no longer needed `WINIT_LINK_COLORSYNC` environment variable. +- **Breaking:** Rename the `Exit` variant of `ControlFlow` to `ExitWithCode`, which holds a value to control the exit code after running. Add an `Exit` constant which aliases to `ExitWithCode(0)` instead to avoid major breakage. This shouldn't affect most existing programs. +- Add `EventLoopBuilder`, which allows you to create and tweak the settings of an event loop before creating it. +- Deprecated `EventLoop::with_user_event`; use `EventLoopBuilder::with_user_event` instead. +- **Breaking:** Replaced `EventLoopExtMacOS` with `EventLoopBuilderExtMacOS` (which also has renamed methods). +- **Breaking:** Replaced `EventLoopExtWindows` with `EventLoopBuilderExtWindows` (which also has renamed methods). +- **Breaking:** Replaced `EventLoopExtUnix` with `EventLoopBuilderExtUnix` (which also has renamed methods). +- **Breaking:** The platform specific extensions for Windows `winit::platform::windows` have changed. All `HANDLE`-like types e.g. `HWND` and `HMENU` were converted from winapi types or `*mut c_void` to `isize`. This was done to be consistent with the type definitions in windows-sys and to not expose internal dependencies. +- The internal bindings to the [Windows API](https://docs.microsoft.com/en-us/windows/) were changed from the unofficial [winapi](https://github.com/retep998/winapi-rs) bindings to the official Microsoft [windows-sys](https://github.com/microsoft/windows-rs) bindings. +- On Wayland, fix polling during consecutive `EventLoop::run_return` invocations. +- On Windows, fix race issue creating fullscreen windows with `WindowBuilder::with_fullscreen` +- On Android, `virtual_keycode` for `KeyboardInput` events is now filled in where a suitable match is found. +- Added helper methods on `ControlFlow` to set its value. +- On Wayland, fix `TouchPhase::Ended` always reporting the location of the first touch down, unless the compositor + sent a cancel or frame event. +- On iOS, send `RedrawEventsCleared` even if there are no redraw events, consistent with other platforms. +- **Breaking:** Replaced `Window::with_app_id` and `Window::with_class` with `Window::with_name` on `WindowBuilderExtUnix`. +- On Wayland, fallback CSD was replaced with proper one: + - `WindowBuilderExtUnix::with_wayland_csd_theme` to set color theme in builder. + - `WindowExtUnix::wayland_set_csd_theme` to set color theme when creating a window. + - `WINIT_WAYLAND_CSD_THEME` env variable was added, it can be used to set "dark"/"light" theme in apps that don't expose theme setting. + - `wayland-csd-adwaita` feature that enables proper CSD with title rendering using FreeType system library. + - `wayland-csd-adwaita-notitle` feature that enables CSD but without title rendering. +- On Wayland and X11, fix window not resizing with `Window::set_inner_size` after calling `Window:set_resizable(false)`. +- On Windows, fix wrong fullscreen monitors being recognized when handling WM_WINDOWPOSCHANGING messages +- **Breaking:** Added new `WindowEvent::Ime` supported on desktop platforms. +- Added `Window::set_ime_allowed` supported on desktop platforms. +- **Breaking:** IME input on desktop platforms won't be received unless it's explicitly allowed via `Window::set_ime_allowed` and new `WindowEvent::Ime` events are handled. +- On macOS, `WindowEvent::Resized` is now emitted in `frameDidChange` instead of `windowDidResize`. +- **Breaking:** On X11, device events are now ignored for unfocused windows by default, use `EventLoopWindowTarget::set_device_event_filter` to set the filter level. +- Implemented `Default` on `EventLoop<()>`. +- Implemented `Eq` for `Fullscreen`, `Theme`, and `UserAttentionType`. +- **Breaking:** `Window::set_cursor_grab` now accepts `CursorGrabMode` to control grabbing behavior. +- On Wayland, add support for `Window::set_cursor_position`. +- Fix on macOS `WindowBuilder::with_disallow_hidpi`, setting true or false by the user no matter the SO default value. +- `EventLoopBuilder::build` will now panic when the `EventLoop` is being created more than once. +- Added `From` for `WindowId` and `From` for `u64`. +- Added `MonitorHandle::refresh_rate_millihertz` to get monitor's refresh rate. +- **Breaking**, Replaced `VideoMode::refresh_rate` with `VideoMode::refresh_rate_millihertz` providing better precision. +- On Web, add `with_prevent_default` and `with_focusable` to `WindowBuilderExtWebSys` to control whether events should be propagated. +- On Windows, fix focus events being sent to inactive windows. +- **Breaking**, update `raw-window-handle` to `v0.5` and implement `HasRawDisplayHandle` for `Window` and `EventLoopWindowTarget`. +- On X11, add function `register_xlib_error_hook` into `winit::platform::unix` to subscribe for errors coming from Xlib. +- On Android, upgrade `ndk` and `ndk-glue` dependencies to the recently released `0.7.0`. +- All platforms can now be relied on to emit a `Resumed` event. Applications are recommended to lazily initialize graphics state and windows on first resume for portability. +- **Breaking:**: Reverse horizontal scrolling sign in `MouseScrollDelta` to match the direction of vertical scrolling. A positive X value now means moving the content to the right. The meaning of vertical scrolling stays the same: a positive Y value means moving the content down. +- On MacOS, fix deadlock when calling `set_maximized` from event loop. diff --git a/third_party/winit-0.30.13/src/changelog/v0.28.md b/third_party/winit-0.30.13/src/changelog/v0.28.md new file mode 100644 index 0000000..a8b3262 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.28.md @@ -0,0 +1,100 @@ +## 0.28.7 + +- Fix window size sometimes being invalid when resizing on macOS 14 Sonoma. + +## 0.28.6 + +- On macOS, fixed memory leak when getting monitor handle. +- On macOS, fix `Backspace` being emitted when clearing preedit with it. + +## 0.28.5 + +- On macOS, fix `key_up` being ignored when `Ime` is disabled. + +## 0.28.4 + +- On macOS, fix empty marked text blocking regular input. +- On macOS, fix potential panic when getting refresh rate. +- On macOS, fix crash when calling `Window::set_ime_position` from another thread. + +## 0.28.3 + +- Fix macOS memory leaks. + +## 0.28.2 + +- Implement `HasRawDisplayHandle` for `EventLoop`. +- On macOS, set resize increments only for live resizes. +- On Wayland, fix rare crash on DPI change +- Web: Added support for `Window::theme`. +- On Wayland, fix rounding issues when doing resize. +- On macOS, fix wrong focused state on startup. +- On Windows, fix crash on setting taskbar when using Visual Studio debugger. +- On macOS, resize simple fullscreen windows on windowDidChangeScreen events. + +## 0.28.1 + +- On Wayland, fix crash when dropping a window in multi-window setup. + +## 0.28.0 + +- On macOS, fixed `Ime::Commit` persisting for all input after interacting with `Ime`. +- On macOS, added `WindowExtMacOS::option_as_alt` and `WindowExtMacOS::set_option_as_alt`. +- On Windows, fix window size for maximized, undecorated windows. +- On Windows and macOS, add `WindowBuilder::with_active`. +- Add `Window::is_minimized`. +- On X11, fix errors handled during `register_xlib_error_hook` invocation bleeding into winit. +- Add `Window::has_focus`. +- On Windows, fix `Window::set_minimized(false)` not working for windows minimized by `Win + D` hotkey. +- **Breaking:** On Web, touch input no longer fires `WindowEvent::Cursor*`, `WindowEvent::MouseInput`, or `DeviceEvent::MouseMotion` like other platforms, but instead it fires `WindowEvent::Touch`. +- **Breaking:** Removed platform specific `WindowBuilder::with_parent` API in favor of `WindowBuilder::with_parent_window`. +- On Windows, retain `WS_MAXIMIZE` window style when un-minimizing a maximized window. +- On Windows, fix left mouse button release event not being sent after `Window::drag_window`. +- On macOS, run most actions on the main thread, which is strictly more correct, but might make multithreaded applications block slightly more. +- On macOS, fix panic when getting current monitor without any monitor attached. +- On Windows and MacOS, add API to enable/disable window buttons (close, minimize, ...etc). +- On Windows, macOS, X11 and Wayland, add `Window::set_theme`. +- **Breaking:** Remove `WindowExtWayland::wayland_set_csd_theme` and `WindowBuilderExtX11::with_gtk_theme_variant`. +- On Windows, revert window background to an empty brush to avoid white flashes when changing scaling. +- **Breaking:** Removed `Window::set_always_on_top` and related APIs in favor of `Window::set_window_level`. +- On Windows, MacOS and X11, add always on bottom APIs. +- On Windows, fix the value in `MouseButton::Other`. +- On macOS, add `WindowExtMacOS::is_document_edited` and `WindowExtMacOS::set_document_edited` APIs. +- **Breaking:** Removed `WindowBuilderExtIOS::with_root_view_class`; instead, you should use `[[view layer] addSublayer: ...]` to add an instance of the desired layer class (e.g. `CAEAGLLayer` or `CAMetalLayer`). See `vulkano-win` or `wgpu` for examples of this. +- On MacOS and Windows, add `Window::set_content_protected`. +- On MacOS, add `EventLoopBuilderExtMacOS::with_activate_ignoring_other_apps`. +- On Windows, fix icons specified on `WindowBuilder` not taking effect for windows created after the first one. +- On Windows and macOS, add `Window::title` to query the current window title. +- On Windows, fix focusing menubar when pressing `Alt`. +- On MacOS, made `accepts_first_mouse` configurable. +- Migrated `WindowBuilderExtUnix::with_resize_increments` to `WindowBuilder`. +- Added `Window::resize_increments`/`Window::set_resize_increments` to update resize increments at runtime for X11/macOS. +- macOS/iOS: Use `objc2` instead of `objc` internally. +- **Breaking:** Bump MSRV from `1.57` to `1.60`. +- **Breaking:** Split the `platform::unix` module into `platform::x11` and `platform::wayland`. The extension types are similarly renamed. +- **Breaking:**: Removed deprecated method `platform::unix::WindowExtUnix::is_ready`. +- Removed `parking_lot` dependency. +- **Breaking:** On macOS, add support for two-finger touchpad magnification and rotation gestures with new events `WindowEvent::TouchpadMagnify` and `WindowEvent::TouchpadRotate`. Also add support for touchpad smart-magnification gesture with a new event `WindowEvent::SmartMagnify`. +- **Breaking:** On web, the `WindowBuilderExtWebSys::with_prevent_default` setting (enabled by default), now additionally prevents scrolling of the webpage in mobile browsers, previously it only disabled scrolling on desktop. +- On Wayland, `wayland-csd-adwaita` now uses `ab_glyph` instead of `crossfont` to render the title for decorations. +- On Wayland, a new `wayland-csd-adwaita-crossfont` feature was added to use `crossfont` instead of `ab_glyph` for decorations. +- On Wayland, if not otherwise specified use upstream automatic CSD theme selection. +- On X11, added `WindowExtX11::with_parent` to create child windows. +- Added support for `WindowBuilder::with_theme` and `Window::theme` to support per-window dark/light/system theme configuration on macos, windows and wayland. +- On macOS, added support for `WindowEvent::ThemeChanged`. +- **Breaking:** Removed `WindowBuilderExtWindows::with_theme` and `WindowBuilderExtWayland::with_wayland_csd_theme` in favour of `WindowBuilder::with_theme`. +- **Breaking:** Removed `WindowExtWindows::theme` in favour of `Window::theme`. +- Enabled `doc_auto_cfg` when generating docs on docs.rs for feature labels. +- **Breaking:** On Android, switched to using [`android-activity`](https://github.com/rib/android-activity) crate as a glue layer instead of [`ndk-glue`](https://github.com/rust-windowing/android-ndk-rs/tree/master/ndk-glue). See [README.md#Android](https://github.com/rust-windowing/winit#Android) for more details. ([#2444](https://github.com/rust-windowing/winit/pull/2444)) +- **Breaking:** Removed support for `raw-window-handle` version `0.4` +- On Wayland, `RedrawRequested` not emitted during resize. +- Add a `set_wait_timeout` function to `ControlFlow` to allow waiting for a `Duration`. +- **Breaking:** Remove the unstable `xlib_xconnection()` function from the private interface. +- Added Orbital support for Redox OS +- On X11, added `drag_resize_window` method. +- Added `Window::set_transparent` to provide a hint about transparency of the window on Wayland and macOS. +- On macOS, fix the mouse buttons other than left/right/middle being reported as middle. +- On Wayland, support fractional scaling via the wp-fractional-scale protocol. +- On web, fix removal of mouse event listeners from the global object upon window destruction. +- Add WindowAttributes getter to WindowBuilder to allow introspection of default values. +- Added `Window::set_ime_purpose` for setting the IME purpose, currently implemented on Wayland only. diff --git a/third_party/winit-0.30.13/src/changelog/v0.29.md b/third_party/winit-0.30.13/src/changelog/v0.29.md new file mode 100644 index 0000000..fc17f17 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.29.md @@ -0,0 +1,289 @@ +## 0.29.15 + +- On X11, fix crash due to xsettings query on systems with incomplete xsettings. + +## 0.29.14 + +- On X11/Wayland, fix `text` and `text_with_all_modifiers` not being `None` during compose. +- On Wayland, don't reapply cursor grab when unchanged. +- On X11, fix a bug where some mouse events would be unexpectedly filtered out. + +## 0.29.13 + +- On Web, fix possible crash with `ControlFlow::Wait` and `ControlFlow::WaitUntil`. + +## 0.29.12 + +- On X11, fix use after free during xinput2 handling. +- On X11, filter close to zero values in mouse device events + +## 0.29.11 + +- Fix compatibility with 32-bit platforms without 64-bit atomics. +- On macOS, fix incorrect IME cursor rect origin. +- On Windows, fixed a race condition when sending an event through the loop proxy. +- On X11, fix swapped instance and general class names. +- On X11, don't require XIM to run. +- On X11, fix xkb state not being updated correctly sometimes leading to wrong input. +- On X11, reload dpi on `_XSETTINGS_SETTINGS` update. +- On X11, fix deadlock when adjusting DPI and resizing at the same time. +- On Wayland, disable `Occluded` event handling. +- On Wayland, fix DeviceEvent::Motion not being sent +- On Wayland, fix `Focused(false)` being send when other seats still have window focused. +- On Wayland, fix `Window::set_{min,max}_inner_size` not always applied. +- On Wayland, fix title in CSD not updated from `AboutToWait`. +- On Windows, fix inconsistent resizing behavior with multi-monitor setups when repositioning outside the event loop. +- On Wayland, fix `WAYLAND_SOCKET` not used when detecting platform. +- On Orbital, fix `logical_key` and `text` not reported in `KeyEvent`. +- On Orbital, implement `KeyEventExtModifierSupplement`. +- On Orbital, map keys to `NamedKey` when possible. +- On Orbital, implement `set_cursor_grab`. +- On Orbital, implement `set_cursor_visible`. +- On Orbital, implement `drag_window`. +- On Orbital, implement `drag_resize_window`. +- On Orbital, implement `set_transparent`. +- On Orbital, implement `set_visible`. +- On Orbital, implement `is_visible`. +- On Orbital, implement `set_resizable`. +- On Orbital, implement `is_resizable`. +- On Orbital, implement `set_maximized`. +- On Orbital, implement `is_maximized`. +- On Orbital, implement `set_decorations`. +- On Orbital, implement `is_decorated`. +- On Orbital, implement `set_window_level`. +- On Orbital, emit `DeviceEvent::MouseMotion`. + +## 0.29.10 + +- On Web, account for canvas being focused already before event loop starts. +- On Web, increase cursor position accuracy. + +## 0.29.9 + +- On X11, fix `NotSupported` error not propagated when creating event loop. +- On Wayland, fix resize not issued when scale changes +- On X11 and Wayland, fix arrow up on keypad reported as `ArrowLeft`. +- On macOS, report correct logical key when Ctrl or Cmd is pressed. + +## 0.29.8 + +- On X11, fix IME input lagging behind. +- On X11, fix `ModifiersChanged` not sent from xdotool-like input +- On X11, fix keymap not updated from xmodmap. +- On X11, reduce the amount of time spent fetching screen resources. +- On Wayland, fix `Window::request_inner_size` being overwritten by resize. +- On Wayland, fix `Window::inner_size` not using the correct rounding. + +## 0.29.7 + +- On X11, fix `Xft.dpi` reload during runtime. +- On X11, fix window minimize. + +## 0.29.6 + +- On Web, fix context menu not being disabled by `with_prevent_default(true)`. +- On Wayland, fix `WindowEvent::Destroyed` not being delivered after destroying window. +- Fix `EventLoopExtRunOnDemand::run_on_demand` not working for consequent invocation + +## 0.29.5 + +- On macOS, remove spurious error logging when handling `Fn`. +- On X11, fix an issue where floating point data from the server is + misinterpreted during a drag and drop operation. +- On X11, fix a bug where focusing the window would panic. +- On macOS, fix `refresh_rate_millihertz`. +- On Wayland, disable Client Side Decorations when `wl_subcompositor` is not supported. +- On X11, fix `Xft.dpi` detection from Xresources. +- On Windows, fix consecutive calls to `window.set_fullscreen(Some(Fullscreen::Borderless(None)))` resulting in losing previous window state when eventually exiting fullscreen using `window.set_fullscreen(None)`. +- On Wayland, fix resize being sent on focus change. +- On Windows, fix `set_ime_cursor_area`. + +## 0.29.4 + +- Fix crash when running iOS app on macOS. +- On X11, check common alternative cursor names when loading cursor. +- On X11, reload the DPI after a property change event. +- On Windows, fix so `drag_window` and `drag_resize_window` can be called from another thread. +- On Windows, fix `set_control_flow` in `AboutToWait` not being taken in account. +- On macOS, send a `Resized` event after each `ScaleFactorChanged` event. +- On Wayland, fix `wl_surface` being destroyed before associated objects. +- On macOS, fix assertion when pressing `Fn` key. +- On Windows, add `WindowBuilderExtWindows::with_clip_children` to control `WS_CLIPCHILDREN` style. + +## 0.29.3 + +- On Wayland, apply correct scale to `PhysicalSize` passed in `WindowBuilder::with_inner_size` when possible. +- On Wayland, fix `RedrawRequested` being always sent without decorations and `sctk-adwaita` feature. +- On Wayland, ignore resize requests when the window is fully tiled. +- On Wayland, use `configure_bounds` to constrain `with_inner_size` when compositor wants users to pick size. +- On Windows, fix deadlock when accessing the state during `Cursor{Enter,Leave}`. +- On Windows, add support for `Window::set_transparent`. +- On macOS, fix deadlock when entering a nested event loop from an event handler. +- On macOS, add support for `Window::set_blur`. + +## 0.29.2 + +- **Breaking:** Bump MSRV from `1.60` to `1.65`. +- **Breaking:** Add `Event::MemoryWarning`; implemented on iOS/Android. +- **Breaking:** Bump `ndk` version to `0.8.0`, ndk-sys to `0.5.0`, `android-activity` to `0.5.0`. +- **Breaking:** Change default `ControlFlow` from `Poll` to `Wait`. +- **Breaking:** Move `Event::RedrawRequested` to `WindowEvent::RedrawRequested`. +- **Breaking:** Moved `ControlFlow::Exit` to `EventLoopWindowTarget::exit()` and `EventLoopWindowTarget::exiting()` and removed `ControlFlow::ExitWithCode(_)` entirely. +- **Breaking:** Moved `ControlFlow` to `EventLoopWindowTarget::set_control_flow()` and `EventLoopWindowTarget::control_flow()`. +- **Breaking:** `EventLoop::new` and `EventLoopBuilder::build` now return `Result` +- **Breaking:** `WINIT_UNIX_BACKEND` was removed in favor of standard `WAYLAND_DISPLAY` and `DISPLAY` variables. +- **Breaking:** on Wayland, dispatching user created Wayland queue won't wake up the loop unless winit has event to send back. +- **Breaking:** remove `DeviceEvent::Text`. +- **Breaking:** Remove lifetime parameter from `Event` and `WindowEvent`. +- **Breaking:** Rename `Window::set_inner_size` to `Window::request_inner_size` and indicate if the size was applied immediately. +- **Breaking:** `ActivationTokenDone` event which could be requested with the new `startup_notify` module, see its docs for more. +- **Breaking:** `ScaleFactorChanged` now contains a writer instead of a reference to update inner size. +- **Breaking** `run() -> !` has been replaced by `run() -> Result<(), EventLoopError>` for returning errors without calling `std::process::exit()` ([#2767](https://github.com/rust-windowing/winit/pull/2767)) +- **Breaking** Removed `EventLoopExtRunReturn` / `run_return` in favor of `EventLoopExtPumpEvents` / `pump_events` and `EventLoopExtRunOnDemand` / `run_on_demand` ([#2767](https://github.com/rust-windowing/winit/pull/2767)) +- `RedrawRequested` is no longer guaranteed to be emitted after `MainEventsCleared`, it is now platform-specific when the event is emitted after being requested via `redraw_request()`. + - On Windows, `RedrawRequested` is now driven by `WM_PAINT` messages which are requested via `redraw_request()` +- **Breaking** `LoopDestroyed` renamed to `LoopExiting` ([#2900](https://github.com/rust-windowing/winit/issues/2900)) +- **Breaking** `RedrawEventsCleared` removed ([#2900](https://github.com/rust-windowing/winit/issues/2900)) +- **Breaking** `MainEventsCleared` removed ([#2900](https://github.com/rust-windowing/winit/issues/2900)) +- **Breaking:** Remove all deprecated `modifiers` fields. +- **Breaking:** Rename `DeviceEventFilter` to `DeviceEvents` reversing the behavior of variants. +- **Breaking** Add `AboutToWait` event which is emitted when the event loop is about to block and wait for new events ([#2900](https://github.com/rust-windowing/winit/issues/2900)) +- **Breaking:** Rename `EventLoopWindowTarget::set_device_event_filter` to `listen_device_events`. +- **Breaking:** Rename `Window::set_ime_position` to `Window::set_ime_cursor_area` adding a way to set exclusive zone. +- **Breaking:** `with_x11_visual` now takes the visual ID instead of the bare pointer. +- **Breaking** `MouseButton` now supports `Back` and `Forward` variants, emitted from mouse events on Wayland, X11, Windows, macOS and Web. +- **Breaking:** On Web, `instant` is now replaced by `web_time`. +- **Breaking:** On Web, dropped support for Safari versions below 13.1. +- **Breaking:** On Web, the canvas output bitmap size is no longer adjusted. +- **Breaking:** On Web, the canvas size is not controlled by Winit anymore and external changes to the canvas size will be reported through `WindowEvent::Resized`. +- **Breaking:** Updated `bitflags` crate version to `2`, which changes the API on exposed types. +- **Breaking:** `CursorIcon::Arrow` was removed. +- **Breaking:** `CursorIcon::Hand` is now named `CursorIcon::Pointer`. +- **Breaking:** `CursorIcon` is now used from the `cursor-icon` crate. +- **Breaking:** `WindowExtWebSys::canvas()` now returns an `Option`. +- **Breaking:** Overhaul keyboard input handling. + - Replace `KeyboardInput` with `KeyEvent` and `RawKeyEvent`. + - Change `WindowEvent::KeyboardInput` to contain a `KeyEvent`. + - Change `Event::Key` to contain a `RawKeyEvent`. + - Remove `Event::ReceivedCharacter`. In its place, you should use + `KeyEvent.text` in combination with `WindowEvent::Ime`. + - Replace `VirtualKeyCode` with the `Key` enum. + - Replace `ScanCode` with the `KeyCode` enum. + - Rename `ModifiersState::LOGO` to `SUPER` and `ModifiersState::CTRL` to `CONTROL`. + - Add `PhysicalKey` wrapping `KeyCode` and `NativeKeyCode`. + - Add `KeyCode` to refer to keys (roughly) by their physical location. + - Add `NativeKeyCode` to represent raw `KeyCode`s which Winit doesn't + understand. + - Add `Key` to represent the keys after they've been interpreted by the + active (software) keyboard layout. + - Add `NamedKey` to represent the categorized keys. + - Add `NativeKey` to represent raw `Key`s which Winit doesn't understand. + - Add `KeyLocation` to tell apart `Key`s which usually "mean" the same thing, + but can appear simultaneously in different spots on the same keyboard + layout. + - Add `Window::reset_dead_keys` to enable application-controlled cancellation + of dead key sequences. + - Add `KeyEventExtModifierSupplement` to expose additional (and less + portable) interpretations of a given key-press. + - Add `PhysicalKeyExtScancode`, which lets you convert between scancodes and + `PhysicalKey`. + - `ModifiersChanged` now uses dedicated `Modifiers` struct. +- Removed platform-specific extensions that should be retrieved through `raw-window-handle` trait implementations instead: + - `platform::windows::HINSTANCE`. + - `WindowExtWindows::hinstance`. + - `WindowExtWindows::hwnd`. + - `WindowExtIOS::ui_window`. + - `WindowExtIOS::ui_view_controller`. + - `WindowExtIOS::ui_view`. + - `WindowExtMacOS::ns_window`. + - `WindowExtMacOS::ns_view`. + - `EventLoopWindowTargetExtWayland::wayland_display`. + - `WindowExtWayland::wayland_surface`. + - `WindowExtWayland::wayland_display`. + - `WindowExtX11::xlib_window`. + - `WindowExtX11::xlib_display`. + - `WindowExtX11::xlib_screen_id`. + - `WindowExtX11::xcb_connection`. +- Reexport `raw-window-handle` in `window` module. +- Add `ElementState::is_pressed`. +- Add `Window::pre_present_notify` to notify winit before presenting to the windowing system. +- Add `Window::set_blur` to request a blur behind the window; implemented on Wayland for now. +- Add `Window::show_window_menu` to request a titlebar/system menu; implemented on Wayland/Windows for now. +- Implement `AsFd`/`AsRawFd` for `EventLoop` on X11 and Wayland. +- Implement `PartialOrd` and `Ord` for `MouseButton`. +- Implement `PartialOrd` and `Ord` on types in the `dpi` module. +- Make `WindowBuilder` `Send + Sync`. +- Make iOS `MonitorHandle` and `VideoMode` usable from other threads. +- Make iOS windows usable from other threads. +- On Android, add force data to touch events. +- On Android, added `EventLoopBuilderExtAndroid::handle_volume_keys` to indicate that the application will handle the volume keys manually. +- On Android, fix `DeviceId` to contain device id's. +- On Orbital, fix `ModifiersChanged` not being sent. +- On Wayland, `Window::outer_size` now accounts for **client side** decorations. +- On Wayland, add `Window::drag_resize_window` method. +- On Wayland, remove `WINIT_WAYLAND_CSD_THEME` variable. +- On Wayland, fix `TouchPhase::Canceled` being sent for moved events. +- On Wayland, fix forward compatibility issues. +- On Wayland, fix initial window size not restored for maximized/fullscreened on startup window. +- On Wayland, fix maximized startup not taking full size on GNOME. +- On Wayland, fix maximized window creation and window geometry handling. +- On Wayland, fix window not checking that it actually got initial configure event. +- On Wayland, make double clicking and moving the CSD frame more reliable. +- On Wayland, support `Occluded` event with xdg-shell v6 +- On Wayland, use frame callbacks to throttle `RedrawRequested` events so redraws will align with compositor. +- On Web, `ControlFlow::WaitUntil` now uses the Prioritized Task Scheduling API. `setTimeout()`, with a trick to circumvent throttling to 4ms, is used as a fallback. +- On Web, `EventLoopProxy` now implements `Send`. +- On Web, `Window` now implements `Send` and `Sync`. +- On Web, account for CSS `padding`, `border`, and `margin` when getting or setting the canvas position. +- On Web, add Fullscreen API compatibility for Safari. +- On Web, add `DeviceEvent::Motion`, `DeviceEvent::MouseWheel`, `DeviceEvent::Button` and `DeviceEvent::Key` support. +- On Web, add `EventLoopWindowTargetExtWebSys` and `PollStrategy`, which allows to set different strategies for `ControlFlow::Poll`. By default the Prioritized Task Scheduling API is used, but an option to use `Window.requestIdleCallback` is available as well. Both use `setTimeout()`, with a trick to circumvent throttling to 4ms, as a fallback. +- On Web, add `WindowBuilderExtWebSys::with_append()` to append the canvas element to the web page on creation. +- On Web, allow event loops to be recreated with `spawn`. +- On Web, enable event propagation. +- On Web, fix `ControlFlow::WaitUntil` to never wake up **before** the given time. +- On Web, fix `DeviceEvent::MouseMotion` only being emitted for each canvas instead of the whole window. +- On Web, fix `Window:::set_fullscreen` doing nothing when called outside the event loop but during transient activation. +- On Web, fix pen treated as mouse input. +- On Web, fix pointer button events not being processed when a buttons is already pressed. +- On Web, fix scale factor resize suggestion always overwriting the canvas size. +- On Web, fix some `WindowBuilder` methods doing nothing. +- On Web, fix some `Window` methods using incorrect HTML attributes instead of CSS properties. +- On Web, fix the bfcache by not using the `beforeunload` event and map bfcache loading/unloading to `Suspended`/`Resumed` events. +- On Web, fix touch input not gaining or losing focus. +- On Web, fix touch location to be as accurate as mouse position. +- On Web, handle coalesced pointer events, which increases the resolution of pointer inputs. +- On Web, implement `Window::focus_window()`. +- On Web, implement `Window::set_(min|max)_inner_size()`. +- On Web, implement `WindowEvent::Occluded`. +- On Web, never return a `MonitorHandle`. +- On Web, prevent clicks on the canvas to select text. +- On Web, remove any fullscreen requests from the queue when an external fullscreen activation was detected. +- On Web, remove unnecessary `Window::is_dark_mode()`, which was replaced with `Window::theme()`. +- On Web, respect `EventLoopWindowTarget::listen_device_events()` settings. +- On Web, scale factor and dark mode detection are now more robust. +- On Web, send mouse position on button release as well. +- On Web, take all transient activations on the canvas and window into account to queue a fullscreen request. +- On Web, use `Window.requestAnimationFrame()` to throttle `RedrawRequested` events. +- On Web, use the correct canvas size when calculating the new size during scale factor change, instead of using the output bitmap size. +- On Web: fix `Window::request_redraw` not waking the event loop when called from outside the loop. +- On Web: fix position of touch events to be relative to the canvas. +- On Windows, add `drag_resize_window` method support. +- On Windows, add horizontal MouseWheel `DeviceEvent`. +- On Windows, added `WindowBuilderExtWindows::with_class_name` to customize the internal class name. +- On Windows, fix IME APIs not working when from non event loop thread. +- On Windows, fix `CursorEnter/Left` not being sent when grabbing the mouse. +- On Windows, fix `RedrawRequested` not being delivered when calling `Window::request_redraw` from `RedrawRequested`. +- On Windows, port to `windows-sys` version 0.48.0. +- On X11, add a `with_embedded_parent_window` function to the window builder to allow embedding a window into another window. +- On X11, fix event loop not waking up on `ControlFlow::Poll` and `ControlFlow::WaitUntil`. +- On X11, fix false positive flagging of key repeats when pressing different keys with no release between presses. +- On X11, set `visual_id` in returned `raw-window-handle`. +- On iOS, add ability to change the status bar style. +- On iOS, add force data to touch events when using the Apple Pencil. +- On iOS, always wake the event loop when transitioning from `ControlFlow::Poll` to `ControlFlow::Poll`. +- On iOS, send events `WindowEvent::Occluded(false)`, `WindowEvent::Occluded(true)` when application enters/leaves foreground. +- On macOS, add tabbing APIs on `WindowExtMacOS` and `EventLoopWindowTargetExtMacOS`. +- On macOS, fix assertion when pressing `Globe` key. +- On macOS, fix crash in `window.set_minimized(false)`. +- On macOS, fix crash when dropping `Window`. diff --git a/third_party/winit-0.30.13/src/changelog/v0.30.md b/third_party/winit-0.30.13/src/changelog/v0.30.md new file mode 100644 index 0000000..6b0b47d --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.30.md @@ -0,0 +1,413 @@ +## 0.30.13 + +### Added + +- On Wayland, add `Window::set_resize_increments`. + +### Fixed + +- On macOS, fixed crash when dragging non-file content onto window. +- On X11, fix `set_hittest` not working on some window managers. +- On X11, fix debug mode overflow panic in `set_timestamp`. +- On macOS, fix crash in `set_marked_text` when native Pinyin IME sends out-of-bounds `selected_range`. +- On Windows, fix `WM_IME_SETCONTEXT` IME UI flag masking on `lParam`. +- On Android, populate `KeyEvent::text` and `KeyEvent::text_with_all_modifiers` via `Key::to_text()`. + +## 0.30.12 + +### Fixed + +- On macOS, fix crash on macOS 26 by using objc2's `relax-sign-encoding` feature. + +## 0.30.11 + +### Fixed + +- On Windows, fixed crash in should_apps_use_dark_mode() for Windows versions < 17763. +- On Wayland, fixed `pump_events` driven loop deadlocking when loop was not drained before exit. + +## 0.30.10 + +### Added + +- On Windows, add `IconExtWindows::from_resource_name`. +- On Windows, add `CursorGrabMode::Locked`. +- On Wayland, add `WindowExtWayland::xdg_toplevel`. + +### Changed + +- On macOS, no longer need control of the main `NSApplication` class (which means you can now override it yourself). +- On iOS, remove custom application delegates. You are now allowed to override the + application delegate yourself. +- On iOS, no longer act as-if the application successfully open all URLs. Override + `application:didFinishLaunchingWithOptions:` and provide the desired behaviour yourself. + +### Fixed + +- On Windows, fixed ~500 ms pause when clicking the title bar during continuous redraw. +- On macOS, `WindowExtMacOS::set_simple_fullscreen` now honors `WindowExtMacOS::set_borderless_game` +- On X11 and Wayland, fixed pump_events with `Some(Duration::Zero)` blocking with `Wait` polling mode +- On Wayland, fixed a crash when consequently calling `set_cursor_grab` without pointer focus. +- On Wayland, ensure that external event loop is woken-up when using pump_events and integrating via `FD`. +- On Wayland, apply fractional scaling to custom cursors. +- On macOS, fixed `run_app_on_demand` returning without closing open windows. +- On macOS, fixed `VideoMode::refresh_rate_millihertz` for fractional refresh rates. +- On macOS, store monitor handle to avoid panics after going in/out of sleep. +- On macOS, allow certain invalid monitor handles and return `None` instead of panicking. +- On Windows, fixed `Ime::Preedit` cursor offset calculation. + +## 0.30.9 + +### Changed + +- On Wayland, no longer send an explicit clearing `Ime::Preedit` just prior to a new `Ime::Preedit`. + +### Fixed + +- On X11, fix crash with uim. +- On X11, fix modifiers for keys that were sent by the same X11 request. +- On iOS, fix high CPU usage even when using `ControlFlow::Wait`. + +## 0.30.8 + +### Added + +- `ActivationToken::from_raw` and `ActivationToken::into_raw`. +- On X11, add a workaround for disabling IME on GNOME. + +### Fixed + +- On Windows, fixed the event loop not waking on accessibility requests. +- On X11, fixed cursor grab mode state tracking on error. + +## 0.30.7 + +### Fixed + +- On X11, fixed KeyboardInput delivered twice when IME enabled. + +## 0.30.6 + +### Added + +- On macOS, add `WindowExtMacOS::set_borderless_game` and `WindowAttributesExtMacOS::with_borderless_game` + to fully disable the menu bar and dock in Borderless Fullscreen as commonly done in games. +- On X11, the `window` example now understands the `X11_VISUAL_ID` and `X11_SCREEN_ID` env + variables to test the respective modifiers of window creation. +- On Android, the soft keyboard can now be shown using `Window::set_ime_allowed`. +- Add basic iOS IME support. The soft keyboard can now be shown using `Window::set_ime_allowed`. + +### Fixed + +- On macOS, fix `WindowEvent::Moved` sometimes being triggered unnecessarily on resize. +- On macOS, package manifest definitions of `LSUIElement` will no longer be overridden with the + default activation policy, unless explicitly provided during initialization. +- On macOS, fix crash when calling `drag_window()` without a left click present. +- On X11, key events forward to IME anyway, even when it's disabled. +- On Windows, make `ControlFlow::WaitUntil` work more precisely using `CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`. +- On X11, creating windows on screen that is not the first one (e.g. `DISPLAY=:0.1`) works again. +- On X11, creating windows while passing `with_x11_screen(non_default_screen)` works again. +- On X11, fix XInput handling that prevented a new window from getting the focus in some cases. +- On macOS, fix crash when pressing Caps Lock in certain configurations. +- On iOS, fixed `MonitorHandle`'s `PartialEq` and `Hash` implementations. +- On macOS, fixed undocumented cursors (e.g. zoom, resize, help) always appearing to be invalid and falling back to the default cursor. + +## 0.30.5 + +### Added + +- Add `ActiveEventLoop::system_theme()`, returning the current system theme. +- On Web, implement `Error` for `platform::web::CustomCursorError`. +- On Android, add `{Active,}EventLoopExtAndroid::android_app()` to access the app used to create the loop. + +### Fixed + +- On MacOS, fix building with `feature = "rwh_04"`. +- On Web, pen events are now routed through to `WindowEvent::Cursor*`. +- On macOS, fix panic when releasing not available monitor. +- On MacOS, return the system theme in `Window::theme()` if no theme override is set. + +## 0.30.4 + +### Changed + +- `DeviceId::dummy()` and `WindowId::dummy()` are no longer marked `unsafe`. + +### Fixed + +- On Wayland, avoid crashing when compositor is misbehaving. +- On Web, fix `WindowEvent::Resized` not using `requestAnimationFrame` when sending + `WindowEvent::RedrawRequested` and also potentially causing `WindowEvent::RedrawRequested` + to not be de-duplicated. +- Account for different browser engine implementations of pointer movement coordinate space. + +## 0.30.3 + +### Added + +- On Web, add `EventLoopExtWebSys::(set_)poll_strategy()` to allow setting + control flow strategies before starting the event loop. +- On Web, add `WaitUntilStrategy`, which allows to set different strategies for + `ControlFlow::WaitUntil`. By default the Prioritized Task Scheduling API is + used, with a fallback to `setTimeout()` with a trick to circumvent throttling + to 4ms. But an option to use a Web worker to schedule the timer is available + as well, which commonly prevents any throttling when the window is not focused. + +### Changed + +- On macOS, set the window theme on the `NSWindow` instead of application-wide. + +### Fixed + +- On X11, build on arm platforms. +- On macOS, fixed `WindowBuilder::with_theme` not having any effect on the window. + +## 0.30.2 + +### Fixed + +- On Web, fix `EventLoopProxy::send_event()` triggering event loop immediately + when not called from inside the event loop. Now queues a microtask instead. +- On Web, stop overwriting default cursor with `CursorIcon::Default`. +- On Web, prevent crash when using `InnerSizeWriter::request_inner_size()`. +- On macOS, fix not working opacity for entire window. + +## 0.30.1 + +### Added + +- Reexport `raw-window-handle` versions 0.4 and 0.5 as `raw_window_handle_04` and `raw_window_handle_05`. +- Implement `ApplicationHandler` for `&mut` references and heap allocations to something that implements `ApplicationHandler`. + +### Fixed + +- On macOS, fix panic on exit when dropping windows outside the event loop. +- On macOS, fix window dragging glitches when dragging across a monitor boundary with different scale factor. +- On macOS, fix the range in `Ime::Preedit`. +- On macOS, use the system's internal mechanisms for queuing events. +- On macOS, handle events directly instead of queuing when possible. + +## 0.30.0 + +### Added + +- Add `OwnedDisplayHandle` type for allowing safe display handle usage outside of + trivial cases. +- Add `ApplicationHandler` trait which mimics `Event`. +- Add `WindowBuilder::with_cursor` and `Window::set_cursor` which takes a + `CursorIcon` or `CustomCursor`. +- Add `Sync` implementation for `EventLoopProxy`. +- Add `Window::default_attributes` to get default `WindowAttributes`. +- Add `EventLoop::builder` to get `EventLoopBuilder` without export. +- Add `CustomCursor::from_rgba` to allow creating cursor images from RGBA data. +- Add `CustomCursorExtWebSys::from_url` to allow loading cursor images from URLs. +- Add `CustomCursorExtWebSys::from_animation` to allow creating animated + cursors from other `CustomCursor`s. +- Add `{Active,}EventLoop::create_custom_cursor` to load custom cursor image sources. +- Add `ActiveEventLoop::create_window` and `EventLoop::create_window`. +- Add `CustomCursor` which could be set via `Window::set_cursor`, implemented on + Windows, macOS, X11, Wayland, and Web. +- On Web, add to toggle calling `Event.preventDefault()` on `Window`. +- On iOS, add `PinchGesture`, `DoubleTapGesture`, `PanGesture` and `RotationGesture`. +- on iOS, use `UIGestureRecognizerDelegate` for fine grained control of gesture recognizers. +- On macOS, add services menu. +- On Windows, add `with_title_text_color`, and `with_corner_preference` on + `WindowAttributesExtWindows`. +- On Windows, implement resize increments. +- On Windows, add `AnyThread` API to access window handle off the main thread. + +### Changed + +- Bump MSRV from `1.65` to `1.70`. +- On Wayland, bump `sctk-adwaita` to `0.9.0`, which changed system library + crates. This change is a **cascading breaking change**, you must do breaking + change as well, even if you don't expose winit. +- Rename `TouchpadMagnify` to `PinchGesture`. +- Rename `SmartMagnify` to `DoubleTapGesture`. +- Rename `TouchpadRotate` to `RotationGesture`. +- Rename `EventLoopWindowTarget` to `ActiveEventLoop`. +- Rename `platform::x11::XWindowType` to `platform::x11::WindowType`. +- Rename `VideoMode` to `VideoModeHandle` to represent that it doesn't hold + static data. +- Make `Debug` formatting of `WindowId` more concise. +- Move `dpi` types to its own crate, and re-export it from the root crate. +- Replace `log` with `tracing`, use `log` feature on `tracing` to restore old + behavior. +- `EventLoop::with_user_event` now returns `EventLoopBuilder`. +- On Web, return `HandleError::Unavailable` when a window handle is not available. +- On Web, return `RawWindowHandle::WebCanvas` instead of `RawWindowHandle::Web`. +- On Web, remove queuing fullscreen request in absence of transient activation. +- On iOS, return `HandleError::Unavailable` when a window handle is not available. +- On macOS, return `HandleError::Unavailable` when a window handle is not available. +- On Windows, remove `WS_CAPTION`, `WS_BORDER`, and `WS_EX_WINDOWEDGE` styles + for child windows without decorations. +- On Android, bump `ndk` to `0.9.0` and `android-activity` to `0.6.0`, + and remove unused direct dependency on `ndk-sys`. + +### Deprecated + +- Deprecate `EventLoop::run`, use `EventLoop::run_app`. +- Deprecate `EventLoopExtRunOnDemand::run_on_demand`, use `EventLoop::run_app_on_demand`. +- Deprecate `EventLoopExtPumpEvents::pump_events`, use `EventLoopExtPumpEvents::pump_app_events`. + + The new `app` APIs accept a newly added `ApplicationHandler` instead of + `Fn`. The semantics are mostly the same, given that the capture list of the + closure is your new `State`. Consider the following code: + + ```rust,no_run + use winit::event::Event; + use winit::event_loop::EventLoop; + use winit::window::Window; + + struct MyUserEvent; + + let event_loop = EventLoop::::with_user_event().build().unwrap(); + let window = event_loop.create_window(Window::default_attributes()).unwrap(); + let mut counter = 0; + + let _ = event_loop.run(move |event, event_loop| { + match event { + Event::AboutToWait => { + window.request_redraw(); + counter += 1; + } + Event::WindowEvent { window_id, event } => { + // Handle window event. + } + Event::UserEvent(event) => { + // Handle user event. + } + Event::DeviceEvent { device_id, event } => { + // Handle device event. + } + _ => (), + } + }); + ``` + + To migrate this code, you should move all the captured values into some + newtype `State` and implement `ApplicationHandler` for this type. Finally, + we move particular `match event` arms into methods on `ApplicationHandler`, + for example: + + ```rust,no_run + use winit::application::ApplicationHandler; + use winit::event::{Event, WindowEvent, DeviceEvent, DeviceId}; + use winit::event_loop::{EventLoop, ActiveEventLoop}; + use winit::window::{Window, WindowId}; + + struct MyUserEvent; + + struct State { + window: Window, + counter: i32, + } + + impl ApplicationHandler for State { + fn user_event(&mut self, event_loop: &ActiveEventLoop, user_event: MyUserEvent) { + // Handle user event. + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + // Your application got resumed. + } + + fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) { + // Handle window event. + } + + fn device_event(&mut self, event_loop: &ActiveEventLoop, device_id: DeviceId, event: DeviceEvent) { + // Handle device event. + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + self.window.request_redraw(); + self.counter += 1; + } + } + + let event_loop = EventLoop::::with_user_event().build().unwrap(); + #[allow(deprecated)] + let window = event_loop.create_window(Window::default_attributes()).unwrap(); + let mut state = State { window, counter: 0 }; + + let _ = event_loop.run_app(&mut state); + ``` + + Please submit your feedback after migrating in [this issue](https://github.com/rust-windowing/winit/issues/3626). + +- Deprecate `Window::set_cursor_icon`, use `Window::set_cursor`. + +### Removed + +- Remove `Window::new`, use `ActiveEventLoop::create_window` instead. + + You now have to create your windows inside the actively running event loop + (usually the `new_events(cause: StartCause::Init)` or `resumed()` events), + and can no longer do it before the application has properly launched. + This change is done to fix many long-standing issues on iOS and macOS, and + will improve things on Wayland once fully implemented. + + To ease migration, we provide the deprecated `EventLoop::create_window` that + will allow you to bypass this restriction in this release. + + Using the migration example from above, you can change your code as follows: + + ```rust,no_run + use winit::application::ApplicationHandler; + use winit::event::{Event, WindowEvent, DeviceEvent, DeviceId}; + use winit::event_loop::{EventLoop, ActiveEventLoop}; + use winit::window::{Window, WindowId}; + + #[derive(Default)] + struct State { + // Use an `Option` to allow the window to not be available until the + // application is properly running. + window: Option, + counter: i32, + } + + impl ApplicationHandler for State { + // This is a common indicator that you can create a window. + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); + } + fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) { + // `unwrap` is fine, the window will always be available when + // receiving a window event. + let window = self.window.as_ref().unwrap(); + // Handle window event. + } + fn device_event(&mut self, event_loop: &ActiveEventLoop, device_id: DeviceId, event: DeviceEvent) { + // Handle window event. + } + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if let Some(window) = self.window.as_ref() { + window.request_redraw(); + self.counter += 1; + } + } + } + + let event_loop = EventLoop::new().unwrap(); + let mut state = State::default(); + let _ = event_loop.run_app(&mut state); + ``` + +- Remove `Deref` implementation for `EventLoop` that gave `EventLoopWindowTarget`. +- Remove `WindowBuilder` in favor of `WindowAttributes`. +- Remove Generic parameter `T` from `ActiveEventLoop`. +- Remove `EventLoopBuilder::with_user_event`, use `EventLoop::with_user_event`. +- Remove Redundant `EventLoopError::AlreadyRunning`. +- Remove `WindowAttributes::fullscreen` and expose as field directly. +- On X11, remove `platform::x11::XNotSupported` export. + +### Fixed + +- On Web, fix setting cursor icon overriding cursor visibility. +- On Windows, fix cursor not confined to center of window when grabbed and hidden. +- On macOS, fix sequence of mouse events being out of order when dragging on the trackpad. +- On Wayland, fix decoration glitch on close with some compositors. +- On Android, fix a regression introduced in #2748 to allow volume key events to be received again. +- On Windows, don't return a valid window handle outside of the GUI thread. +- On macOS, don't set the background color when initializing a window with transparency. diff --git a/third_party/winit-0.30.13/src/changelog/v0.8.md b/third_party/winit-0.30.13/src/changelog/v0.8.md new file mode 100644 index 0000000..1963429 --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.8.md @@ -0,0 +1,33 @@ +## 0.8.3 + +- Fixed issue of calls to `set_inner_size` blocking on Windows. +- Mapped `ISO_Left_Tab` to `VirtualKeyCode::Tab` to make the key work with modifiers +- Fixed the X11 backed on 32bit targets + +## 0.8.2 + +- Uniformize keyboard scancode values across Wayland and X11 (#297). +- Internal rework of the wayland event loop +- Added method `os::linux::WindowExt::is_ready` + +## 0.8.1 + +- Added various methods to `os::linux::EventsLoopExt`, plus some hidden items necessary to make + glutin work. + +## 0.8.0 + +- Added `Window::set_maximized`, `WindowAttributes::maximized` and `WindowBuilder::with_maximized`. +- Added `Window::set_fullscreen`. +- Changed `with_fullscreen` to take a `Option` instead of a `MonitorId`. +- Removed `MonitorId::get_native_identifier()` in favor of platform-specific traits in the `os` + module. +- Changed `get_available_monitors()` and `get_primary_monitor()` to be methods of `EventsLoop` + instead of stand-alone methods. +- Changed `EventsLoop` to be tied to a specific X11 or Wayland connection. +- Added a `os::linux::EventsLoopExt` trait that makes it possible to configure the connection. +- Fixed the emscripten code, which now compiles. +- Changed the X11 fullscreen code to use `xrandr` instead of `xxf86vm`. +- Fixed the Wayland backend to produce `Refresh` event after window creation. +- Changed the `Suspended` event to be outside of `WindowEvent`. +- Fixed the X11 backend sometimes reporting the wrong virtual key (#273). diff --git a/third_party/winit-0.30.13/src/changelog/v0.9.md b/third_party/winit-0.30.13/src/changelog/v0.9.md new file mode 100644 index 0000000..2a9e8cc --- /dev/null +++ b/third_party/winit-0.30.13/src/changelog/v0.9.md @@ -0,0 +1,22 @@ +## 0.9.0 + +- Added event `WindowEvent::HiDPIFactorChanged`. +- Added method `MonitorId::get_hidpi_factor`. +- Deprecated `get_inner_size_pixels` and `get_inner_size_points` methods of `Window` in favor of + `get_inner_size`. +- **Breaking:** `EventsLoop` is `!Send` and `!Sync` because of platform-dependant constraints, + but `Window`, `WindowId`, `DeviceId` and `MonitorId` guaranteed to be `Send`. +- `MonitorId::get_position` now returns `(i32, i32)` instead of `(u32, u32)`. +- Rewrite of the wayland backend to use wayland-client-0.11 +- Support for dead keys on wayland for keyboard utf8 input +- Monitor enumeration on Windows is now implemented using `EnumDisplayMonitors` instead of + `EnumDisplayDevices`. This changes the value returned by `MonitorId::get_name()`. +- On Windows added `MonitorIdExt::hmonitor` method +- Impl `Clone` for `EventsLoopProxy` +- `EventsLoop::get_primary_monitor()` on X11 will fallback to any available monitor if no primary is found +- Support for touch event on wayland +- `WindowEvent`s `MouseMoved`, `MouseEntered`, and `MouseLeft` have been renamed to + `CursorMoved`, `CursorEntered`, and `CursorLeft`. +- New `DeviceEvent`s added, `MouseMotion` and `MouseWheel`. +- Send `CursorMoved` event after `CursorEntered` and `Focused` events. +- Add support for `ModifiersState`, `MouseMove`, `MouseInput`, `MouseMotion` for emscripten backend. diff --git a/third_party/winit-0.30.13/src/cursor.rs b/third_party/winit-0.30.13/src/cursor.rs new file mode 100644 index 0000000..7bcac54 --- /dev/null +++ b/third_party/winit-0.30.13/src/cursor.rs @@ -0,0 +1,263 @@ +use core::fmt; +use std::error::Error; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use cursor_icon::CursorIcon; + +use crate::platform_impl::{PlatformCustomCursor, PlatformCustomCursorSource}; + +/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`]. +pub const MAX_CURSOR_SIZE: u16 = 2048; + +const PIXEL_SIZE: usize = 4; + +/// See [`Window::set_cursor()`][crate::window::Window::set_cursor] for more details. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum Cursor { + Icon(CursorIcon), + Custom(CustomCursor), +} + +impl Default for Cursor { + fn default() -> Self { + Self::Icon(CursorIcon::default()) + } +} + +impl From for Cursor { + fn from(icon: CursorIcon) -> Self { + Self::Icon(icon) + } +} + +impl From for Cursor { + fn from(custom: CustomCursor) -> Self { + Self::Custom(custom) + } +} + +/// Use a custom image as a cursor (mouse pointer). +/// +/// Is guaranteed to be cheap to clone. +/// +/// ## Platform-specific +/// +/// **Web**: Some browsers have limits on cursor sizes usually at 128x128. +/// +/// # Example +/// +/// ```no_run +/// # use winit::event_loop::ActiveEventLoop; +/// # use winit::window::Window; +/// # fn scope(event_loop: &ActiveEventLoop, window: &Window) { +/// use winit::window::CustomCursor; +/// +/// let w = 10; +/// let h = 10; +/// let rgba = vec![255; (w * h * 4) as usize]; +/// +/// #[cfg(not(target_family = "wasm"))] +/// let source = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap(); +/// +/// #[cfg(target_family = "wasm")] +/// let source = { +/// use winit::platform::web::CustomCursorExtWebSys; +/// CustomCursor::from_url(String::from("http://localhost:3000/cursor.png"), 0, 0) +/// }; +/// +/// let custom_cursor = event_loop.create_custom_cursor(source); +/// +/// window.set_cursor(custom_cursor.clone()); +/// # } +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct CustomCursor { + /// Platforms should make sure this is cheap to clone. + pub(crate) inner: PlatformCustomCursor, +} + +impl CustomCursor { + /// Creates a new cursor from an rgba buffer. + /// + /// The alpha channel is assumed to be **not** premultiplied. + pub fn from_rgba( + rgba: impl Into>, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + let _span = + tracing::debug_span!("winit::Cursor::from_rgba", width, height, hotspot_x, hotspot_y) + .entered(); + + Ok(CustomCursorSource { + inner: PlatformCustomCursorSource::from_rgba( + rgba.into(), + width, + height, + hotspot_x, + hotspot_y, + )?, + }) + } +} + +/// Source for [`CustomCursor`]. +/// +/// See [`CustomCursor`] for more details. +#[derive(Debug)] +pub struct CustomCursorSource { + pub(crate) inner: PlatformCustomCursorSource, +} + +/// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments. +#[derive(Debug, Clone)] +pub enum BadImage { + /// Produced when the image dimensions are larger than [`MAX_CURSOR_SIZE`]. This doesn't + /// guarantee that the cursor will work, but should avoid many platform and device specific + /// limits. + TooLarge { width: u16, height: u16 }, + /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be + /// safely interpreted as 32bpp RGBA pixels. + 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. + DimensionsVsPixelCount { width: u16, height: u16, width_x_height: u64, pixel_count: u64 }, + /// Produced when the hotspot is outside the image bounds + HotspotOutOfBounds { width: u16, height: u16, hotspot_x: u16, hotspot_y: u16 }, +} + +impl fmt::Display for BadImage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BadImage::TooLarge { width, height } => write!( + f, + "The specified dimensions ({width:?}x{height:?}) are too large. The maximum is \ + {MAX_CURSOR_SIZE:?}x{MAX_CURSOR_SIZE:?}.", + ), + BadImage::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.", + ), + BadImage::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:?}.", + ) + }, + BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y } => write!( + f, + "The specified hotspot ({hotspot_x:?}, {hotspot_y:?}) is outside the image bounds \ + ({width:?}x{height:?}).", + ), + } + } +} + +impl Error for BadImage {} + +/// Platforms export this directly as `PlatformCustomCursorSource` if they need to only work with +/// images. +#[allow(dead_code)] +#[derive(Debug)] +pub(crate) struct OnlyCursorImageSource(pub(crate) CursorImage); + +#[allow(dead_code)] +impl OnlyCursorImageSource { + pub(crate) fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y).map(Self) + } +} + +/// Platforms export this directly as `PlatformCustomCursor` if they don't implement caching. +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub(crate) struct OnlyCursorImage(pub(crate) Arc); + +impl Hash for OnlyCursorImage { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.0).hash(state); + } +} + +impl PartialEq for OnlyCursorImage { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for OnlyCursorImage {} + +#[derive(Debug)] +#[allow(dead_code)] +pub(crate) struct CursorImage { + pub(crate) rgba: Vec, + pub(crate) width: u16, + pub(crate) height: u16, + pub(crate) hotspot_x: u16, + pub(crate) hotspot_y: u16, +} + +impl CursorImage { + pub(crate) fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + if width > MAX_CURSOR_SIZE || height > MAX_CURSOR_SIZE { + return Err(BadImage::TooLarge { width, height }); + } + + if rgba.len() % PIXEL_SIZE != 0 { + return Err(BadImage::ByteCountNotDivisibleBy4 { byte_count: rgba.len() }); + } + + let pixel_count = (rgba.len() / PIXEL_SIZE) as u64; + let width_x_height = width as u64 * height as u64; + if pixel_count != width_x_height { + return Err(BadImage::DimensionsVsPixelCount { + width, + height, + width_x_height, + pixel_count, + }); + } + + if hotspot_x >= width || hotspot_y >= height { + return Err(BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y }); + } + + Ok(CursorImage { rgba, width, height, hotspot_x, hotspot_y }) + } +} + +// Platforms that don't support cursors will export this as `PlatformCustomCursor`. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct NoCustomCursor; + +#[allow(dead_code)] +impl NoCustomCursor { + pub(crate) fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?; + Ok(Self) + } +} diff --git a/third_party/winit-0.30.13/src/error.rs b/third_party/winit-0.30.13/src/error.rs new file mode 100644 index 0000000..d15bb9e --- /dev/null +++ b/third_party/winit-0.30.13/src/error.rs @@ -0,0 +1,131 @@ +use std::{error, fmt}; + +use crate::platform_impl; + +// TODO: Rename +/// An error that may be generated when requesting Winit state +#[derive(Debug)] +pub enum ExternalError { + /// The operation is not supported by the backend. + NotSupported(NotSupportedError), + /// The operation was ignored. + Ignored, + /// 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, +} + +/// A general error that may occur while running the Winit event loop +#[derive(Debug)] +pub enum EventLoopError { + /// The operation is not supported by the backend. + NotSupported(NotSupportedError), + /// The OS cannot perform the operation. + Os(OsError), + /// The event loop can't be re-created. + RecreationAttempt, + /// Application has exit with an error status. + ExitFailure(i32), +} + +impl From for EventLoopError { + fn from(value: OsError) -> Self { + Self::Os(value) + } +} + +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::Ignored => write!(f, "Operation was ignored"), + 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 Winit") + } +} + +impl fmt::Display for EventLoopError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + EventLoopError::RecreationAttempt => write!(f, "EventLoop can't be recreated"), + EventLoopError::NotSupported(e) => e.fmt(f), + EventLoopError::Os(e) => e.fmt(f), + EventLoopError::ExitFailure(status) => write!(f, "Exit Failure: {status}"), + } + } +} + +impl error::Error for OsError {} +impl error::Error for ExternalError {} +impl error::Error for NotSupportedError {} +impl error::Error for EventLoopError {} + +#[cfg(test)] +#[allow(clippy::redundant_clone)] +mod tests { + use super::*; + + // Eat attributes for testing + #[test] + fn ensure_fmt_does_not_panic() { + let _ = format!("{:?}, {}", NotSupportedError::new(), NotSupportedError::new().clone()); + let _ = format!( + "{:?}, {}", + ExternalError::NotSupported(NotSupportedError::new()), + ExternalError::NotSupported(NotSupportedError::new()) + ); + } +} diff --git a/third_party/winit-0.30.13/src/event.rs b/third_party/winit-0.30.13/src/event.rs new file mode 100644 index 0000000..4e01420 --- /dev/null +++ b/third_party/winit-0.30.13/src/event.rs @@ -0,0 +1,1183 @@ +//! The [`Event`] enum and assorted supporting types. +//! +//! These are sent to the closure given to [`EventLoop::run_app(...)`], 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_app(...)`] like this: +//! +//! ```rust,ignore +//! let mut start_cause = StartCause::Init; +//! +//! while !elwt.exiting() { +//! app.new_events(event_loop, start_cause); +//! +//! for event in (window events, user events, device events) { +//! // This will pick the right method on the application based on the event. +//! app.handle_event(event_loop, event); +//! } +//! +//! for window_id in (redraw windows) { +//! app.window_event(event_loop, window_id, RedrawRequested); +//! } +//! +//! app.about_to_wait(event_loop); +//! start_cause = wait_if_necessary(); +//! } +//! +//! app.exiting(event_loop); +//! ``` +//! +//! This leaves out timing details like [`ControlFlow::WaitUntil`] but hopefully +//! describes what happens in what order. +//! +//! [`EventLoop::run_app(...)`]: crate::event_loop::EventLoop::run_app +//! [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil +use std::path::PathBuf; +use std::sync::{Mutex, Weak}; +#[cfg(not(web_platform))] +use std::time::Instant; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use smol_str::SmolStr; +#[cfg(web_platform)] +use web_time::Instant; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::error::ExternalError; +use crate::event_loop::AsyncRequestSerial; +use crate::keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState}; +use crate::platform_impl; +#[cfg(doc)] +use crate::window::Window; +use crate::window::{ActivationToken, Theme, WindowId}; + +/// Describes a generic event. +/// +/// See the module-level docs for more information on the event loop manages each event. +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + /// See [`ApplicationHandler::new_events`] for details. + /// + /// [`ApplicationHandler::new_events`]: crate::application::ApplicationHandler::new_events + NewEvents(StartCause), + + /// See [`ApplicationHandler::window_event`] for details. + /// + /// [`ApplicationHandler::window_event`]: crate::application::ApplicationHandler::window_event + WindowEvent { window_id: WindowId, event: WindowEvent }, + + /// See [`ApplicationHandler::device_event`] for details. + /// + /// [`ApplicationHandler::device_event`]: crate::application::ApplicationHandler::device_event + DeviceEvent { device_id: DeviceId, event: DeviceEvent }, + + /// See [`ApplicationHandler::user_event`] for details. + /// + /// [`ApplicationHandler::user_event`]: crate::application::ApplicationHandler::user_event + UserEvent(T), + + /// See [`ApplicationHandler::suspended`] for details. + /// + /// [`ApplicationHandler::suspended`]: crate::application::ApplicationHandler::suspended + Suspended, + + /// See [`ApplicationHandler::resumed`] for details. + /// + /// [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed + Resumed, + + /// See [`ApplicationHandler::about_to_wait`] for details. + /// + /// [`ApplicationHandler::about_to_wait`]: crate::application::ApplicationHandler::about_to_wait + AboutToWait, + + /// See [`ApplicationHandler::exiting`] for details. + /// + /// [`ApplicationHandler::exiting`]: crate::application::ApplicationHandler::exiting + LoopExiting, + + /// See [`ApplicationHandler::memory_warning`] for details. + /// + /// [`ApplicationHandler::memory_warning`]: crate::application::ApplicationHandler::memory_warning + MemoryWarning, +} + +impl Event { + #[allow(clippy::result_large_err)] + pub fn map_nonuser_event(self) -> Result, Event> { + 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)), + AboutToWait => Ok(AboutToWait), + LoopExiting => Ok(LoopExiting), + Suspended => Ok(Suspended), + Resumed => Ok(Resumed), + MemoryWarning => Ok(MemoryWarning), + } + } +} + +/// Describes the reason the event loop is resuming. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +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. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + 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. + 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`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + Poll, + + /// Sent once, immediately after `run` is called. Indicates that the loop was just initialized. + Init, +} + +/// Describes an event from a [`Window`]. +#[derive(Debug, Clone, PartialEq)] +pub enum WindowEvent { + /// The activation token was delivered back and now could be used. + #[cfg_attr(not(any(x11_platform, wayland_platform)), allow(rustdoc::broken_intra_doc_links))] + /// Delivered in response to [`request_activation_token`]. + /// + /// [`request_activation_token`]: crate::platform::startup_notify::WindowExtStartupNotify::request_activation_token + ActivationTokenDone { serial: AsyncRequestSerial, token: ActivationToken }, + + /// 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 + /// + /// - **iOS / Android / Web / Wayland:** Unsupported. + Moved(PhysicalPosition), + + /// The window has been requested to close. + CloseRequested, + + /// The window has been destroyed. + 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 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`. + KeyboardInput { + device_id: DeviceId, + event: KeyEvent, + + /// If `true`, the event was generated synthetically by winit + /// 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 X11 and + /// Windows*** + /// + /// Otherwise, this value is always `false`. + is_synthetic: bool, + }, + + /// The keyboard modifiers have changed. + ModifiersChanged(Modifiers), + + /// An event from an input method. + /// + /// **Note:** You have to explicitly enable this event using [`Window::set_ime_allowed`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Unsupported. + Ime(Ime), + + /// The cursor has moved on the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + 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, + }, + + /// The cursor has entered the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + CursorEntered { device_id: DeviceId }, + + /// The cursor has left the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + CursorLeft { device_id: DeviceId }, + + /// A mouse wheel movement or touchpad scroll occurred. + MouseWheel { device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase }, + + /// An mouse button press has been received. + MouseInput { device_id: DeviceId, state: ElementState, button: MouseButton }, + + /// Two-finger pinch gesture, often used for magnification. + /// + /// ## Platform-specific + /// + /// - Only available on **macOS** and **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + PinchGesture { + device_id: DeviceId, + /// Positive values indicate magnification (zooming in) and negative + /// values indicate shrinking (zooming out). + /// + /// This value may be NaN. + delta: f64, + phase: TouchPhase, + }, + + /// N-finger pan gesture + /// + /// ## Platform-specific + /// + /// - Only available on **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + PanGesture { + device_id: DeviceId, + /// Change in pixels of pan gesture from last update. + delta: PhysicalPosition, + phase: TouchPhase, + }, + + /// Double tap gesture. + /// + /// On a Mac, smart magnification is triggered by a double tap with two fingers + /// on the trackpad and is commonly used to zoom on a certain object + /// (e.g. a paragraph of a PDF) or (sort of like a toggle) to reset any zoom. + /// The gesture is also supported in Safari, Pages, etc. + /// + /// The event is general enough that its generating gesture is allowed to vary + /// across platforms. It could also be generated by another device. + /// + /// Unfortunately, neither [Windows](https://support.microsoft.com/en-us/windows/touch-gestures-for-windows-a9d28305-4818-a5df-4e2b-e5590f850741) + /// nor [Wayland](https://wayland.freedesktop.org/libinput/doc/latest/gestures.html) + /// support this gesture or any other gesture with the same effect. + /// + /// ## Platform-specific + /// + /// - Only available on **macOS 10.8** and later, and **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + DoubleTapGesture { device_id: DeviceId }, + + /// Two-finger rotation gesture. + /// + /// Positive delta values indicate rotation counterclockwise and + /// negative delta values indicate rotation clockwise. + /// + /// ## Platform-specific + /// + /// - Only available on **macOS** and **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + RotationGesture { + device_id: DeviceId, + /// change in rotation in degrees + delta: f32, + phase: TouchPhase, + }, + + /// 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 + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// - **macOS:** Unsupported. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + 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. + /// + /// To update the window size, use the provided [`InnerSizeWriter`] handle. By default, the + /// window is resized to the value suggested by the OS, but it can be changed to any value. + /// + /// For more information about DPI in general, see the [`dpi`] crate. + ScaleFactorChanged { + scale_factor: f64, + /// Handle to update inner size during scale changes. + /// + /// See [`InnerSizeWriter`] docs for more details. + inner_size_writer: InnerSizeWriter, + }, + + /// 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. + /// + /// This only reports a change if the window theme was not overridden by [`Window::set_theme`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / X11 / Wayland / Orbital:** Unsupported. + ThemeChanged(Theme), + + /// The window has been occluded (completely hidden from view). + /// + /// This is different to window visibility as it depends on whether the window is closed, + /// minimised, set invisible, or fully occluded by another window. + /// + /// ## Platform-specific + /// + /// ### iOS + /// + /// On iOS, the `Occluded(false)` event is emitted in response to an + /// [`applicationWillEnterForeground`] callback which means the application should start + /// preparing its data. The `Occluded(true)` event is emitted in response to an + /// [`applicationDidEnterBackground`] callback which means the application should free + /// resources (according to the [iOS application lifecycle]). + /// + /// [`applicationWillEnterForeground`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623076-applicationwillenterforeground + /// [`applicationDidEnterBackground`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// ### Others + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// - **Android / Wayland / Windows / Orbital:** Unsupported. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + Occluded(bool), + + /// Emitted 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`]. + /// + /// Winit will aggregate duplicate redraw requests into a single event, to + /// help avoid duplicating rendering work. + RedrawRequested, +} + +/// 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 { + /// Returns a dummy id, useful for unit testing. + /// + /// # Notes + /// + /// 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`. + pub const 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. +#[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`]. + MouseMotion { + /// (x, y) change in position in unspecified units. + /// + /// Different devices may use different units. + delta: (f64, f64), + }, + + /// Physical scroll event + MouseWheel { + delta: MouseScrollDelta, + }, + + /// Motion on some analog axis. This event will be reported for all arbitrary input devices + /// that winit supports on this platform, including mouse devices. If the device is a mouse + /// device then this will be reported alongside the MouseMotion event. + Motion { + axis: AxisId, + value: f64, + }, + + Button { + button: ButtonId, + state: ElementState, + }, + + Key(RawKeyEvent), +} + +/// 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, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RawKeyEvent { + pub physical_key: keyboard::PhysicalKey, + 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.) + /// + /// ## Caveats + /// + /// - Certain niche hardware will shuffle around physical key positions, e.g. a keyboard that + /// implements DVORAK in hardware (or firmware) + /// - Your application will likely have to handle keyboards which are missing keys that your + /// own keyboard has. + /// - Certain `KeyCode`s will move between a couple of different positions depending on what + /// layout the keyboard was manufactured to support. + /// + /// **Because of these caveats, it is important that you provide users with a way to configure + /// most (if not all) keybinds in your application.** + /// + /// ## `Fn` and `FnLock` + /// + /// `Fn` and `FnLock` key events are *exceedingly unlikely* to be emitted by Winit. These keys + /// are usually handled at the hardware or OS level, and aren't surfaced to applications. If + /// you somehow see this in the wild, we'd like to know :) + pub physical_key: keyboard::PhysicalKey, + + // Allowing `broken_intra_doc_links` for `logical_key`, because + // `key_without_modifiers` is not available on all platforms + #[cfg_attr( + not(any(windows_platform, macos_platform, x11_platform, wayland_platform)), + allow(rustdoc::broken_intra_doc_links) + )] + /// 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`]. + /// + /// If you use this field (or [`key_without_modifiers`] for that matter) for keyboard + /// shortcuts, **it is important that you provide users with a way to configure your + /// application's shortcuts so you don't render your application unusable for users with an + /// incompatible keyboard layout.** + /// + /// ## Platform-specific + /// - **Web:** Dead keys might be reported as the real key instead of `Dead` depending on the + /// browser/OS. + /// + /// [`key_without_modifiers`]: crate::platform::modifier_supplement::KeyEventExtModifierSupplement::key_without_modifiers + pub logical_key: keyboard::Key, + + /// 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::Named(NamedKey::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, + + /// Contains the location of this key on the keyboard. + /// + /// Certain keys on the keyboard may appear in more than once place. For example, the "Shift" + /// key appears on the left side of the QWERTY keyboard as well as the right side. However, + /// both keys have the same symbolic value. Another example of this phenomenon is the "1" + /// key, which appears both above the "Q" key and as the "Keypad 1" key. + /// + /// This field allows the user to differentiate between keys like this that have the same + /// symbolic value but different locations on the keyboard. + /// + /// See the [`KeyLocation`] type for more details. + /// + /// [`KeyLocation`]: crate::keyboard::KeyLocation + pub location: keyboard::KeyLocation, + + /// Whether the key is being pressed or released. + /// + /// See the [`ElementState`] type for more details. + pub state: ElementState, + + /// Whether or not this key is a key repeat event. + /// + /// On some systems, holding down a key for some period of time causes that key to be repeated + /// as though it were being pressed and released repeatedly. This field is `true` if and only + /// if this event is the result of one of those repeats. + /// + /// # Example + /// + /// In games, you often want to ignore repeated key events - this can be + /// done by ignoring events where this property is set. + /// + /// ``` + /// use winit::event::{ElementState, KeyEvent, WindowEvent}; + /// use winit::keyboard::{KeyCode, PhysicalKey}; + /// # let window_event = WindowEvent::RedrawRequested; // To make the example compile + /// match window_event { + /// WindowEvent::KeyboardInput { + /// event: + /// KeyEvent { + /// physical_key: PhysicalKey::Code(KeyCode::KeyW), + /// state: ElementState::Pressed, + /// repeat: false, + /// .. + /// }, + /// .. + /// } => { + /// // The physical key `W` was pressed, and it was not a repeat + /// }, + /// _ => {}, // Handle other events + /// } + /// ``` + pub repeat: bool, + + /// Platform-specific key event information. + /// + /// On Windows, Linux and macOS, this type contains the key without modifiers and the text with + /// all modifiers applied. + /// + /// On Android, iOS, Redox and Web, this type is a no-op. + pub(crate) platform_specific: platform_impl::KeyEventExtra, +} + +/// Describes keyboard modifiers event. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Modifiers { + pub(crate) state: ModifiersState, + + // NOTE: Currently pressed modifiers keys. + // + // The field providing a metadata, it shouldn't be used as a source of truth. + pub(crate) pressed_mods: ModifiersKeys, +} + +impl Modifiers { + /// The state of the modifiers. + pub fn state(&self) -> ModifiersState { + self.state + } + + /// The state of the left shift key. + pub fn lshift_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LSHIFT) + } + + /// The state of the right shift key. + pub fn rshift_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RSHIFT) + } + + /// The state of the left alt key. + pub fn lalt_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LALT) + } + + /// The state of the right alt key. + pub fn ralt_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RALT) + } + + /// The state of the left control key. + pub fn lcontrol_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LCONTROL) + } + + /// The state of the right control key. + pub fn rcontrol_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RCONTROL) + } + + /// The state of the left super key. + pub fn lsuper_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LSUPER) + } + + /// The state of the right super key. + pub fn rsuper_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RSUPER) + } + + fn mod_state(&self, modifier: ModifiersKeys) -> ModifiersKeyState { + if self.pressed_mods.contains(modifier) { + ModifiersKeyState::Pressed + } else { + ModifiersKeyState::Unknown + } + } +} + +impl From for Modifiers { + fn from(value: ModifiersState) -> Self { + Self { state: value, pressed_mods: Default::default() } + } +} + +/// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events. +/// +/// This is also called a "composition event". +/// +/// Most keypresses using a latin-like keyboard layout simply generate a +/// [`WindowEvent::KeyboardInput`]. However, one couldn't possibly have a key for every single +/// unicode character that the user might want to type +/// - so the solution operating systems employ is to allow the user to type these using _a sequence +/// of keypresses_ instead. +/// +/// A prominent example of this is accents - many keyboard layouts allow you to first click the +/// "accent key", and then the character you want to apply the accent to. In this case, some +/// platforms will generate the following event sequence: +/// +/// ```ignore +/// // Press "`" key +/// Ime::Preedit("`", Some((0, 0))) +/// // Press "E" key +/// Ime::Preedit("", None) // Synthetic event generated by winit to clear preedit. +/// Ime::Commit("é") +/// ``` +/// +/// Additionally, certain input devices are configured to display a candidate box that allow the +/// user to select the desired character interactively. (To properly position this box, you must use +/// [`Window::set_ime_cursor_area`].) +/// +/// An example of a keyboard layout which uses candidate boxes is pinyin. On a latin keyboard the +/// following event sequence could be obtained: +/// +/// ```ignore +/// // Press "A" key +/// Ime::Preedit("a", Some((1, 1))) +/// // Press "B" key +/// Ime::Preedit("a b", Some((3, 3))) +/// // Press left arrow key +/// Ime::Preedit("a b", Some((1, 1))) +/// // Press space key +/// Ime::Preedit("啊b", Some((3, 3))) +/// // Press space key +/// Ime::Preedit("", None) // Synthetic event generated by winit to clear preedit. +/// Ime::Commit("啊不") +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Ime { + /// Notifies when the IME was enabled. + /// + /// After getting this event you could receive [`Preedit`][Self::Preedit] and + /// [`Commit`][Self::Commit] events. You should also start performing IME related requests + /// like [`Window::set_ime_cursor_area`]. + Enabled, + + /// Notifies when a new composing text should be set at the cursor position. + /// + /// The value represents a pair of the preedit string and the cursor begin position and end + /// position. When it's `None`, the cursor should be hidden. When `String` is an empty string + /// this indicates that preedit was cleared. + /// + /// The cursor position is byte-wise indexed. + Preedit(String, Option<(usize, usize)>), + + /// Notifies when text should be inserted into the editor widget. + /// + /// Right before this event winit will send empty [`Self::Preedit`] event. + Commit(String), + + /// Notifies when the IME was disabled. + /// + /// After receiving this event you won't get any more [`Preedit`][Self::Preedit] or + /// [`Commit`][Self::Commit] events until the next [`Enabled`][Self::Enabled] event. You should + /// also stop issuing IME related requests like [`Window::set_ime_cursor_area`] and clear + /// pending preedit text. + Disabled, +} + +/// Describes touch-screen input state. +#[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 [`TouchPhase::Started`] event with an unique +/// identifier for the finger is generated. When the finger is lifted, an [`TouchPhase::Ended`] +/// event is generated with the same finger id. +/// +/// After a `Started` 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 `Ended` event. The user +/// should assume that a new `Started` event received with the same id has nothing +/// to do with the old finger and is a new finger. +/// +/// A [`TouchPhase::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. +/// +/// ## Platform-specific +/// +/// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. +/// - **macOS:** Unsupported. +/// +/// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border +/// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding +/// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform +#[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+, **Windows** 8+, **Web**, and **Android**. + /// - **Android**: This will never be [None]. If the device doesn't support pressure + /// sensitivity, force will either be 0.0 or 1.0. Also see the + /// [android documentation](https://developer.android.com/reference/android/view/MotionEvent#AXIS_PRESSURE). + pub force: Option, + /// Unique identifier of a finger. + pub id: u64, +} + +/// Describes the force of a touch event +#[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. + 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. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ElementState { + Pressed, + Released, +} + +impl ElementState { + /// True if `self == Pressed`. + pub fn is_pressed(self) -> bool { + self == ElementState::Pressed + } +} + +/// Describes a button of a mouse controller. +/// +/// ## Platform-specific +/// +/// **macOS:** `Back` and `Forward` might not work with all hardware. +/// **Orbital:** `Back` and `Forward` are unsupported due to orbital not supporting them. +#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum MouseButton { + Left, + Right, + Middle, + Back, + Forward, + Other(u16), +} + +/// Describes a difference in the mouse scroll wheel state. +#[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 that the content that is being scrolled should move + /// right and down (revealing more content left and up). + 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. + /// + /// Positive values indicate that the content being scrolled should + /// move right/down. + /// + /// For a 'natural scrolling' touch pad (that acts like a touch screen) + /// this means moving your fingers right and down should give positive values, + /// and move the content right and down (to reveal more things left and up). + PixelDelta(PhysicalPosition), +} + +/// Handle to synchronously change the size of the window from the +/// [`WindowEvent`]. +#[derive(Debug, Clone)] +pub struct InnerSizeWriter { + pub(crate) new_inner_size: Weak>>, +} + +impl InnerSizeWriter { + #[cfg(not(orbital_platform))] + pub(crate) fn new(new_inner_size: Weak>>) -> Self { + Self { new_inner_size } + } + + /// Try to request inner size which will be set synchronously on the window. + pub fn request_inner_size( + &mut self, + new_inner_size: PhysicalSize, + ) -> Result<(), ExternalError> { + if let Some(inner) = self.new_inner_size.upgrade() { + *inner.lock().unwrap() = new_inner_size; + Ok(()) + } else { + Err(ExternalError::Ignored) + } + } +} + +impl PartialEq for InnerSizeWriter { + fn eq(&self, other: &Self) -> bool { + self.new_inner_size.as_ptr() == other.new_inner_size.as_ptr() + } +} + +#[cfg(test)] +mod tests { + use crate::dpi::PhysicalPosition; + use crate::event; + use std::collections::{BTreeSet, HashSet}; + + macro_rules! foreach_event { + ($closure:expr) => {{ + #[allow(unused_mut)] + let mut x = $closure; + let did = event::DeviceId::dummy(); + + #[allow(deprecated)] + { + use crate::event::Event::*; + use crate::event::Ime::Enabled; + use crate::event::WindowEvent::*; + use crate::window::WindowId; + + // Mainline events. + let wid = WindowId::dummy(); + x(UserEvent(())); + x(NewEvents(event::StartCause::Init)); + x(AboutToWait); + x(LoopExiting); + x(Suspended); + x(Resumed); + + // Window events. + let with_window_event = |wev| x(WindowEvent { window_id: wid, event: wev }); + + with_window_event(CloseRequested); + with_window_event(Destroyed); + with_window_event(Focused(true)); + with_window_event(Moved((0, 0).into())); + with_window_event(Resized((0, 0).into())); + with_window_event(DroppedFile("x.txt".into())); + with_window_event(HoveredFile("x.txt".into())); + with_window_event(HoveredFileCancelled); + with_window_event(Ime(Enabled)); + with_window_event(CursorMoved { device_id: did, position: (0, 0).into() }); + with_window_event(ModifiersChanged(event::Modifiers::default())); + with_window_event(CursorEntered { device_id: did }); + with_window_event(CursorLeft { device_id: did }); + with_window_event(MouseWheel { + device_id: did, + delta: event::MouseScrollDelta::LineDelta(0.0, 0.0), + phase: event::TouchPhase::Started, + }); + with_window_event(MouseInput { + device_id: did, + state: event::ElementState::Pressed, + button: event::MouseButton::Other(0), + }); + with_window_event(PinchGesture { + device_id: did, + delta: 0.0, + phase: event::TouchPhase::Started, + }); + with_window_event(DoubleTapGesture { device_id: did }); + with_window_event(RotationGesture { + device_id: did, + delta: 0.0, + phase: event::TouchPhase::Started, + }); + with_window_event(PanGesture { + device_id: did, + delta: PhysicalPosition::::new(0.0, 0.0), + phase: event::TouchPhase::Started, + }); + with_window_event(TouchpadPressure { device_id: did, pressure: 0.0, stage: 0 }); + with_window_event(AxisMotion { device_id: did, axis: 0, value: 0.0 }); + with_window_event(Touch(event::Touch { + device_id: did, + phase: event::TouchPhase::Started, + location: (0.0, 0.0).into(), + id: 0, + force: Some(event::Force::Normalized(0.0)), + })); + with_window_event(ThemeChanged(crate::window::Theme::Light)); + with_window_event(Occluded(true)); + } + + #[allow(deprecated)] + { + use event::DeviceEvent::*; + + let with_device_event = + |dev_ev| x(event::Event::DeviceEvent { device_id: did, event: dev_ev }); + + with_device_event(Added); + with_device_event(Removed); + with_device_event(MouseMotion { delta: (0.0, 0.0).into() }); + with_device_event(MouseWheel { + delta: event::MouseScrollDelta::LineDelta(0.0, 0.0), + }); + with_device_event(Motion { axis: 0, value: 0.0 }); + with_device_event(Button { button: 0, state: event::ElementState::Pressed }); + } + }}; + } + + #[allow(clippy::redundant_clone)] + #[test] + fn test_event_clone() { + foreach_event!(|event: event::Event<()>| { + let event2 = event.clone(); + assert_eq!(event, event2); + }) + } + + #[test] + fn test_map_nonuser_event() { + foreach_event!(|event: event::Event<()>| { + let is_user = matches!(event, event::Event::UserEvent(())); + let event2 = event.map_nonuser_event::<()>(); + if is_user { + assert_eq!(event2, Err(event::Event::UserEvent(()))); + } else { + assert!(event2.is_ok()); + } + }) + } + + #[test] + fn test_force_normalize() { + let force = event::Force::Normalized(0.0); + assert_eq!(force.normalized(), 0.0); + + let force2 = + event::Force::Calibrated { force: 5.0, max_possible_force: 2.5, altitude_angle: None }; + assert_eq!(force2.normalized(), 2.0); + + let force3 = event::Force::Calibrated { + force: 5.0, + max_possible_force: 2.5, + altitude_angle: Some(std::f64::consts::PI / 2.0), + }; + assert_eq!(force3.normalized(), 2.0); + } + + #[allow(clippy::clone_on_copy)] + #[test] + fn ensure_attrs_do_not_panic() { + foreach_event!(|event: event::Event<()>| { + let _ = format!("{event:?}"); + }); + let _ = event::StartCause::Init.clone(); + + let did = crate::event::DeviceId::dummy().clone(); + HashSet::new().insert(did); + let mut set = [did, did, did]; + set.sort_unstable(); + let mut set2 = BTreeSet::new(); + set2.insert(did); + set2.insert(did); + + HashSet::new().insert(event::TouchPhase::Started.clone()); + HashSet::new().insert(event::MouseButton::Left.clone()); + HashSet::new().insert(event::Ime::Enabled); + + let _ = event::Touch { + device_id: did, + phase: event::TouchPhase::Started, + location: (0.0, 0.0).into(), + id: 0, + force: Some(event::Force::Normalized(0.0)), + } + .clone(); + let _ = + event::Force::Calibrated { force: 0.0, max_possible_force: 0.0, altitude_angle: None } + .clone(); + } +} diff --git a/third_party/winit-0.30.13/src/event_loop.rs b/third_party/winit-0.30.13/src/event_loop.rs new file mode 100644 index 0000000..233374b --- /dev/null +++ b/third_party/winit-0.30.13/src/event_loop.rs @@ -0,0 +1,651 @@ +//! The [`EventLoop`] struct and assorted supporting types, including +//! [`ControlFlow`]. +//! +//! If you want to send custom events to the event loop, use +//! [`EventLoop::create_proxy`] to acquire an [`EventLoopProxy`] and call its +//! [`send_event`][EventLoopProxy::send_event] method. +//! +//! See the root-level documentation for information on how to create and use an event loop to +//! handle events. +use std::marker::PhantomData; +#[cfg(any(x11_platform, wayland_platform))] +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::{error, fmt}; + +#[cfg(not(web_platform))] +use std::time::{Duration, Instant}; +#[cfg(web_platform)] +use web_time::{Duration, Instant}; + +use crate::application::ApplicationHandler; +use crate::error::{EventLoopError, OsError}; +use crate::event::Event; +use crate::monitor::MonitorHandle; +use crate::platform_impl; +use crate::window::{CustomCursor, CustomCursorSource, Theme, Window, WindowAttributes}; + +/// 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. For example on Linux creating +/// an event loop opens a connection to the X or Wayland server. +/// +/// To wake up an `EventLoop` from a another thread, see the [`EventLoopProxy`] docs. +/// +/// Note that this 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 _can_ be sent to an other thread, and the +/// [`EventLoopProxy`] allows you to wake up an `EventLoop` from another thread. +/// +/// [`Window`]: crate::window::Window +pub struct EventLoop { + pub(crate) event_loop: platform_impl::EventLoop, + pub(crate) _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 Winit executes +/// your callback. +pub struct ActiveEventLoop { + pub(crate) p: platform_impl::ActiveEventLoop, + pub(crate) _marker: PhantomData<*mut ()>, // Not Send nor Sync +} + +/// 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. +/// +/// This can be created using [`EventLoop::new`] or [`EventLoop::with_user_event`]. +#[derive(Default)] +pub struct EventLoopBuilder { + pub(crate) platform_specific: platform_impl::PlatformSpecificEventLoopAttributes, + _p: PhantomData, +} + +static EVENT_LOOP_CREATED: AtomicBool = AtomicBool::new(false); + +impl EventLoopBuilder<()> { + /// Start building a new event loop. + #[inline] + #[deprecated = "use `EventLoop::builder` instead"] + pub fn new() -> Self { + EventLoop::builder() + } +} + +impl EventLoopBuilder { + /// Builds a new event loop. + /// + /// ***For cross-platform compatibility, the [`EventLoop`] must be created on the main thread, + /// and only once per application.*** + /// + /// Calling this function will result in display backend initialisation. + /// + /// ## Panics + /// + /// Attempting to create the event loop off the main 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. + /// + /// ## Platform-specific + /// + /// - **Wayland/X11:** to prevent running under `Wayland` or `X11` unset `WAYLAND_DISPLAY` or + /// `DISPLAY` respectively when building the event loop. + /// - **Android:** must be configured with an `AndroidApp` from `android_main()` by calling + /// [`.with_android_app(app)`] before calling `.build()`, otherwise it'll panic. + /// + /// [`platform`]: crate::platform + #[cfg_attr( + android_platform, + doc = "[`.with_android_app(app)`]: \ + crate::platform::android::EventLoopBuilderExtAndroid::with_android_app" + )] + #[cfg_attr( + not(android_platform), + doc = "[`.with_android_app(app)`]: #only-available-on-android" + )] + #[inline] + pub fn build(&mut self) -> Result, EventLoopError> { + let _span = tracing::debug_span!("winit::EventLoopBuilder::build").entered(); + + if EVENT_LOOP_CREATED.swap(true, Ordering::Relaxed) { + return Err(EventLoopError::RecreationAttempt); + } + + // Certain platforms accept a mutable reference in their API. + #[allow(clippy::unnecessary_mut_passed)] + Ok(EventLoop { + event_loop: platform_impl::EventLoop::new(&mut self.platform_specific)?, + _marker: PhantomData, + }) + } + + #[cfg(web_platform)] + pub(crate) fn allow_event_loop_recreation() { + EVENT_LOOP_CREATED.store(false, Ordering::Relaxed); + } +} + +impl fmt::Debug for EventLoop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("EventLoop { .. }") + } +} + +impl fmt::Debug for ActiveEventLoop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("ActiveEventLoop { .. }") + } +} + +/// Set through [`ActiveEventLoop::set_control_flow()`]. +/// +/// Indicates the desired behavior of the event loop after [`Event::AboutToWait`] is emitted. +/// +/// Defaults to [`Wait`]. +/// +/// [`Wait`]: Self::Wait +#[derive(Copy, Clone, Debug, Default, 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. + #[default] + Wait, + + /// When the current loop iteration finishes, suspend the thread until either another event + /// arrives or the given time is reached. + /// + /// Useful for implementing efficient timers. Applications which want to render at the + /// display's native refresh rate should instead use [`Poll`] and the VSync functionality + /// of a graphics API to reduce odds of missed frames. + /// + /// [`Poll`]: Self::Poll + WaitUntil(Instant), +} + +impl ControlFlow { + /// Creates a [`ControlFlow`] that waits until a timeout has expired. + /// + /// In most cases, this is set to [`WaitUntil`]. However, if the timeout overflows, it is + /// instead set to [`Wait`]. + /// + /// [`WaitUntil`]: Self::WaitUntil + /// [`Wait`]: Self::Wait + pub fn wait_duration(timeout: Duration) -> Self { + match Instant::now().checked_add(timeout) { + Some(instant) => Self::WaitUntil(instant), + None => Self::Wait, + } + } +} + +impl EventLoop<()> { + /// Create the event loop. + /// + /// This is an alias of `EventLoop::builder().build()`. + #[inline] + pub fn new() -> Result, EventLoopError> { + Self::builder().build() + } + + /// Start building a new event loop. + /// + /// This returns an [`EventLoopBuilder`], to allow configuring the event loop before creation. + /// + /// To get the actual event loop, call [`build`][EventLoopBuilder::build] on that. + #[inline] + pub fn builder() -> EventLoopBuilder<()> { + Self::with_user_event() + } +} + +impl EventLoop { + /// Start building a new event loop, with the given type as the user event + /// type. + pub fn with_user_event() -> EventLoopBuilder { + EventLoopBuilder { platform_specific: Default::default(), _p: PhantomData } + } + + /// See [`run_app`]. + /// + /// [`run_app`]: Self::run_app + #[inline] + #[deprecated = "use `EventLoop::run_app` instead"] + #[cfg(not(all(web_platform, target_feature = "exception-handling")))] + pub fn run(self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &ActiveEventLoop), + { + let _span = tracing::debug_span!("winit::EventLoop::run").entered(); + + self.event_loop.run(event_handler) + } + + /// Run the application with the event loop on the calling thread. + /// + /// See the [`set_control_flow()`] docs on how to change the event loop's behavior. + /// + /// ## Platform-specific + /// + /// - **iOS:** Will never return to the caller and so values not passed to this function will + /// *not* be dropped before the process exits. + /// - **Web:** Will _act_ as if it never returns to the caller by throwing a Javascript + /// exception (that Rust doesn't see) that will also mean that the rest of the function is + /// never executed and any values not passed to this function will *not* be dropped. + /// + /// Web applications are recommended to use + #[cfg_attr( + web_platform, + doc = "[`EventLoopExtWebSys::spawn_app()`][crate::platform::web::EventLoopExtWebSys::spawn_app()]" + )] + #[cfg_attr(not(web_platform), doc = "`EventLoopExtWebSys::spawn()`")] + /// [^1] instead of [`run_app()`] to avoid the need + /// for the Javascript exception trick, and to make it clearer that the event loop runs + /// asynchronously (via the browser's own, internal, event loop) and doesn't block the + /// current thread of execution like it does on other platforms. + /// + /// This function won't be available with `target_feature = "exception-handling"`. + /// + /// [`set_control_flow()`]: ActiveEventLoop::set_control_flow() + /// [`run_app()`]: Self::run_app() + /// [^1]: `EventLoopExtWebSys::spawn_app()` is only available on Web. + #[inline] + #[cfg(not(all(web_platform, target_feature = "exception-handling")))] + pub fn run_app>(self, app: &mut A) -> Result<(), EventLoopError> { + self.event_loop.run(|event, event_loop| dispatch_event_for_app(app, event_loop, event)) + } + + /// Creates an [`EventLoopProxy`] that can be used to dispatch user events + /// to the main event loop, possibly from another thread. + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { event_loop_proxy: self.event_loop.create_proxy() } + } + + /// Gets a persistent reference to the underlying platform display. + /// + /// See the [`OwnedDisplayHandle`] type for more information. + pub fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle { platform: self.event_loop.window_target().p.owned_display_handle() } + } + + /// Change if or when [`DeviceEvent`]s are captured. + /// + /// See [`ActiveEventLoop::listen_device_events`] for details. + /// + /// [`DeviceEvent`]: crate::event::DeviceEvent + pub fn listen_device_events(&self, allowed: DeviceEvents) { + let _span = tracing::debug_span!( + "winit::EventLoop::listen_device_events", + allowed = ?allowed + ) + .entered(); + + self.event_loop.window_target().p.listen_device_events(allowed); + } + + /// Sets the [`ControlFlow`]. + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.event_loop.window_target().p.set_control_flow(control_flow) + } + + /// Create a window. + /// + /// Creating window without event loop running often leads to improper window creation; + /// use [`ActiveEventLoop::create_window`] instead. + #[deprecated = "use `ActiveEventLoop::create_window` instead"] + #[inline] + pub fn create_window(&self, window_attributes: WindowAttributes) -> Result { + let _span = tracing::debug_span!( + "winit::EventLoop::create_window", + window_attributes = ?window_attributes + ) + .entered(); + + let window = + platform_impl::Window::new(&self.event_loop.window_target().p, window_attributes)?; + Ok(Window { window }) + } + + /// Create custom cursor. + pub fn create_custom_cursor(&self, custom_cursor: CustomCursorSource) -> CustomCursor { + self.event_loop.window_target().p.create_custom_cursor(custom_cursor) + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for EventLoop { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + rwh_06::HasDisplayHandle::display_handle(self.event_loop.window_target()) + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for EventLoop { + /// Returns a [`rwh_05::RawDisplayHandle`] for the event loop. + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + rwh_05::HasRawDisplayHandle::raw_display_handle(self.event_loop.window_target()) + } +} + +#[cfg(any(x11_platform, wayland_platform))] +impl AsFd for EventLoop { + /// Get the underlying [EventLoop]'s `fd` which you can register + /// into other event loop, like [`calloop`] or [`mio`]. When doing so, the + /// loop must be polled with the [`pump_app_events`] API. + /// + /// [`calloop`]: https://crates.io/crates/calloop + /// [`mio`]: https://crates.io/crates/mio + /// [`pump_app_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_app_events + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +#[cfg(any(x11_platform, wayland_platform))] +impl AsRawFd for EventLoop { + /// Get the underlying [EventLoop]'s raw `fd` which you can register + /// into other event loop, like [`calloop`] or [`mio`]. When doing so, the + /// loop must be polled with the [`pump_app_events`] API. + /// + /// [`calloop`]: https://crates.io/crates/calloop + /// [`mio`]: https://crates.io/crates/mio + /// [`pump_app_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_app_events + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() + } +} + +impl ActiveEventLoop { + /// Create the window. + /// + /// Possible causes of error include denied permission, incompatible system, and lack of memory. + /// + /// ## Platform-specific + /// + /// - **Web:** The window is created but not inserted into the web page automatically. Please + /// see the web platform module for more information. + #[inline] + pub fn create_window(&self, window_attributes: WindowAttributes) -> Result { + let _span = tracing::debug_span!( + "winit::ActiveEventLoop::create_window", + window_attributes = ?window_attributes + ) + .entered(); + + let window = platform_impl::Window::new(&self.p, window_attributes)?; + Ok(Window { window }) + } + + /// Create custom cursor. + pub fn create_custom_cursor(&self, custom_cursor: CustomCursorSource) -> CustomCursor { + let _span = tracing::debug_span!("winit::ActiveEventLoop::create_custom_cursor",).entered(); + + self.p.create_custom_cursor(custom_cursor) + } + + /// Returns the list of all the monitors available on the system. + #[inline] + pub fn available_monitors(&self) -> impl Iterator { + let _span = tracing::debug_span!("winit::ActiveEventLoop::available_monitors",).entered(); + + #[allow(clippy::useless_conversion)] // false positive on some platforms + 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. + /// + /// ## Platform-specific + /// + /// **Wayland / Web:** Always returns `None`. + #[inline] + pub fn primary_monitor(&self) -> Option { + let _span = tracing::debug_span!("winit::ActiveEventLoop::primary_monitor",).entered(); + + self.p.primary_monitor().map(|inner| MonitorHandle { inner }) + } + + /// Change if or when [`DeviceEvent`]s are captured. + /// + /// Since the [`DeviceEvent`] capture can lead to high CPU usage for unfocused windows, winit + /// will ignore them by default for unfocused windows on Linux/BSD. This method allows changing + /// this at runtime to explicitly capture them again. + /// + /// ## Platform-specific + /// + /// - **Wayland / macOS / iOS / Android / Orbital:** Unsupported. + /// + /// [`DeviceEvent`]: crate::event::DeviceEvent + pub fn listen_device_events(&self, allowed: DeviceEvents) { + let _span = tracing::debug_span!( + "winit::ActiveEventLoop::listen_device_events", + allowed = ?allowed + ) + .entered(); + + self.p.listen_device_events(allowed); + } + + /// Returns the current system theme. + /// + /// Returns `None` if it cannot be determined on the current platform. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Wayland / x11 / Orbital:** Unsupported. + pub fn system_theme(&self) -> Option { + self.p.system_theme() + } + + /// Sets the [`ControlFlow`]. + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.p.set_control_flow(control_flow) + } + + /// Gets the current [`ControlFlow`]. + pub fn control_flow(&self) -> ControlFlow { + self.p.control_flow() + } + + /// This exits the event loop. + /// + /// See [`LoopExiting`][Event::LoopExiting]. + pub fn exit(&self) { + let _span = tracing::debug_span!("winit::ActiveEventLoop::exit",).entered(); + + self.p.exit() + } + + /// Returns if the [`EventLoop`] is about to stop. + /// + /// See [`exit()`][Self::exit]. + pub fn exiting(&self) -> bool { + self.p.exiting() + } + + /// Gets a persistent reference to the underlying platform display. + /// + /// See the [`OwnedDisplayHandle`] type for more information. + pub fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle { platform: self.p.owned_display_handle() } + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for ActiveEventLoop { + 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) }) + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for ActiveEventLoop { + /// Returns a [`rwh_05::RawDisplayHandle`] for the event loop. + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + self.p.raw_display_handle_rwh_05() + } +} + +/// A proxy for the underlying display handle. +/// +/// The purpose of this type is to provide a cheaply cloneable handle to the underlying +/// display handle. This is often used by graphics APIs to connect to the underlying APIs. +/// It is difficult to keep a handle to the [`EventLoop`] type or the [`ActiveEventLoop`] +/// type. In contrast, this type involves no lifetimes and can be persisted for as long as +/// needed. +/// +/// For all platforms, this is one of the following: +/// +/// - A zero-sized type that is likely optimized out. +/// - A reference-counted pointer to the underlying type. +#[derive(Clone)] +pub struct OwnedDisplayHandle { + #[cfg_attr(not(any(feature = "rwh_05", feature = "rwh_06")), allow(dead_code))] + platform: platform_impl::OwnedDisplayHandle, +} + +impl fmt::Debug for OwnedDisplayHandle { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OwnedDisplayHandle").finish_non_exhaustive() + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for OwnedDisplayHandle { + #[inline] + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.platform.raw_display_handle_rwh_06()?; + + // SAFETY: The underlying display handle should be safe. + let handle = unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }; + + Ok(handle) + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for OwnedDisplayHandle { + #[inline] + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + self.platform.raw_display_handle_rwh_05() + } +} + +/// 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. + /// + /// [`UserEvent(event)`]: Event::UserEvent + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + let _span = tracing::debug_span!("winit::EventLoopProxy::send_event",).entered(); + + 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 [`EventLoopProxy::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 {} + +/// Control when device events are captured. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +pub enum DeviceEvents { + /// Report device events regardless of window focus. + Always, + /// Only capture device events while the window is focused. + #[default] + WhenFocused, + /// Never capture device events. + Never, +} + +/// A unique identifier of the winit's async request. +/// +/// This could be used to identify the async request once it's done +/// and a specific action must be taken. +/// +/// One of the handling scenarios could be to maintain a working list +/// containing [`AsyncRequestSerial`] and some closure associated with it. +/// Then once event is arriving the working list is being traversed and a job +/// executed and removed from the list. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AsyncRequestSerial { + serial: usize, +} + +impl AsyncRequestSerial { + // TODO(kchibisov): Remove `cfg` when the clipboard will be added. + #[allow(dead_code)] + pub(crate) fn get() -> Self { + static CURRENT_SERIAL: AtomicUsize = AtomicUsize::new(0); + // NOTE: We rely on wrap around here, while the user may just request + // in the loop usize::MAX times that's issue is considered on them. + let serial = CURRENT_SERIAL.fetch_add(1, Ordering::Relaxed); + Self { serial } + } +} + +/// Shim for various run APIs. +#[inline(always)] +pub(crate) fn dispatch_event_for_app>( + app: &mut A, + event_loop: &ActiveEventLoop, + event: Event, +) { + match event { + Event::NewEvents(cause) => app.new_events(event_loop, cause), + Event::WindowEvent { window_id, event } => app.window_event(event_loop, window_id, event), + Event::DeviceEvent { device_id, event } => app.device_event(event_loop, device_id, event), + Event::UserEvent(event) => app.user_event(event_loop, event), + Event::Suspended => app.suspended(event_loop), + Event::Resumed => app.resumed(event_loop), + Event::AboutToWait => app.about_to_wait(event_loop), + Event::LoopExiting => app.exiting(event_loop), + Event::MemoryWarning => app.memory_warning(event_loop), + } +} diff --git a/third_party/winit-0.30.13/src/icon.rs b/third_party/winit-0.30.13/src/icon.rs new file mode 100644 index 0000000..b013d2f --- /dev/null +++ b/third_party/winit-0.30.13/src/icon.rs @@ -0,0 +1,117 @@ +use crate::platform_impl::PlatformIcon; +use std::error::Error; +use std::{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::(); + +#[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. + 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. + DimensionsVsPixelCount { width: u32, height: u32, width_x_height: usize, pixel_count: usize }, + /// 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::OsError(e) => write!(f, "OS error when instantiating the icon: {e:?}"), + } + } +} + +impl Error for BadIcon {} + +#[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 { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + if rgba.len() % PIXEL_SIZE != 0 { + return Err(BadIcon::ByteCountNotDivisibleBy4 { byte_count: rgba.len() }); + } + let pixel_count = rgba.len() / PIXEL_SIZE; + if pixel_count != (width * height) as usize { + Err(BadIcon::DimensionsVsPixelCount { + width, + height, + width_x_height: (width * height) as usize, + 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 { + let _span = tracing::debug_span!("winit::Icon::from_rgba", width, height).entered(); + + Ok(Icon { inner: PlatformIcon::from_rgba(rgba, width, height)? }) + } +} diff --git a/third_party/winit-0.30.13/src/keyboard.rs b/third_party/winit-0.30.13/src/keyboard.rs new file mode 100644 index 0000000..7b406f0 --- /dev/null +++ b/third_party/winit-0.30.13/src/keyboard.rs @@ -0,0 +1,1804 @@ +//! 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. + +// --------- BEGINNING 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 -------------------------------------------------------------------- + +// --------- BEGINNING OF W3C SHORT NOTICE --------------------------------------------------------- +// +// winit: https://github.com/rust-windowing/winit +// +// 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 bitflags::bitflags; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +pub use smol_str::SmolStr; + +/// Contains the platform-native physical key identifier +/// +/// The exact values vary from platform to platform (which is part of why this is a per-platform +/// enum), but the values are primarily tied to the key's physical location on the keyboard. +/// +/// This enum is primarily used to store raw keycodes when Winit doesn't map a given native +/// physical key identifier to a meaningful [`KeyCode`] variant. In the presence of identifiers we +/// haven't mapped for you yet, this lets you use use [`KeyCode`] to: +/// +/// - Correctly match key press and release events. +/// - On non-web platforms, support assigning keybinds to virtually any key through a UI. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum NativeKeyCode { + Unidentified, + /// An Android "scancode". + Android(u32), + /// A macOS "scancode". + MacOS(u16), + /// A Windows "scancode". + Windows(u16), + /// An XKB "keycode". + Xkb(u32), +} + +impl std::fmt::Debug for NativeKeyCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use NativeKeyCode::{Android, MacOS, Unidentified, Windows, Xkb}; + let mut debug_tuple; + match self { + Unidentified => { + debug_tuple = f.debug_tuple("Unidentified"); + }, + Android(code) => { + debug_tuple = f.debug_tuple("Android"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + MacOS(code) => { + debug_tuple = f.debug_tuple("MacOS"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + Windows(code) => { + debug_tuple = f.debug_tuple("Windows"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + Xkb(code) => { + debug_tuple = f.debug_tuple("Xkb"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + } + debug_tuple.finish() + } +} + +/// Contains the platform-native logical key identifier +/// +/// Exactly what that means differs from platform to platform, but the values are to some degree +/// tied to the currently active keyboard layout. The same key on the same keyboard may also report +/// different values on different platforms, which is one of the reasons this is a per-platform +/// enum. +/// +/// This enum is primarily used to store raw keysym when Winit doesn't map a given native logical +/// key identifier to a meaningful [`Key`] variant. This lets you use [`Key`], and let the user +/// define keybinds which work in the presence of identifiers we haven't mapped for you yet. +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum NativeKey { + Unidentified, + /// An Android "keycode", which is similar to a "virtual-key code" on Windows. + Android(u32), + /// A macOS "scancode". There does not appear to be any direct analogue to either keysyms or + /// "virtual-key" codes in macOS, so we report the scancode instead. + MacOS(u16), + /// A Windows "virtual-key code". + Windows(u16), + /// An XKB "keysym". + Xkb(u32), + /// A "key value string". + Web(SmolStr), +} + +impl std::fmt::Debug for NativeKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use NativeKey::{Android, MacOS, Unidentified, Web, Windows, Xkb}; + let mut debug_tuple; + match self { + Unidentified => { + debug_tuple = f.debug_tuple("Unidentified"); + }, + Android(code) => { + debug_tuple = f.debug_tuple("Android"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + MacOS(code) => { + debug_tuple = f.debug_tuple("MacOS"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + Windows(code) => { + debug_tuple = f.debug_tuple("Windows"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + Xkb(code) => { + debug_tuple = f.debug_tuple("Xkb"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + Web(code) => { + debug_tuple = f.debug_tuple("Web"); + debug_tuple.field(code); + }, + } + debug_tuple.finish() + } +} + +impl From for NativeKey { + #[inline] + fn from(code: NativeKeyCode) -> Self { + match code { + NativeKeyCode::Unidentified => NativeKey::Unidentified, + NativeKeyCode::Android(x) => NativeKey::Android(x), + NativeKeyCode::MacOS(x) => NativeKey::MacOS(x), + NativeKeyCode::Windows(x) => NativeKey::Windows(x), + NativeKeyCode::Xkb(x) => NativeKey::Xkb(x), + } + } +} + +impl PartialEq for NativeKeyCode { + #[allow(clippy::cmp_owned)] // uses less code than direct match; target is stack allocated + #[inline] + fn eq(&self, rhs: &NativeKey) -> bool { + NativeKey::from(*self) == *rhs + } +} + +impl PartialEq for NativeKey { + #[inline] + fn eq(&self, rhs: &NativeKeyCode) -> bool { + rhs == self + } +} + +/// Represents the location of a physical key. +/// +/// This type is a superset of [`KeyCode`], including an [`Unidentified`][Self::Unidentified] +/// variant. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum PhysicalKey { + /// A known key code + Code(KeyCode), + /// This variant is used when the key cannot be translated to a [`KeyCode`] + /// + /// The native keycode is provided (if available) so you're able to more reliably match + /// key-press and key-release events by hashing the [`PhysicalKey`]. It is also possible to use + /// this for keybinds for non-standard keys, but such keybinds are tied to a given platform. + Unidentified(NativeKeyCode), +} + +impl From for PhysicalKey { + #[inline] + fn from(code: KeyCode) -> Self { + PhysicalKey::Code(code) + } +} + +impl From for PhysicalKey { + #[inline] + fn from(code: NativeKeyCode) -> Self { + PhysicalKey::Unidentified(code) + } +} + +impl PartialEq for PhysicalKey { + #[inline] + fn eq(&self, rhs: &KeyCode) -> bool { + match self { + PhysicalKey::Code(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq for KeyCode { + #[inline] + fn eq(&self, rhs: &PhysicalKey) -> bool { + rhs == self + } +} + +impl PartialEq for PhysicalKey { + #[inline] + fn eq(&self, rhs: &NativeKeyCode) -> bool { + match self { + PhysicalKey::Unidentified(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq for NativeKeyCode { + #[inline] + fn eq(&self, rhs: &PhysicalKey) -> bool { + rhs == self + } +} + +/// Code representing the location 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. +/// +/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum KeyCode { + /// ` 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, + /// . 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, + // Legacy modifier key. Also called "Super" in certain places. + Meta, + // Legacy modifier key. + 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, +} + +/// A [`Key::Named`] value +/// +/// 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 +/// specification. +/// +/// [`KeyboardEvent.key`]: https://w3c.github.io/uievents-key/ +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum NamedKey { + /// 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, + // Legacy modifier key. Also called "Super" in certain places. + Meta, + // Legacy modifier key. + 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, + /// Initiate 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 Recorder. (`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, +} + +/// Key represents the meaning of a keypress. +/// +/// This is a superset of the UI Events Specification's [`KeyboardEvent.key`] with +/// additions: +/// - All simple variants are wrapped under the `Named` variant +/// - The `Unidentified` variant here, can still identify 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/ +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Key { + /// A simple (unparameterised) action + Named(NamedKey), + + /// 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(Str), + + /// This variant is used when the key cannot be translated to any other variant. + /// + /// The native key is provided (if available) in order to allow the user to specify keybindings + /// for keys which are not defined by this API, mainly through some sort of UI. + Unidentified(NativeKey), + + /// Contains the text representation of the dead-key when available. + /// + /// ## Platform-specific + /// - **Web:** Always contains `None` + Dead(Option), +} + +impl From for Key { + #[inline] + fn from(action: NamedKey) -> Self { + Key::Named(action) + } +} + +impl From for Key { + #[inline] + fn from(code: NativeKey) -> Self { + Key::Unidentified(code) + } +} + +impl PartialEq for Key { + #[inline] + fn eq(&self, rhs: &NamedKey) -> bool { + match self { + Key::Named(ref a) => a == rhs, + _ => false, + } + } +} + +impl> PartialEq for Key { + #[inline] + fn eq(&self, rhs: &str) -> bool { + match self { + Key::Character(ref s) => s == rhs, + _ => false, + } + } +} + +impl> PartialEq<&str> for Key { + #[inline] + fn eq(&self, rhs: &&str) -> bool { + self == *rhs + } +} + +impl PartialEq for Key { + #[inline] + fn eq(&self, rhs: &NativeKey) -> bool { + match self { + Key::Unidentified(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq> for NativeKey { + #[inline] + fn eq(&self, rhs: &Key) -> bool { + rhs == self + } +} + +impl Key { + /// Convert `Key::Character(SmolStr)` to `Key::Character(&str)` so you can more easily match on + /// `Key`. All other variants remain unchanged. + pub fn as_ref(&self) -> Key<&str> { + match self { + Key::Named(a) => Key::Named(*a), + Key::Character(ch) => Key::Character(ch.as_str()), + Key::Dead(d) => Key::Dead(*d), + Key::Unidentified(u) => Key::Unidentified(u.clone()), + } + } +} + +impl NamedKey { + /// Convert an action to its approximate textual equivalent. + /// + /// # Examples + /// + /// ``` + /// use winit::keyboard::NamedKey; + /// + /// assert_eq!(NamedKey::Enter.to_text(), Some("\r")); + /// assert_eq!(NamedKey::F20.to_text(), None); + /// ``` + pub fn to_text(&self) -> Option<&str> { + match self { + NamedKey::Enter => Some("\r"), + NamedKey::Backspace => Some("\x08"), + NamedKey::Tab => Some("\t"), + NamedKey::Space => Some(" "), + NamedKey::Escape => Some("\x1b"), + _ => None, + } + } +} + +impl Key { + /// Convert a key to its approximate textual equivalent. + /// + /// # Examples + /// + /// ``` + /// use winit::keyboard::{Key, NamedKey}; + /// + /// assert_eq!(Key::Character("a".into()).to_text(), Some("a")); + /// assert_eq!(Key::Named(NamedKey::Enter).to_text(), Some("\r")); + /// assert_eq!(Key::Named(NamedKey::F20).to_text(), None); + /// ``` + pub fn to_text(&self) -> Option<&str> { + match self { + Key::Named(action) => action.to_text(), + Key::Character(ch) => Some(ch.as_str()), + _ => None, + } + } +} + +/// The location of the key on the keyboard. +/// +/// Certain physical keys on the keyboard can have the same value, but are in different locations. +/// For instance, the Shift key can be on the left or right side of the keyboard, or the number +/// keys can be above the letters or on the numpad. This enum allows the user to differentiate +/// them. +/// +/// See the documentation for the [`location`] field on the [`KeyEvent`] struct for more +/// information. +/// +/// [`location`]: ../event/struct.KeyEvent.html#structfield.location +/// [`KeyEvent`]: crate::event::KeyEvent +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum KeyLocation { + /// The key is in its "normal" location on the keyboard. + /// + /// For instance, the "1" key above the "Q" key on a QWERTY keyboard will use this location. + /// This invariant is also returned when the location of the key cannot be identified. + /// + /// ![Standard 1 key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_standard_1_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Standard, + + /// The key is on the left side of the keyboard. + /// + /// For instance, the left Shift key below the Caps Lock key on a QWERTY keyboard will use this + /// location. + /// + /// ![Left Shift key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_left_shift_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Left, + + /// The key is on the right side of the keyboard. + /// + /// For instance, the right Shift key below the Enter key on a QWERTY keyboard will use this + /// location. + /// + /// ![Right Shift key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_right_shift_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Right, + + /// The key is on the numpad. + /// + /// For instance, the "1" key on the numpad will use this location. + /// + /// ![Numpad 1 key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_numpad_1_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Numpad, +} + +bitflags! { + /// Represents the current state of the keyboard modifiers + /// + /// Each flag represents a modifier and is set if this modifier is active. + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ModifiersState: u32 { + /// The "shift" key. + const SHIFT = 0b100; + /// The "control" key. + const CONTROL = 0b100 << 3; + /// The "alt" key. + const ALT = 0b100 << 6; + /// This is the "windows" key on PC and "command" key on Mac. + const SUPER = 0b100 << 9; + } +} + +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) + } +} + +/// The state of the particular modifiers key. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModifiersKeyState { + /// The particular key is pressed. + Pressed, + /// The state of the key is unknown. + #[default] + Unknown, +} + +// NOTE: the exact modifier key is not used to represent modifiers state in the +// first place due to a fact that modifiers state could be changed without any +// key being pressed and on some platforms like Wayland/X11 which key resulted +// in modifiers change is hidden, also, not that it really matters. +// +// The reason this API is even exposed is mostly to provide a way for users +// to treat modifiers differently based on their position, which is required +// on macOS due to their AltGr/Option situation. +bitflags! { + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub(crate) struct ModifiersKeys: u8 { + const LSHIFT = 0b0000_0001; + const RSHIFT = 0b0000_0010; + const LCONTROL = 0b0000_0100; + const RCONTROL = 0b0000_1000; + const LALT = 0b0001_0000; + const RALT = 0b0010_0000; + const LSUPER = 0b0100_0000; + const RSUPER = 0b1000_0000; + } +} + +#[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) + } + } +} diff --git a/third_party/winit-0.30.13/src/lib.rs b/third_party/winit-0.30.13/src/lib.rs new file mode 100644 index 0000000..41a55b2 --- /dev/null +++ b/third_party/winit-0.30.13/src/lib.rs @@ -0,0 +1,217 @@ +//! Winit is a cross-platform window creation and event loop management library. +//! +//! # Building windows +//! +//! Before you can create a [`Window`], you first need to build an [`EventLoop`]. This is done with +//! the [`EventLoop::new()`] function. +//! +//! ```no_run +//! use winit::event_loop::EventLoop; +//! +//! # // Intentionally use `fn main` for clarity +//! fn main() { +//! let event_loop = EventLoop::new().unwrap(); +//! // ... +//! } +//! ``` +//! +//! Then you create a [`Window`] with [`create_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 [`Event::UserEvent`]s, if +//! desired. +//! +//! You can retrieve events by calling [`EventLoop::run_app()`]. This function will +//! dispatch events for every [`Window`] that was created with that particular [`EventLoop`], and +//! will run until [`exit()`] is used, at which point [`Event::LoopExiting`]. +//! +//! Winit 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 +#![cfg_attr( + any(windows_platform, macos_platform, android_platform, x11_platform, wayland_platform), + doc = "[`EventLoopExtPumpEvents::pump_app_events()`][platform::pump_events::EventLoopExtPumpEvents::pump_app_events()]" +)] +#![cfg_attr( + not(any(windows_platform, macos_platform, android_platform, x11_platform, wayland_platform)), + doc = "`EventLoopExtPumpEvents::pump_app_events()`" +)] +//! [^1]. See that method's documentation for more reasons about why +//! it's discouraged beyond compatibility reasons. +//! +//! +//! ```no_run +//! use winit::application::ApplicationHandler; +//! use winit::event::WindowEvent; +//! use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +//! use winit::window::{Window, WindowId}; +//! +//! #[derive(Default)] +//! struct App { +//! window: Option, +//! } +//! +//! impl ApplicationHandler for App { +//! fn resumed(&mut self, event_loop: &ActiveEventLoop) { +//! self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); +//! } +//! +//! fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { +//! match event { +//! WindowEvent::CloseRequested => { +//! println!("The close button was pressed; stopping"); +//! event_loop.exit(); +//! }, +//! WindowEvent::RedrawRequested => { +//! // Redraw the application. +//! // +//! // It's preferable for applications that do not render continuously to render in +//! // this event rather than in AboutToWait, since rendering in here allows +//! // the program to gracefully handle redraws requested by the OS. +//! +//! // Draw. +//! +//! // 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 render here instead. +//! self.window.as_ref().unwrap().request_redraw(); +//! } +//! _ => (), +//! } +//! } +//! } +//! +//! # // Intentionally use `fn main` for clarity +//! fn main() { +//! let event_loop = EventLoop::new().unwrap(); +//! +//! // ControlFlow::Poll continuously runs the event loop, even if the OS hasn't +//! // dispatched any events. This is ideal for games and similar applications. +//! event_loop.set_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. +//! event_loop.set_control_flow(ControlFlow::Wait); +//! +//! let mut app = App::default(); +//! event_loop.run_app(&mut app); +//! } +//! ``` +//! +//! [`WindowEvent`] has a [`WindowId`] member. In multi-window environments, it should be +//! compared to the value returned by [`Window::id()`] to determine which [`Window`] +//! dispatched the event. +//! +//! # Drawing on the window +//! +//! Winit 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::WindowAttributes::with_visible] and explicitly make +//! the window visible only once you're ready to render into it. +//! +//! # UI scaling +//! +//! UI scaling is important, go read the docs for the [`dpi`] crate for an +//! introduction. +//! +//! All of Winit's functions return physical types, but can take either logical or physical +//! coordinates as input, allowing you to use the most convenient coordinate system for your +//! particular application. +//! +//! Winit will dispatch a [`ScaleFactorChanged`] event whenever a window's scale factor has changed. +//! This can happen if the user drags their window from a standard-resolution monitor to a high-DPI +//! monitor or if the user changes their DPI settings. This allows you to rescale your application's +//! UI elements and adjust how the platform changes the window's size to reflect the new scale +//! factor. If a window hasn't received a [`ScaleFactorChanged`] event, its scale factor +//! can be found by calling [`window.scale_factor()`]. +//! +//! [`ScaleFactorChanged`]: event::WindowEvent::ScaleFactorChanged +//! [`window.scale_factor()`]: window::Window::scale_factor +//! +//! # Cargo Features +//! +//! Winit provides the following Cargo features: +//! +//! * `x11` (enabled by default): On Unix platforms, enables the X11 backend. +//! * `wayland` (enabled by default): On Unix platforms, enables the Wayland backend. +//! * `rwh_04`: Implement `raw-window-handle v0.4` traits. +//! * `rwh_05`: Implement `raw-window-handle v0.5` traits. +//! * `rwh_06`: Implement `raw-window-handle v0.6` traits. +//! * `serde`: Enables serialization/deserialization of certain types with [Serde](https://crates.io/crates/serde). +//! * `mint`: Enables mint (math interoperability standard types) conversions. +//! +//! See the [`platform`] module for documentation on platform-specific cargo +//! features. +//! +//! [`EventLoop`]: event_loop::EventLoop +//! [`EventLoop::new()`]: event_loop::EventLoop::new +//! [`EventLoop::run_app()`]: event_loop::EventLoop::run_app +//! [`exit()`]: event_loop::ActiveEventLoop::exit +//! [`Window`]: window::Window +//! [`WindowId`]: window::WindowId +//! [`WindowAttributes`]: window::WindowAttributes +//! [`create_window`]: event_loop::ActiveEventLoop::create_window +//! [`Window::id()`]: window::Window::id +//! [`WindowEvent`]: event::WindowEvent +//! [`DeviceEvent`]: event::DeviceEvent +//! [`Event::UserEvent`]: event::Event::UserEvent +//! [`Event::LoopExiting`]: event::Event::LoopExiting +//! [`raw_window_handle`]: ./window/struct.Window.html#method.raw_window_handle +//! [`raw_display_handle`]: ./window/struct.Window.html#method.raw_display_handle +//! [^1]: `EventLoopExtPumpEvents::pump_app_events()` is only available on Windows, macOS, Android, X11 and Wayland. + +#![deny(rust_2018_idioms)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(clippy::all)] +#![deny(unsafe_op_in_unsafe_fn)] +#![cfg_attr(clippy, deny(warnings))] +// Doc feature labels can be tested locally by running RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly +// doc +#![cfg_attr(docsrs, feature(doc_cfg), doc(auto_cfg(hide(doc, docsrs))))] +#![allow(clippy::missing_safety_doc)] +#![warn(clippy::uninlined_format_args)] +// TODO: wasm-binding needs to be updated for that to be resolved, for now just silence it. +#![cfg_attr(web_platform, allow(unknown_lints, renamed_and_removed_lints, wasm_c_abi))] + +#[cfg(feature = "rwh_04")] +pub use rwh_04 as raw_window_handle_04; +#[cfg(feature = "rwh_05")] +pub use rwh_05 as raw_window_handle_05; +#[cfg(feature = "rwh_06")] +pub use rwh_06 as raw_window_handle; + +// Re-export DPI types so that users don't have to put it in Cargo.toml. +#[doc(inline)] +pub use dpi; + +pub mod application; +#[cfg(any(doc, doctest, test))] +pub mod changelog; +#[macro_use] +pub mod error; +mod cursor; +pub mod event; +pub mod event_loop; +mod icon; +pub mod keyboard; +pub mod monitor; +mod platform_impl; +mod utils; +pub mod window; + +pub mod platform; diff --git a/third_party/winit-0.30.13/src/monitor.rs b/third_party/winit-0.30.13/src/monitor.rs new file mode 100644 index 0000000..ed987e2 --- /dev/null +++ b/third_party/winit-0.30.13/src/monitor.rs @@ -0,0 +1,167 @@ +//! Types useful for interacting with a user's monitors. +//! +//! If you want to get basic information about a monitor, you can use the +//! [`MonitorHandle`] type. This is retrieved from one of the following +//! methods, which return an iterator of [`MonitorHandle`]: +//! - [`ActiveEventLoop::available_monitors`][crate::event_loop::ActiveEventLoop::available_monitors]. +//! - [`Window::available_monitors`][crate::window::Window::available_monitors]. +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::platform_impl; + +/// Deprecated! Use `VideoModeHandle` instead. +#[deprecated = "Renamed to `VideoModeHandle`"] +pub type VideoMode = VideoModeHandle; + +/// Describes a fullscreen video mode of a monitor. +/// +/// Can be acquired with [`MonitorHandle::video_modes`]. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct VideoModeHandle { + pub(crate) video_mode: platform_impl::VideoModeHandle, +} + +impl std::fmt::Debug for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.video_mode.fmt(f) + } +} + +impl PartialOrd for VideoModeHandle { + fn partial_cmp(&self, other: &VideoModeHandle) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VideoModeHandle { + fn cmp(&self, other: &VideoModeHandle) -> std::cmp::Ordering { + self.monitor().cmp(&other.monitor()).then( + self.size() + .cmp(&other.size()) + .then( + self.refresh_rate_millihertz() + .cmp(&other.refresh_rate_millihertz()) + .then(self.bit_depth().cmp(&other.bit_depth())), + ) + .reverse(), + ) + } +} + +impl VideoModeHandle { + /// 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 + /// + /// - **Wayland / Orbital:** Always returns 32. + /// - **iOS:** Always returns 32. + #[inline] + pub fn bit_depth(&self) -> u16 { + self.video_mode.bit_depth() + } + + /// Returns the refresh rate of this video mode in mHz. + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + self.video_mode.refresh_rate_millihertz() + } + + /// 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 { + MonitorHandle { inner: self.video_mode.monitor() } + } +} + +impl std::fmt::Display for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}x{} @ {} mHz ({} bpp)", + self.size().width, + self.size().height, + self.refresh_rate_millihertz(), + 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() + } + + /// The monitor refresh rate used by the system. + /// + /// Return `Some` if succeed, or `None` if failed, which usually happens when the monitor + /// the window is on is removed. + /// + /// When using exclusive fullscreen, the refresh rate of the [`VideoModeHandle`] that was + /// used to enter fullscreen should be used instead. + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + self.inner.refresh_rate_millihertz() + } + + /// Returns the scale factor of the underlying monitor. To map logical pixels to physical + /// pixels and vice versa, use [`Window::scale_factor`]. + /// + /// See the [`dpi`] module for more information. + /// + /// ## Platform-specific + /// + /// - **X11:** Can be overridden using the `WINIT_X11_SCALE_FACTOR` environment variable. + /// - **Wayland:** May differ from [`Window::scale_factor`]. + /// - **Android:** Always returns 1.0. + /// + /// [`Window::scale_factor`]: crate::window::Window::scale_factor + #[inline] + pub fn scale_factor(&self) -> f64 { + self.inner.scale_factor() + } + + /// Returns all fullscreen video modes supported by this monitor. + /// + /// ## Platform-specific + /// + /// - **Web:** Always returns an empty iterator + #[inline] + pub fn video_modes(&self) -> impl Iterator { + self.inner.video_modes().map(|video_mode| VideoModeHandle { video_mode }) + } +} diff --git a/third_party/winit-0.30.13/src/platform/android.rs b/third_party/winit-0.30.13/src/platform/android.rs new file mode 100644 index 0000000..b76159c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/android.rs @@ -0,0 +1,188 @@ +//! # Android +//! +//! The Android backend builds on (and exposes types from) the [`ndk`](https://docs.rs/ndk/) crate. +//! +//! Native Android applications need some form of "glue" crate that is responsible +//! for defining the main entry point for your Rust application as well as tracking +//! various life-cycle events and synchronizing with the main JVM thread. +//! +//! Winit uses the [android-activity](https://docs.rs/android-activity/) as a +//! glue crate (prior to `0.28` it used +//! [ndk-glue](https://github.com/rust-windowing/android-ndk-rs/tree/master/ndk-glue)). +//! +//! The version of the glue crate that your application depends on _must_ match the +//! version that Winit depends on because the glue crate is responsible for your +//! application's main entry point. If Cargo resolves multiple versions, they will +//! clash. +//! +//! `winit` glue compatibility table: +//! +//! | winit | ndk-glue | +//! | :---: | :--------------------------: | +//! | 0.30 | `android-activity = "0.6"` | +//! | 0.29 | `android-activity = "0.5"` | +//! | 0.28 | `android-activity = "0.4"` | +//! | 0.27 | `ndk-glue = "0.7"` | +//! | 0.26 | `ndk-glue = "0.5"` | +//! | 0.25 | `ndk-glue = "0.3"` | +//! | 0.24 | `ndk-glue = "0.2"` | +//! +//! The recommended way to avoid a conflict with the glue version is to avoid explicitly +//! depending on the `android-activity` crate, and instead consume the API that +//! is re-exported by Winit under `winit::platform::android::activity::*` +//! +//! Running on an Android device needs a dynamic system library. Add this to Cargo.toml: +//! +//! ```toml +//! [lib] +//! name = "main" +//! crate-type = ["cdylib"] +//! ``` +//! +//! All Android applications are based on an `Activity` subclass, and the +//! `android-activity` crate is designed to support different choices for this base +//! class. Your application _must_ specify the base class it needs via a feature flag: +//! +//! | Base Class | Feature Flag | Notes | +//! | :--------------: | :---------------: | :-----: | +//! | `NativeActivity` | `android-native-activity` | Built-in to Android - it is possible to use without compiling any Java or Kotlin code. Java or Kotlin code may be needed to subclass `NativeActivity` to access some platform features. It does not derive from the [`AndroidAppCompat`] base class.| +//! | [`GameActivity`] | `android-game-activity` | Derives from [`AndroidAppCompat`], a defacto standard `Activity` base class that helps support a wider range of Android versions. Requires a build system that can compile Java or Kotlin and fetch Android dependencies from a [Maven repository][agdk_jetpack] (or link with an embedded [release][agdk_releases] of [`GameActivity`]) | +//! +//! [`GameActivity`]: https://developer.android.com/games/agdk/game-activity +//! [`GameTextInput`]: https://developer.android.com/games/agdk/add-support-for-text-input +//! [`AndroidAppCompat`]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity +//! [agdk_jetpack]: https://developer.android.com/jetpack/androidx/releases/games +//! [agdk_releases]: https://developer.android.com/games/agdk/download#agdk-libraries +//! [Gradle]: https://developer.android.com/studio/build +//! +//! For more details, refer to these `android-activity` [example applications](https://github.com/rust-mobile/android-activity/tree/main/examples). +//! +//! ## Converting from `ndk-glue` to `android-activity` +//! +//! If your application is currently based on `NativeActivity` via the `ndk-glue` crate and building +//! with `cargo apk`, then the minimal changes would be: +//! 1. Remove `ndk-glue` from your `Cargo.toml` +//! 2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.30.13", +//! features = [ "android-native-activity" ] }` +//! 3. Add an `android_main` entrypoint (as above), instead of using the '`[ndk_glue::main]` proc +//! macro from `ndk-macros` (optionally add a dependency on `android_logger` and initialize +//! logging as above). +//! 4. Pass a clone of the `AndroidApp` that your application receives to Winit when building your +//! event loop (as shown above). + +use crate::event_loop::{ActiveEventLoop, EventLoop, EventLoopBuilder}; +use crate::window::{Window, WindowAttributes}; + +use self::activity::{AndroidApp, ConfigurationRef, Rect}; + +/// Additional methods on [`EventLoop`] that are specific to Android. +pub trait EventLoopExtAndroid { + /// Get the [`AndroidApp`] which was used to create this event loop. + fn android_app(&self) -> &AndroidApp; +} + +impl EventLoopExtAndroid for EventLoop { + fn android_app(&self) -> &AndroidApp { + &self.event_loop.android_app + } +} + +/// Additional methods on [`ActiveEventLoop`] that are specific to Android. +pub trait ActiveEventLoopExtAndroid { + /// Get the [`AndroidApp`] which was used to create this event loop. + fn android_app(&self) -> &AndroidApp; +} + +/// Additional methods on [`Window`] that are specific to Android. +pub trait WindowExtAndroid { + fn content_rect(&self) -> Rect; + + fn config(&self) -> ConfigurationRef; +} + +impl WindowExtAndroid for Window { + fn content_rect(&self) -> Rect { + self.window.content_rect() + } + + fn config(&self) -> ConfigurationRef { + self.window.config() + } +} + +impl ActiveEventLoopExtAndroid for ActiveEventLoop { + fn android_app(&self) -> &AndroidApp { + &self.p.app + } +} + +/// Additional methods on [`WindowAttributes`] that are specific to Android. +pub trait WindowAttributesExtAndroid {} + +impl WindowAttributesExtAndroid for WindowAttributes {} + +pub trait EventLoopBuilderExtAndroid { + /// Associates the [`AndroidApp`] that was passed to `android_main()` with the event loop + /// + /// This must be called on Android since the [`AndroidApp`] is not global state. + fn with_android_app(&mut self, app: AndroidApp) -> &mut Self; + + /// Calling this will mark the volume keys to be manually handled by the application + /// + /// Default is to let the operating system handle the volume keys + fn handle_volume_keys(&mut self) -> &mut Self; +} + +impl EventLoopBuilderExtAndroid for EventLoopBuilder { + fn with_android_app(&mut self, app: AndroidApp) -> &mut Self { + self.platform_specific.android_app = Some(app); + self + } + + fn handle_volume_keys(&mut self) -> &mut Self { + self.platform_specific.ignore_volume_keys = false; + self + } +} + +/// Re-export of the `android_activity` API +/// +/// Winit re-exports the `android_activity` API for convenience so that most +/// applications can rely on the Winit crate to resolve the required version of +/// `android_activity` and avoid any chance of a conflict between Winit and the +/// application crate. +/// +/// Unlike most libraries there can only be a single implementation +/// of the `android_activity` glue crate linked with an application because +/// it is responsible for the application's `android_main()` entry point. +/// +/// Since Winit depends on a specific version of `android_activity` the simplest +/// way to avoid creating a conflict is for applications to avoid explicitly +/// depending on the `android_activity` crate, and instead consume the API that +/// is re-exported by Winit. +/// +/// For compatibility applications should then import the [`AndroidApp`] type for +/// their `android_main(app: AndroidApp)` function like: +/// ```rust +/// #[cfg(target_os = "android")] +/// use winit::platform::android::activity::AndroidApp; +/// ``` +pub mod activity { + // We enable the `"native-activity"` feature just so that we can build the + // docs, but it'll be very confusing for users to see the docs with that + // feature enabled, so we avoid inlining it so that they're forced to view + // it on the crate's own docs.rs page. + #[doc(no_inline)] + #[cfg(android_platform)] + pub use android_activity::*; + + #[cfg(not(android_platform))] + #[doc(hidden)] + pub struct Rect; + #[cfg(not(android_platform))] + #[doc(hidden)] + pub struct ConfigurationRef; + #[cfg(not(android_platform))] + #[doc(hidden)] + pub struct AndroidApp; +} diff --git a/third_party/winit-0.30.13/src/platform/ios.rs b/third_party/winit-0.30.13/src/platform/ios.rs new file mode 100644 index 0000000..6e62876 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/ios.rs @@ -0,0 +1,435 @@ +//! # iOS / UIKit +//! +//! Winit has an OS requirement of iOS 8 or higher, and is regularly tested on +//! iOS 9.3. +//! +//! ## Window initialization +//! +//! iOS's main `UIApplicationMain` does some init work that's required by all +//! UI-related code (see issue [#1705]). It is best to create your windows +//! inside [`ApplicationHandler::resumed`]. +//! +//! [#1705]: https://github.com/rust-windowing/winit/issues/1705 +//! [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed +//! +//! ## 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_winit_app() { +//! start_inner() +//! } +//! +//! fn start_inner() { +//! ... +//! } +//! ``` +//! +//! Compile project and then drag resulting .a into Xcode project. Add winit.h to xcode. +//! +//! ```ignore +//! void start_winit_app(); +//! ``` +//! +//! Use start_winit_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 winit: +//! +//! - applicationDidBecomeActive is Resumed +//! - applicationWillResignActive is Suspended +//! - applicationWillTerminate is LoopExiting +//! +//! Keep in mind that after LoopExiting event is received every attempt to draw with +//! opengl will result in segfault. +//! +//! Also note that app may not receive the LoopExiting event if suspended; it might be SIGKILL'ed. +//! +//! ## Custom `UIApplicationDelegate` +//! +//! Winit usually handles everything related to the lifecycle events of the application. Sometimes, +//! though, you might want to access some of the more niche stuff that [the application +//! delegate][app-delegate] provides. This functionality is not exposed directly in Winit, since it +//! would increase the API surface by quite a lot. Instead, Winit guarantees that it will not +//! register an application delegate, so you can set up a custom one in a nib file instead. +//! +//! [app-delegate]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate?language=objc + +use std::os::raw::c_void; + +use crate::event_loop::EventLoop; +use crate::monitor::{MonitorHandle, VideoModeHandle}; +use crate::window::{Window, WindowAttributes}; + +/// 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 { + /// 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 sets the value of the + /// [`prefersStatusBarHidden`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc) + /// property. + /// + /// [`setNeedsStatusBarAppearanceUpdate()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc) + /// is also called for you. + fn set_prefers_status_bar_hidden(&self, hidden: bool); + + /// Sets the preferred status bar style for the [`Window`]. + /// + /// The default is system-defined. + /// + /// This sets the value of the + /// [`preferredStatusBarStyle`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621416-preferredstatusbarstyle?language=objc) + /// property. + /// + /// [`setNeedsStatusBarAppearanceUpdate()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc) + /// is also called for you. + fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle); + + /// Sets whether the [`Window`] should recognize pinch gestures. + /// + /// The default is to not recognize gestures. + fn recognize_pinch_gesture(&self, should_recognize: bool); + + /// Sets whether the [`Window`] should recognize pan gestures. + /// + /// The default is to not recognize gestures. + /// Installs [`UIPanGestureRecognizer`](https://developer.apple.com/documentation/uikit/uipangesturerecognizer) onto view + /// + /// Set the minimum number of touches required: [`minimumNumberOfTouches`](https://developer.apple.com/documentation/uikit/uipangesturerecognizer/1621208-minimumnumberoftouches) + /// + /// Set the maximum number of touches recognized: [`maximumNumberOfTouches`](https://developer.apple.com/documentation/uikit/uipangesturerecognizer/1621208-maximumnumberoftouches) + fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ); + + /// Sets whether the [`Window`] should recognize double tap gestures. + /// + /// The default is to not recognize gestures. + fn recognize_doubletap_gesture(&self, should_recognize: bool); + + /// Sets whether the [`Window`] should recognize rotation gestures. + /// + /// The default is to not recognize gestures. + fn recognize_rotation_gesture(&self, should_recognize: bool); +} + +impl WindowExtIOS for Window { + #[inline] + fn set_scale_factor(&self, scale_factor: f64) { + self.window.maybe_queue_on_main(move |w| w.set_scale_factor(scale_factor)) + } + + #[inline] + fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { + self.window.maybe_queue_on_main(move |w| w.set_valid_orientations(valid_orientations)) + } + + #[inline] + fn set_prefers_home_indicator_hidden(&self, hidden: bool) { + self.window.maybe_queue_on_main(move |w| w.set_prefers_home_indicator_hidden(hidden)) + } + + #[inline] + fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { + self.window.maybe_queue_on_main(move |w| { + w.set_preferred_screen_edges_deferring_system_gestures(edges) + }) + } + + #[inline] + fn set_prefers_status_bar_hidden(&self, hidden: bool) { + self.window.maybe_queue_on_main(move |w| w.set_prefers_status_bar_hidden(hidden)) + } + + #[inline] + fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle) { + self.window.maybe_queue_on_main(move |w| w.set_preferred_status_bar_style(status_bar_style)) + } + + #[inline] + fn recognize_pinch_gesture(&self, should_recognize: bool) { + self.window.maybe_queue_on_main(move |w| w.recognize_pinch_gesture(should_recognize)); + } + + #[inline] + fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ) { + self.window.maybe_queue_on_main(move |w| { + w.recognize_pan_gesture( + should_recognize, + minimum_number_of_touches, + maximum_number_of_touches, + ) + }); + } + + #[inline] + fn recognize_doubletap_gesture(&self, should_recognize: bool) { + self.window.maybe_queue_on_main(move |w| w.recognize_doubletap_gesture(should_recognize)); + } + + #[inline] + fn recognize_rotation_gesture(&self, should_recognize: bool) { + self.window.maybe_queue_on_main(move |w| w.recognize_rotation_gesture(should_recognize)); + } +} + +/// Additional methods on [`WindowAttributes`] that are specific to iOS. +pub trait WindowAttributesExtIOS { + /// 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) -> Self; + + /// 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) -> Self; + + /// 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) -> Self; + + /// 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) -> Self; + + /// 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) -> Self; + + /// Sets the style of the [`Window`]'s status bar. + /// + /// The default is system-defined. + /// + /// This sets the initial value returned by + /// [`-[UIViewController preferredStatusBarStyle]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621416-preferredstatusbarstyle?language=objc), + fn with_preferred_status_bar_style(self, status_bar_style: StatusBarStyle) -> Self; +} + +impl WindowAttributesExtIOS for WindowAttributes { + #[inline] + fn with_scale_factor(mut self, scale_factor: f64) -> Self { + self.platform_specific.scale_factor = Some(scale_factor); + self + } + + #[inline] + fn with_valid_orientations(mut self, valid_orientations: ValidOrientations) -> Self { + self.platform_specific.valid_orientations = valid_orientations; + self + } + + #[inline] + fn with_prefers_home_indicator_hidden(mut self, hidden: bool) -> Self { + self.platform_specific.prefers_home_indicator_hidden = hidden; + self + } + + #[inline] + fn with_preferred_screen_edges_deferring_system_gestures(mut self, edges: ScreenEdge) -> Self { + self.platform_specific.preferred_screen_edges_deferring_system_gestures = edges; + self + } + + #[inline] + fn with_prefers_status_bar_hidden(mut self, hidden: bool) -> Self { + self.platform_specific.prefers_status_bar_hidden = hidden; + self + } + + #[inline] + fn with_preferred_status_bar_style(mut self, status_bar_style: StatusBarStyle) -> Self { + self.platform_specific.preferred_status_bar_style = status_bar_style; + 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 [`VideoModeHandle`] 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) -> VideoModeHandle; +} + +impl MonitorHandleExtIOS for MonitorHandle { + #[inline] + fn ui_screen(&self) -> *mut c_void { + // SAFETY: The marker is only used to get the pointer of the screen + let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() }; + objc2::rc::Retained::as_ptr(self.inner.ui_screen(mtm)) as *mut c_void + } + + #[inline] + fn preferred_video_mode(&self) -> VideoModeHandle { + VideoModeHandle { video_mode: self.inner.preferred_video_mode() } + } +} + +/// Valid orientations for a particular [`Window`]. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ValidOrientations { + /// Excludes `PortraitUpsideDown` on iphone + #[default] + LandscapeAndPortrait, + + Landscape, + + /// Excludes `PortraitUpsideDown` on iphone + Portrait, +} + +/// The device [idiom]. +/// +/// [idiom]: https://developer.apple.com/documentation/uikit/uidevice/1620037-userinterfaceidiom?language=objc +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Idiom { + Unspecified, + + /// iPhone and iPod touch. + Phone, + + /// iPad. + Pad, + + /// tvOS and Apple TV. + TV, + CarPlay, +} + +bitflags::bitflags! { + /// The [edges] of a screen. + /// + /// [edges]: https://developer.apple.com/documentation/uikit/uirectedge?language=objc + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + 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(); + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum StatusBarStyle { + #[default] + Default, + LightContent, + DarkContent, +} diff --git a/third_party/winit-0.30.13/src/platform/macos.rs b/third_party/winit-0.30.13/src/platform/macos.rs new file mode 100644 index 0000000..30623b0 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/macos.rs @@ -0,0 +1,531 @@ +//! # macOS / AppKit +//! +//! Winit has an OS requirement of macOS 10.11 or higher (same as Rust +//! itself), and is regularly tested on macOS 10.14. +//! +//! ## Window initialization +//! +//! A lot of functionality expects the application to be ready before you +//! start doing anything; this includes creating windows, fetching monitors, +//! drawing, and so on, see issues [#2238], [#2051] and [#2087]. +//! +//! If you encounter problems, you should try doing your initialization inside +//! [`ApplicationHandler::resumed`]. +//! +//! [#2238]: https://github.com/rust-windowing/winit/issues/2238 +//! [#2051]: https://github.com/rust-windowing/winit/issues/2051 +//! [#2087]: https://github.com/rust-windowing/winit/issues/2087 +//! [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed +//! +//! ## Custom `NSApplicationDelegate` +//! +//! Winit usually handles everything related to the lifecycle events of the application. Sometimes, +//! though, you might want to do more niche stuff, such as [handle when the user re-activates the +//! application][reopen]. Such functionality is not exposed directly in Winit, since it would +//! increase the API surface by quite a lot. +//! +//! [reopen]: https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428638-applicationshouldhandlereopen?language=objc +//! +//! Instead, Winit guarantees that it will not register an application delegate, so the solution is +//! to register your own application delegate, as outlined in the following example (see +//! `objc2-app-kit` for more detailed information). +#![cfg_attr(target_os = "macos", doc = "```")] +#![cfg_attr(not(target_os = "macos"), doc = "```ignore")] +//! use objc2::rc::Retained; +//! use objc2::runtime::ProtocolObject; +//! use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; +//! use objc2_app_kit::{NSApplication, NSApplicationDelegate}; +//! use objc2_foundation::{NSArray, NSURL, MainThreadMarker, NSObject, NSObjectProtocol}; +//! use winit::event_loop::EventLoop; +//! +//! declare_class!( +//! struct AppDelegate; +//! +//! unsafe impl ClassType for AppDelegate { +//! type Super = NSObject; +//! type Mutability = mutability::MainThreadOnly; +//! const NAME: &'static str = "MyAppDelegate"; +//! } +//! +//! impl DeclaredClass for AppDelegate {} +//! +//! unsafe impl NSObjectProtocol for AppDelegate {} +//! +//! unsafe impl NSApplicationDelegate for AppDelegate { +//! #[method(application:openURLs:)] +//! fn application_openURLs(&self, application: &NSApplication, urls: &NSArray) { +//! // Note: To specifically get `application:openURLs:` to work, you _might_ +//! // have to bundle your application. This is not done in this example. +//! println!("open urls: {application:?}, {urls:?}"); +//! } +//! } +//! ); +//! +//! impl AppDelegate { +//! fn new(mtm: MainThreadMarker) -> Retained { +//! unsafe { msg_send_id![super(mtm.alloc().set_ivars(())), init] } +//! } +//! } +//! +//! fn main() -> Result<(), Box> { +//! let event_loop = EventLoop::new()?; +//! +//! let mtm = MainThreadMarker::new().unwrap(); +//! let delegate = AppDelegate::new(mtm); +//! // Important: Call `sharedApplication` after `EventLoop::new`, +//! // doing it before is not yet supported. +//! let app = NSApplication::sharedApplication(mtm); +//! app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); +//! +//! // event_loop.run_app(&mut my_app); +//! Ok(()) +//! } +//! ``` + +use std::os::raw::c_void; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::event_loop::{ActiveEventLoop, EventLoopBuilder}; +use crate::monitor::MonitorHandle; +use crate::window::{Window, WindowAttributes}; + +/// Additional methods on [`Window`] that are specific to MacOS. +pub trait WindowExtMacOS { + /// 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); + + /// 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; + + /// Select next tab. + fn select_next_tab(&self); + + /// Select previous tab. + fn select_previous_tab(&self); + + /// Select the tab with the given index. + /// + /// Will no-op when the index is out of bounds. + fn select_tab_at_index(&self, index: usize); + + /// Get the number of tabs in the window tab group. + fn num_tabs(&self) -> usize; + + /// Get the window's edit state. + /// + /// # Examples + /// + /// ```ignore + /// WindowEvent::CloseRequested => { + /// if window.is_document_edited() { + /// // Show the user a save pop-up or similar + /// } else { + /// // Close the window + /// drop(window); + /// } + /// } + /// ``` + fn is_document_edited(&self) -> bool; + + /// Put the window in a state which indicates a file save is required. + fn set_document_edited(&self, edited: bool); + + /// Set option as alt behavior as described in [`OptionAsAlt`]. + /// + /// This will ignore diacritical marks and accent characters from + /// being processed as received characters. Instead, the input + /// device's raw character will be placed in event queues with the + /// Alt modifier set. + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt); + + /// Getter for the [`WindowExtMacOS::set_option_as_alt`]. + fn option_as_alt(&self) -> OptionAsAlt; + + /// Disable the Menu Bar and Dock in Simple or Borderless Fullscreen mode. Useful for games. + /// The effect is applied when [`WindowExtMacOS::set_simple_fullscreen`] or + /// [`Window::set_fullscreen`] is called. + fn set_borderless_game(&self, borderless_game: bool); + + /// Getter for the [`WindowExtMacOS::set_borderless_game`]. + fn is_borderless_game(&self) -> bool; +} + +impl WindowExtMacOS for Window { + #[inline] + fn simple_fullscreen(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.simple_fullscreen()) + } + + #[inline] + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { + self.window.maybe_wait_on_main(move |w| w.set_simple_fullscreen(fullscreen)) + } + + #[inline] + fn has_shadow(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.has_shadow()) + } + + #[inline] + fn set_has_shadow(&self, has_shadow: bool) { + self.window.maybe_queue_on_main(move |w| w.set_has_shadow(has_shadow)) + } + + #[inline] + fn set_tabbing_identifier(&self, identifier: &str) { + self.window.maybe_wait_on_main(|w| w.set_tabbing_identifier(identifier)) + } + + #[inline] + fn tabbing_identifier(&self) -> String { + self.window.maybe_wait_on_main(|w| w.tabbing_identifier()) + } + + #[inline] + fn select_next_tab(&self) { + self.window.maybe_queue_on_main(|w| w.select_next_tab()) + } + + #[inline] + fn select_previous_tab(&self) { + self.window.maybe_queue_on_main(|w| w.select_previous_tab()) + } + + #[inline] + fn select_tab_at_index(&self, index: usize) { + self.window.maybe_queue_on_main(move |w| w.select_tab_at_index(index)) + } + + #[inline] + fn num_tabs(&self) -> usize { + self.window.maybe_wait_on_main(|w| w.num_tabs()) + } + + #[inline] + fn is_document_edited(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.is_document_edited()) + } + + #[inline] + fn set_document_edited(&self, edited: bool) { + self.window.maybe_queue_on_main(move |w| w.set_document_edited(edited)) + } + + #[inline] + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) { + self.window.maybe_queue_on_main(move |w| w.set_option_as_alt(option_as_alt)) + } + + #[inline] + fn option_as_alt(&self) -> OptionAsAlt { + self.window.maybe_wait_on_main(|w| w.option_as_alt()) + } + + #[inline] + fn set_borderless_game(&self, borderless_game: bool) { + self.window.maybe_wait_on_main(|w| w.set_borderless_game(borderless_game)) + } + + #[inline] + fn is_borderless_game(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.is_borderless_game()) + } +} + +/// Corresponds to `NSApplicationActivationPolicy`. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ActivationPolicy { + /// Corresponds to `NSApplicationActivationPolicyRegular`. + #[default] + Regular, + + /// Corresponds to `NSApplicationActivationPolicyAccessory`. + Accessory, + + /// Corresponds to `NSApplicationActivationPolicyProhibited`. + Prohibited, +} + +/// Additional methods on [`WindowAttributes`] that are specific to MacOS. +/// +/// **Note:** Properties dealing with the titlebar will be overwritten by the +/// [`WindowAttributes::with_decorations`] method: +/// - `with_titlebar_transparent` +/// - `with_title_hidden` +/// - `with_titlebar_hidden` +/// - `with_titlebar_buttons_hidden` +/// - `with_fullsize_content_view` +pub trait WindowAttributesExtMacOS { + /// 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) -> Self; + /// Makes the titlebar transparent and allows the content to appear behind it. + fn with_titlebar_transparent(self, titlebar_transparent: bool) -> Self; + /// Hides the window title. + fn with_title_hidden(self, title_hidden: bool) -> Self; + /// Hides the window titlebar. + fn with_titlebar_hidden(self, titlebar_hidden: bool) -> Self; + /// Hides the window titlebar buttons. + fn with_titlebar_buttons_hidden(self, titlebar_buttons_hidden: bool) -> Self; + /// Makes the window content appear behind the titlebar. + fn with_fullsize_content_view(self, fullsize_content_view: bool) -> Self; + fn with_disallow_hidpi(self, disallow_hidpi: bool) -> Self; + fn with_has_shadow(self, has_shadow: bool) -> Self; + /// Window accepts click-through mouse events. + fn with_accepts_first_mouse(self, accepts_first_mouse: bool) -> Self; + /// Defines the window tabbing identifier. + /// + /// + fn with_tabbing_identifier(self, identifier: &str) -> Self; + /// Set how the Option keys are interpreted. + /// + /// See [`WindowExtMacOS::set_option_as_alt`] for details on what this means if set. + fn with_option_as_alt(self, option_as_alt: OptionAsAlt) -> Self; + /// See [`WindowExtMacOS::set_borderless_game`] for details on what this means if set. + fn with_borderless_game(self, borderless_game: bool) -> Self; +} + +impl WindowAttributesExtMacOS for WindowAttributes { + #[inline] + fn with_movable_by_window_background(mut self, movable_by_window_background: bool) -> Self { + self.platform_specific.movable_by_window_background = movable_by_window_background; + self + } + + #[inline] + fn with_titlebar_transparent(mut self, titlebar_transparent: bool) -> Self { + self.platform_specific.titlebar_transparent = titlebar_transparent; + self + } + + #[inline] + fn with_titlebar_hidden(mut self, titlebar_hidden: bool) -> Self { + self.platform_specific.titlebar_hidden = titlebar_hidden; + self + } + + #[inline] + fn with_titlebar_buttons_hidden(mut self, titlebar_buttons_hidden: bool) -> Self { + self.platform_specific.titlebar_buttons_hidden = titlebar_buttons_hidden; + self + } + + #[inline] + fn with_title_hidden(mut self, title_hidden: bool) -> Self { + self.platform_specific.title_hidden = title_hidden; + self + } + + #[inline] + fn with_fullsize_content_view(mut self, fullsize_content_view: bool) -> Self { + self.platform_specific.fullsize_content_view = fullsize_content_view; + self + } + + #[inline] + fn with_disallow_hidpi(mut self, disallow_hidpi: bool) -> Self { + self.platform_specific.disallow_hidpi = disallow_hidpi; + self + } + + #[inline] + fn with_has_shadow(mut self, has_shadow: bool) -> Self { + self.platform_specific.has_shadow = has_shadow; + self + } + + #[inline] + fn with_accepts_first_mouse(mut self, accepts_first_mouse: bool) -> Self { + self.platform_specific.accepts_first_mouse = accepts_first_mouse; + self + } + + #[inline] + fn with_tabbing_identifier(mut self, tabbing_identifier: &str) -> Self { + self.platform_specific.tabbing_identifier.replace(tabbing_identifier.to_string()); + self + } + + #[inline] + fn with_option_as_alt(mut self, option_as_alt: OptionAsAlt) -> Self { + self.platform_specific.option_as_alt = option_as_alt; + self + } + + #[inline] + fn with_borderless_game(mut self, borderless_game: bool) -> Self { + self.platform_specific.borderless_game = borderless_game; + self + } +} + +pub trait EventLoopBuilderExtMacOS { + /// Sets the activation policy for the application. If used, this will override + /// any relevant settings provided in the package manifest. + /// For instance, `with_activation_policy(ActivationPolicy::Regular)` will prevent + /// the application from running as an "agent", even if LSUIElement is set to true. + /// + /// If unused, the Winit will honor the package manifest. + /// + /// # Example + /// + /// Set the activation policy to "accessory". + /// + /// ``` + /// use winit::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "macos")] + /// use winit::platform::macos::{ActivationPolicy, EventLoopBuilderExtMacOS}; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "macos")] + /// builder.with_activation_policy(ActivationPolicy::Accessory); + /// # if false { // We can't test this part + /// let event_loop = builder.build(); + /// # } + /// ``` + fn with_activation_policy(&mut self, activation_policy: ActivationPolicy) -> &mut Self; + + /// Used to control whether a default menubar menu is created. + /// + /// Menu creation is enabled by default. + /// + /// # Example + /// + /// Disable creating a default menubar. + /// + /// ``` + /// use winit::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "macos")] + /// use winit::platform::macos::EventLoopBuilderExtMacOS; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "macos")] + /// builder.with_default_menu(false); + /// # if false { // We can't test this part + /// let event_loop = builder.build(); + /// # } + /// ``` + fn with_default_menu(&mut self, enable: bool) -> &mut Self; + + /// 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. + fn with_activate_ignoring_other_apps(&mut self, ignore: bool) -> &mut Self; +} + +impl EventLoopBuilderExtMacOS for EventLoopBuilder { + #[inline] + fn with_activation_policy(&mut self, activation_policy: ActivationPolicy) -> &mut Self { + self.platform_specific.activation_policy = Some(activation_policy); + self + } + + #[inline] + fn with_default_menu(&mut self, enable: bool) -> &mut Self { + self.platform_specific.default_menu = enable; + self + } + + #[inline] + fn with_activate_ignoring_other_apps(&mut self, ignore: bool) -> &mut Self { + self.platform_specific.activate_ignoring_other_apps = ignore; + self + } +} + +/// 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> { + // SAFETY: We only use the marker to get a pointer + let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() }; + self.inner.ns_screen(mtm).map(|s| objc2::rc::Retained::as_ptr(&s) as _) + } +} + +/// Additional methods on [`ActiveEventLoop`] that are specific to macOS. +pub trait ActiveEventLoopExtMacOS { + /// Hide the entire application. In most applications this is typically triggered with + /// Command-H. + fn hide_application(&self); + /// Hide the other applications. In most applications this is typically triggered with + /// Command+Option-H. + fn hide_other_applications(&self); + /// Set 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; +} + +impl ActiveEventLoopExtMacOS for ActiveEventLoop { + fn hide_application(&self) { + self.p.hide_application() + } + + fn hide_other_applications(&self) { + self.p.hide_other_applications() + } + + fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + self.p.set_allows_automatic_window_tabbing(enabled); + } + + fn allows_automatic_window_tabbing(&self) -> bool { + self.p.allows_automatic_window_tabbing() + } +} + +/// Option as alt behavior. +/// +/// The default is `None`. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum OptionAsAlt { + /// The left `Option` key is treated as `Alt`. + OnlyLeft, + + /// The right `Option` key is treated as `Alt`. + OnlyRight, + + /// Both `Option` keys are treated as `Alt`. + Both, + + /// No special handling is applied for `Option` key. + #[default] + None, +} diff --git a/third_party/winit-0.30.13/src/platform/mod.rs b/third_party/winit-0.30.13/src/platform/mod.rs new file mode 100644 index 0000000..4f59303 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/mod.rs @@ -0,0 +1,55 @@ +//! Contains traits with platform-specific methods in them. +//! +//! Only the modules corresponding to the platform you're compiling to will be available. + +#[cfg(any(android_platform, docsrs))] +pub mod android; +#[cfg(any(ios_platform, docsrs))] +pub mod ios; +#[cfg(any(macos_platform, docsrs))] +pub mod macos; +#[cfg(any(orbital_platform, docsrs))] +pub mod orbital; +#[cfg(any(x11_platform, wayland_platform, docsrs))] +pub mod startup_notify; +#[cfg(any(wayland_platform, docsrs))] +pub mod wayland; +#[cfg(any(web_platform, docsrs))] +pub mod web; +#[cfg(any(windows_platform, docsrs))] +pub mod windows; +#[cfg(any(x11_platform, docsrs))] +pub mod x11; + +#[cfg(any( + windows_platform, + macos_platform, + android_platform, + x11_platform, + wayland_platform, + docsrs, +))] +pub mod run_on_demand; + +#[cfg(any( + windows_platform, + macos_platform, + android_platform, + x11_platform, + wayland_platform, + docsrs, +))] +pub mod pump_events; + +#[cfg(any( + windows_platform, + macos_platform, + x11_platform, + wayland_platform, + orbital_platform, + docsrs +))] +pub mod modifier_supplement; + +#[cfg(any(windows_platform, macos_platform, x11_platform, wayland_platform, docsrs))] +pub mod scancode; diff --git a/third_party/winit-0.30.13/src/platform/modifier_supplement.rs b/third_party/winit-0.30.13/src/platform/modifier_supplement.rs new file mode 100644 index 0000000..b1db734 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/modifier_supplement.rs @@ -0,0 +1,35 @@ +use crate::event::KeyEvent; +use crate::keyboard::Key; + +/// Additional methods for the `KeyEvent` which cannot be implemented on all +/// platforms. +pub trait KeyEventExtModifierSupplement { + /// Identical to `KeyEvent::text` but this is affected by Ctrl. + /// + /// For example, pressing Ctrl+a produces `Some("\x01")`. + fn text_with_all_modifiers(&self) -> Option<&str>; + + /// 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`. + fn key_without_modifiers(&self) -> Key; +} + +impl KeyEventExtModifierSupplement for KeyEvent { + #[inline] + fn text_with_all_modifiers(&self) -> Option<&str> { + self.platform_specific.text_with_all_modifiers.as_ref().map(|s| s.as_str()) + } + + #[inline] + fn key_without_modifiers(&self) -> Key { + self.platform_specific.key_without_modifiers.clone() + } +} diff --git a/third_party/winit-0.30.13/src/platform/orbital.rs b/third_party/winit-0.30.13/src/platform/orbital.rs new file mode 100644 index 0000000..d809350 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/orbital.rs @@ -0,0 +1,6 @@ +//! # Orbital / Redox OS +//! +//! Redox OS has some functionality not yet present that will be implemented +//! when its orbital display server provides it. + +// There are no Orbital specific traits yet. diff --git a/third_party/winit-0.30.13/src/platform/pump_events.rs b/third_party/winit-0.30.13/src/platform/pump_events.rs new file mode 100644 index 0000000..4476635 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/pump_events.rs @@ -0,0 +1,143 @@ +use std::time::Duration; + +use crate::application::ApplicationHandler; +use crate::event::Event; +use crate::event_loop::{self, ActiveEventLoop, EventLoop}; + +/// Additional methods on [`EventLoop`] for pumping events within an external event loop +pub trait EventLoopExtPumpEvents { + /// A type provided by the user that can be passed through [`Event::UserEvent`]. + type UserEvent: 'static; + + /// Pump the `EventLoop` to check for and dispatch pending events. + /// + /// This API is designed to enable applications to integrate Winit into an + /// external event loop, for platforms that can support this. + /// + /// The given `timeout` limits how long it may block waiting for new events. + /// + /// Passing a `timeout` of `Some(Duration::ZERO)` would ensure your external + /// event loop is never blocked but you would likely need to consider how + /// to throttle your own external loop. + /// + /// Passing a `timeout` of `None` means that it may wait indefinitely for new + /// events before returning control back to the external loop. + /// + /// **Note:** This is not a portable API, and its usage involves a number of + /// caveats and trade offs that should be considered before using this API! + /// + /// You almost certainly shouldn't use this API, unless you absolutely know it's + /// the only practical option you have. + /// + /// ## Synchronous events + /// + /// Some events _must_ only be handled synchronously via the closure that + /// is passed to Winit so that the handler will also be synchronized with + /// the window system and operating system. + /// + /// This is because some events are driven by a window system callback + /// where the window systems expects the application to have handled the + /// event before returning. + /// + /// **These events can not be buffered and handled outside of the closure + /// passed to Winit.** + /// + /// As a general rule it is not recommended to ever buffer events to handle + /// them outside of the closure passed to Winit since it's difficult to + /// provide guarantees about which events are safe to buffer across all + /// operating systems. + /// + /// Notable events that will certainly create portability problems if + /// buffered and handled outside of Winit include: + /// - `RedrawRequested` events, used to schedule rendering. + /// + /// macOS for example uses a `drawRect` callback to drive rendering + /// within applications and expects rendering to be finished before + /// the `drawRect` callback returns. + /// + /// For portability it's strongly recommended that applications should + /// keep their rendering inside the closure provided to Winit. + /// - Any lifecycle events, such as `Suspended` / `Resumed`. + /// + /// The handling of these events needs to be synchronized with the + /// operating system and it would never be appropriate to buffer a + /// notification that your application has been suspended or resumed and + /// then handled that later since there would always be a chance that + /// other lifecycle events occur while the event is buffered. + /// + /// ## Supported Platforms + /// + /// - Windows + /// - Linux + /// - MacOS + /// - Android + /// + /// ## Unsupported Platforms + /// + /// - **Web:** This API is fundamentally incompatible with the event-based way in which Web + /// browsers work because it's not possible to have a long-running external loop that would + /// block the browser and there is nothing that can be polled to ask for new new events. + /// Events are delivered via callbacks based on an event loop that is internal to the browser + /// itself. + /// - **iOS:** It's not possible to stop and start an `NSApplication` repeatedly on iOS so + /// there's no way to support the same approach to polling as on MacOS. + /// + /// ## Platform-specific + /// + /// - **Windows**: The implementation will use `PeekMessage` when checking for window messages + /// to avoid blocking your external event loop. + /// + /// - **MacOS**: The implementation works in terms of stopping the global application whenever + /// the application `RunLoop` indicates that it is preparing to block and wait for new events. + /// + /// This is very different to the polling APIs that are available on other + /// platforms (the lower level polling primitives on MacOS are private + /// implementation details for `NSApplication` which aren't accessible to + /// application developers) + /// + /// It's likely this will be less efficient than polling on other OSs and + /// it also means the `NSApplication` is stopped while outside of the Winit + /// event loop - and that's observable (for example to crates like `rfd`) + /// because the `NSApplication` is global state. + /// + /// If you render outside of Winit you are likely to see window resizing artifacts + /// since MacOS expects applications to render synchronously during any `drawRect` + /// callback. + fn pump_app_events>( + &mut self, + timeout: Option, + app: &mut A, + ) -> PumpStatus { + #[allow(deprecated)] + self.pump_events(timeout, |event, event_loop| { + event_loop::dispatch_event_for_app(app, event_loop, event) + }) + } + + /// See [`pump_app_events`]. + /// + /// [`pump_app_events`]: Self::pump_app_events + #[deprecated = "use EventLoopExtPumpEvents::pump_app_events"] + fn pump_events(&mut self, timeout: Option, event_handler: F) -> PumpStatus + where + F: FnMut(Event, &ActiveEventLoop); +} + +impl EventLoopExtPumpEvents for EventLoop { + type UserEvent = T; + + fn pump_events(&mut self, timeout: Option, event_handler: F) -> PumpStatus + where + F: FnMut(Event, &ActiveEventLoop), + { + self.event_loop.pump_events(timeout, event_handler) + } +} + +/// The return status for `pump_events` +pub enum PumpStatus { + /// Continue running external loop. + Continue, + /// Exit external loop. + Exit(i32), +} diff --git a/third_party/winit-0.30.13/src/platform/run_on_demand.rs b/third_party/winit-0.30.13/src/platform/run_on_demand.rs new file mode 100644 index 0000000..0bb96e9 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/run_on_demand.rs @@ -0,0 +1,111 @@ +use crate::application::ApplicationHandler; +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::{self, ActiveEventLoop, EventLoop}; + +#[cfg(doc)] +use crate::{platform::pump_events::EventLoopExtPumpEvents, window::Window}; + +/// Additional methods on [`EventLoop`] to return control flow to the caller. +pub trait EventLoopExtRunOnDemand { + /// A type provided by the user that can be passed through [`Event::UserEvent`]. + type UserEvent: 'static; + + /// See [`run_app_on_demand`]. + /// + /// [`run_app_on_demand`]: Self::run_app_on_demand + #[deprecated = "use EventLoopExtRunOnDemand::run_app_on_demand"] + fn run_on_demand(&mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &ActiveEventLoop); + + /// Run the application with the event loop on the calling thread. + /// + /// Unlike [`EventLoop::run_app`], this function accepts non-`'static` (i.e. non-`move`) + /// closures and it is possible to return control back to the caller without + /// consuming the `EventLoop` (by using [`exit()`]) and + /// so the event loop can be re-run after it has exit. + /// + /// It's expected that each run of the loop will be for orthogonal instantiations of your + /// Winit application, but internally each instantiation may re-use some common window + /// system resources, such as a display server connection. + /// + /// This API is not designed to run an event loop in bursts that you can exit from and return + /// to while maintaining the full state of your application. (If you need something like this + /// you can look at the [`EventLoopExtPumpEvents::pump_app_events()`] API) + /// + /// Each time `run_app_on_demand` is called the startup sequence of `init`, followed by + /// `resume` is being preserved. + /// + /// See the [`set_control_flow()`] docs on how to change the event loop's behavior. + /// + /// # Caveats + /// - This extension isn't available on all platforms, since it's not always possible to return + /// to the caller (specifically this is impossible on iOS and Web - though with the Web + /// backend it is possible to use `EventLoopExtWebSys::spawn()` + #[cfg_attr(not(web_platform), doc = "[^1]")] + /// more than once instead). + /// - No [`Window`] state can be carried between separate runs of the event loop. + /// + /// You are strongly encouraged to use [`EventLoop::run_app()`] for portability, unless you + /// specifically need the ability to re-run a single event loop more than once + /// + /// # Supported Platforms + /// - Windows + /// - Linux + /// - macOS + /// - Android + /// + /// # Unsupported Platforms + /// - **Web:** This API is fundamentally incompatible with the event-based way in which Web + /// browsers work because it's not possible to have a long-running external loop that would + /// block the browser and there is nothing that can be polled to ask for new events. Events + /// are delivered via callbacks based on an event loop that is internal to the browser itself. + /// - **iOS:** It's not possible to stop and start an `UIApplication` repeatedly on iOS. + #[cfg_attr(not(web_platform), doc = "[^1]: `spawn()` is only available on `wasm` platforms.")] + /// + #[rustfmt::skip] + /// [`exit()`]: ActiveEventLoop::exit() + /// [`set_control_flow()`]: ActiveEventLoop::set_control_flow() + fn run_app_on_demand>( + &mut self, + app: &mut A, + ) -> Result<(), EventLoopError> { + #[allow(deprecated)] + self.run_on_demand(|event, event_loop| { + event_loop::dispatch_event_for_app(app, event_loop, event) + }) + } +} + +impl EventLoopExtRunOnDemand for EventLoop { + type UserEvent = T; + + fn run_on_demand(&mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &ActiveEventLoop), + { + self.event_loop.window_target().clear_exit(); + self.event_loop.run_on_demand(event_handler) + } +} + +impl ActiveEventLoop { + /// Clear exit status. + pub(crate) fn clear_exit(&self) { + self.p.clear_exit() + } +} + +/// ```compile_fail +/// use winit::event_loop::EventLoop; +/// use winit::platform::run_on_demand::EventLoopExtRunOnDemand; +/// +/// let mut event_loop = EventLoop::new().unwrap(); +/// event_loop.run_on_demand(|_, _| { +/// // Attempt to run the event loop re-entrantly; this must fail. +/// event_loop.run_on_demand(|_, _| {}); +/// }); +/// ``` +#[allow(dead_code)] +fn test_run_on_demand_cannot_access_event_loop() {} diff --git a/third_party/winit-0.30.13/src/platform/scancode.rs b/third_party/winit-0.30.13/src/platform/scancode.rs new file mode 100644 index 0000000..0d78313 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/scancode.rs @@ -0,0 +1,50 @@ +use crate::keyboard::{KeyCode, PhysicalKey}; + +// TODO: Describe what this value contains for each platform + +/// Additional methods for the [`PhysicalKey`] type that allow the user to access the +/// platform-specific scancode. +/// +/// [`PhysicalKey`]: crate::keyboard::PhysicalKey +pub trait PhysicalKeyExtScancode { + /// The raw value of the platform-specific physical key identifier. + /// + /// Returns `Some(key_id)` if the conversion was successful; returns `None` otherwise. + /// + /// ## Platform-specific + /// - **Windows:** A 16bit extended scancode + /// - **Wayland/X11**: A 32-bit linux scancode, which is X11/Wayland keycode subtracted by 8. + fn to_scancode(self) -> Option; + + /// Constructs a `PhysicalKey` from a platform-specific physical key identifier. + /// + /// Note that this conversion may be lossy, i.e. converting the returned `PhysicalKey` back + /// using `to_scancode` might not yield the original value. + /// + /// ## Platform-specific + /// - **Wayland/X11**: A 32-bit linux scancode. When building from X11/Wayland keycode subtract + /// `8` to get the value you wanted. + fn from_scancode(scancode: u32) -> PhysicalKey; +} + +impl PhysicalKeyExtScancode for PhysicalKey { + fn to_scancode(self) -> Option { + crate::platform_impl::physicalkey_to_scancode(self) + } + + fn from_scancode(scancode: u32) -> PhysicalKey { + crate::platform_impl::scancode_to_physicalkey(scancode) + } +} + +impl PhysicalKeyExtScancode for KeyCode { + #[inline] + fn to_scancode(self) -> Option { + ::to_scancode(PhysicalKey::Code(self)) + } + + #[inline] + fn from_scancode(scancode: u32) -> PhysicalKey { + ::from_scancode(scancode) + } +} diff --git a/third_party/winit-0.30.13/src/platform/startup_notify.rs b/third_party/winit-0.30.13/src/platform/startup_notify.rs new file mode 100644 index 0000000..6fab284 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/startup_notify.rs @@ -0,0 +1,99 @@ +//! Window startup notification to handle window raising. +//! +//! The [`ActivationToken`] is essential to ensure that your newly +//! created window will obtain the focus, otherwise the user could +//! be requered to click on the window. +//! +//! Such token is usually delivered via the environment variable and +//! could be read from it with the [`EventLoopExtStartupNotify::read_token_from_env`]. +//! +//! Such token must also be reset after reading it from your environment with +//! [`reset_activation_token_env`] otherwise child processes could inherit it. +//! +//! When starting a new child process with a newly obtained [`ActivationToken`] from +//! [`WindowExtStartupNotify::request_activation_token`] the [`set_activation_token_env`] +//! must be used to propagate it to the child +//! +//! To ensure the delivery of such token by other processes to you, the user should +//! set `StartupNotify=true` inside the `.desktop` file of their application. +//! +//! The specification could be found [`here`]. +//! +//! [`here`]: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-latest.txt + +use std::env; + +use crate::error::NotSupportedError; +use crate::event_loop::{ActiveEventLoop, AsyncRequestSerial}; +use crate::window::{ActivationToken, Window, WindowAttributes}; + +/// The variable which is used mostly on X11. +const X11_VAR: &str = "DESKTOP_STARTUP_ID"; + +/// The variable which is used mostly on Wayland. +const WAYLAND_VAR: &str = "XDG_ACTIVATION_TOKEN"; + +pub trait EventLoopExtStartupNotify { + /// Read the token from the environment. + /// + /// It's recommended **to unset** this environment variable for child processes. + fn read_token_from_env(&self) -> Option; +} + +pub trait WindowExtStartupNotify { + /// Request a new activation token. + /// + /// The token will be delivered inside + fn request_activation_token(&self) -> Result; +} + +pub trait WindowAttributesExtStartupNotify { + /// Use this [`ActivationToken`] during window creation. + /// + /// Not using such a token upon a window could make your window not gaining + /// focus until the user clicks on the window. + fn with_activation_token(self, token: ActivationToken) -> Self; +} + +impl EventLoopExtStartupNotify for ActiveEventLoop { + fn read_token_from_env(&self) -> Option { + match self.p { + #[cfg(wayland_platform)] + crate::platform_impl::ActiveEventLoop::Wayland(_) => env::var(WAYLAND_VAR), + #[cfg(x11_platform)] + crate::platform_impl::ActiveEventLoop::X(_) => env::var(X11_VAR), + } + .ok() + .map(ActivationToken::from_raw) + } +} + +impl WindowExtStartupNotify for Window { + fn request_activation_token(&self) -> Result { + self.window.request_activation_token() + } +} + +impl WindowAttributesExtStartupNotify for WindowAttributes { + fn with_activation_token(mut self, token: ActivationToken) -> Self { + self.platform_specific.activation_token = Some(token); + self + } +} + +/// Remove the activation environment variables from the current process. +/// +/// This is wise to do before running child processes, +/// which may not to support the activation token. +pub fn reset_activation_token_env() { + env::remove_var(X11_VAR); + env::remove_var(WAYLAND_VAR); +} + +/// Set environment variables responsible for activation token. +/// +/// This could be used before running daemon processes. +pub fn set_activation_token_env(token: ActivationToken) { + env::set_var(X11_VAR, &token.token); + env::set_var(WAYLAND_VAR, token.token); +} diff --git a/third_party/winit-0.30.13/src/platform/wayland.rs b/third_party/winit-0.30.13/src/platform/wayland.rs new file mode 100644 index 0000000..9c1a8e4 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/wayland.rs @@ -0,0 +1,131 @@ +//! # Wayland +//! +//! **Note:** Windows don't appear on Wayland until you draw/present to them. +//! +//! By default, Winit loads system libraries using `dlopen`. This can be +//! disabled by disabling the `"wayland-dlopen"` cargo feature. +//! +//! ## Client-side decorations +//! +//! Winit provides client-side decorations by default, but the behaviour can +//! be controlled with the following feature flags: +//! +//! * `wayland-csd-adwaita` (default). +//! * `wayland-csd-adwaita-crossfont`. +//! * `wayland-csd-adwaita-notitle`. + +use std::ffi::c_void; +use std::ptr::NonNull; + +use crate::event_loop::{ActiveEventLoop, EventLoop, EventLoopBuilder}; +use crate::monitor::MonitorHandle; +use crate::window::{Window, WindowAttributes}; + +pub use crate::window::Theme; + +/// Additional methods on [`ActiveEventLoop`] that are specific to Wayland. +pub trait ActiveEventLoopExtWayland { + /// True if the [`ActiveEventLoop`] uses Wayland. + fn is_wayland(&self) -> bool; +} + +impl ActiveEventLoopExtWayland for ActiveEventLoop { + #[inline] + fn is_wayland(&self) -> bool { + self.p.is_wayland() + } +} + +/// Additional methods on [`EventLoop`] that are specific to Wayland. +pub trait EventLoopExtWayland { + /// True if the [`EventLoop`] uses Wayland. + fn is_wayland(&self) -> bool; +} + +impl EventLoopExtWayland for EventLoop { + #[inline] + fn is_wayland(&self) -> bool { + self.event_loop.is_wayland() + } +} + +/// Additional methods on [`EventLoopBuilder`] that are specific to Wayland. +pub trait EventLoopBuilderExtWayland { + /// Force using Wayland. + fn with_wayland(&mut self) -> &mut Self; + + /// 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. + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self; +} + +impl EventLoopBuilderExtWayland for EventLoopBuilder { + #[inline] + fn with_wayland(&mut self) -> &mut Self { + self.platform_specific.forced_backend = Some(crate::platform_impl::Backend::Wayland); + self + } + + #[inline] + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self { + self.platform_specific.any_thread = any_thread; + self + } +} + +/// Additional methods on [`Window`] that are specific to Wayland. +/// +/// [`Window`]: crate::window::Window +pub trait WindowExtWayland { + /// Returns `xdg_toplevel` of the window or [`None`] if the window is X11 window. + fn xdg_toplevel(&self) -> Option>; +} + +impl WindowExtWayland for Window { + #[inline] + fn xdg_toplevel(&self) -> Option> { + #[allow(clippy::single_match)] + match &self.window { + #[cfg(x11_platform)] + crate::platform_impl::Window::X(_) => None, + #[cfg(wayland_platform)] + crate::platform_impl::Window::Wayland(window) => window.xdg_toplevel(), + } + } +} + +/// Additional methods on [`WindowAttributes`] that are specific to Wayland. +pub trait WindowAttributesExtWayland { + /// Build window with the given name. + /// + /// The `general` name sets an application ID, which should match the `.desktop` + /// file distributed with your program. The `instance` is a `no-op`. + /// + /// For details about application ID conventions, see the + /// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id) + fn with_name(self, general: impl Into, instance: impl Into) -> Self; +} + +impl WindowAttributesExtWayland for WindowAttributes { + #[inline] + fn with_name(mut self, general: impl Into, instance: impl Into) -> Self { + self.platform_specific.name = + Some(crate::platform_impl::ApplicationName::new(general.into(), instance.into())); + self + } +} + +/// Additional methods on `MonitorHandle` that are specific to Wayland. +pub trait MonitorHandleExtWayland { + /// Returns the inner identifier of the monitor. + fn native_id(&self) -> u32; +} + +impl MonitorHandleExtWayland for MonitorHandle { + #[inline] + fn native_id(&self) -> u32 { + self.inner.native_identifier() + } +} diff --git a/third_party/winit-0.30.13/src/platform/web.rs b/third_party/winit-0.30.13/src/platform/web.rs new file mode 100644 index 0000000..f257ca4 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/web.rs @@ -0,0 +1,465 @@ +//! # Web +//! +//! The officially supported browsers are Chrome, Firefox and Safari 13.1+, +//! though forks of these should work fine. +//! +//! Winit supports compiling to the `wasm32-unknown-unknown` target with +//! `web-sys`. +//! +//! On the web platform, a Winit window is backed by a `` element. You +//! can either [provide Winit with a `` element][with_canvas], or +//! [let Winit create a `` element which you can then retrieve][get] +//! and insert it into the DOM yourself. +//! +//! Currently, there is no example code using Winit on Web, see [#3473]. For +//! information on using Rust on WebAssembly, check out the [Rust and +//! WebAssembly book]. +//! +//! [with_canvas]: WindowAttributesExtWebSys::with_canvas +//! [get]: WindowExtWebSys::canvas +//! [#3473]: https://github.com/rust-windowing/winit/issues/3473 +//! [Rust and WebAssembly book]: https://rustwasm.github.io/book/ +//! +//! ## CSS properties +//! +//! It is recommended **not** to apply certain CSS properties to the canvas: +//! - [`transform`](https://developer.mozilla.org/en-US/docs/Web/CSS/transform) +//! - [`border`](https://developer.mozilla.org/en-US/docs/Web/CSS/border) +//! - [`padding`](https://developer.mozilla.org/en-US/docs/Web/CSS/padding) +//! +//! The following APIs can't take them into account and will therefore provide inaccurate results: +//! - [`WindowEvent::Resized`] and [`Window::(set_)inner_size()`] +//! - [`WindowEvent::Occluded`] +//! - [`WindowEvent::CursorMoved`], [`WindowEvent::CursorEntered`], [`WindowEvent::CursorLeft`], and +//! [`WindowEvent::Touch`]. +//! - [`Window::set_outer_position()`] +//! +//! [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized +//! [`Window::(set_)inner_size()`]: crate::window::Window::inner_size +//! [`WindowEvent::Occluded`]: crate::event::WindowEvent::Occluded +//! [`WindowEvent::CursorMoved`]: crate::event::WindowEvent::CursorMoved +//! [`WindowEvent::CursorEntered`]: crate::event::WindowEvent::CursorEntered +//! [`WindowEvent::CursorLeft`]: crate::event::WindowEvent::CursorLeft +//! [`WindowEvent::Touch`]: crate::event::WindowEvent::Touch +//! [`Window::set_outer_position()`]: crate::window::Window::set_outer_position + +use std::error::Error; +use std::fmt::{self, Display, Formatter}; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +#[cfg(web_platform)] +use web_sys::HtmlCanvasElement; + +use crate::application::ApplicationHandler; +use crate::cursor::CustomCursorSource; +use crate::event::Event; +use crate::event_loop::{self, ActiveEventLoop, EventLoop}; +#[cfg(web_platform)] +use crate::platform_impl::CustomCursorFuture as PlatformCustomCursorFuture; +use crate::platform_impl::PlatformCustomCursorSource; +use crate::window::{CustomCursor, Window, WindowAttributes}; + +#[cfg(not(web_platform))] +#[doc(hidden)] +pub struct HtmlCanvasElement; + +pub trait WindowExtWebSys { + /// Only returns the canvas if called from inside the window context (the + /// main thread). + fn canvas(&self) -> Option; + + /// Returns [`true`] if calling `event.preventDefault()` is enabled. + /// + /// See [`Window::set_prevent_default()`] for more details. + fn prevent_default(&self) -> bool; + + /// Sets whether `event.preventDefault()` should be called on events on the + /// canvas that have side effects. + /// + /// For example, by default using the mouse wheel would cause the page to scroll, enabling this + /// would prevent that. + /// + /// Some events are impossible to prevent. E.g. Firefox allows to access the native browser + /// context menu with Shift+Rightclick. + fn set_prevent_default(&self, prevent_default: bool); +} + +impl WindowExtWebSys for Window { + #[inline] + fn canvas(&self) -> Option { + self.window.canvas() + } + + fn prevent_default(&self) -> bool { + self.window.prevent_default() + } + + fn set_prevent_default(&self, prevent_default: bool) { + self.window.set_prevent_default(prevent_default) + } +} + +pub trait WindowAttributesExtWebSys { + /// Pass an [`HtmlCanvasElement`] to be used for this [`Window`]. If [`None`], + /// [`WindowAttributes::default()`] will create one. + /// + /// In any case, the canvas won't be automatically inserted into the web page. + /// + /// [`None`] by default. + #[cfg_attr(not(web_platform), doc = "", doc = "[`HtmlCanvasElement`]: #only-available-on-wasm")] + fn with_canvas(self, canvas: Option) -> Self; + + /// Sets whether `event.preventDefault()` should be called on events on the + /// canvas that have side effects. + /// + /// See [`Window::set_prevent_default()`] for more details. + /// + /// Enabled by default. + fn with_prevent_default(self, prevent_default: bool) -> Self; + + /// Whether the canvas should be focusable using the tab key. This is necessary to capture + /// canvas keyboard events. + /// + /// Enabled by default. + fn with_focusable(self, focusable: bool) -> Self; + + /// On window creation, append the canvas element to the web page if it isn't already. + /// + /// Disabled by default. + fn with_append(self, append: bool) -> Self; +} + +impl WindowAttributesExtWebSys for WindowAttributes { + fn with_canvas(mut self, canvas: Option) -> Self { + self.platform_specific.set_canvas(canvas); + self + } + + fn with_prevent_default(mut self, prevent_default: bool) -> Self { + self.platform_specific.prevent_default = prevent_default; + self + } + + fn with_focusable(mut self, focusable: bool) -> Self { + self.platform_specific.focusable = focusable; + self + } + + fn with_append(mut self, append: bool) -> Self { + self.platform_specific.append = append; + self + } +} + +/// Additional methods on `EventLoop` that are specific to the web. +pub trait EventLoopExtWebSys { + /// A type provided by the user that can be passed through `Event::UserEvent`. + type UserEvent: 'static; + + /// Initializes the winit event loop. + /// + /// Unlike + #[cfg_attr(all(web_platform, target_feature = "exception-handling"), doc = "`run_app()`")] + #[cfg_attr( + not(all(web_platform, target_feature = "exception-handling")), + doc = "[`run_app()`]" + )] + /// [^1], this returns immediately, and doesn't throw an exception in order to + /// satisfy its [`!`] return type. + /// + /// Once the event loop has been destroyed, it's possible to reinitialize another event loop + /// by calling this function again. This can be useful if you want to recreate the event loop + /// while the WebAssembly module is still loaded. For example, this can be used to recreate the + /// event loop when switching between tabs on a single page application. + #[rustfmt::skip] + /// + #[cfg_attr( + not(all(web_platform, target_feature = "exception-handling")), + doc = "[`run_app()`]: EventLoop::run_app()" + )] + /// [^1]: `run_app()` is _not_ available on WASM when the target supports `exception-handling`. + fn spawn_app + 'static>(self, app: A); + + /// See [`spawn_app`]. + /// + /// [`spawn_app`]: Self::spawn_app + #[deprecated = "use EventLoopExtWebSys::spawn_app"] + fn spawn(self, event_handler: F) + where + F: 'static + FnMut(Event, &ActiveEventLoop); + + /// Sets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn set_poll_strategy(&self, strategy: PollStrategy); + + /// Gets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn poll_strategy(&self) -> PollStrategy; + + /// Sets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy); + + /// Gets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn wait_until_strategy(&self) -> WaitUntilStrategy; +} + +impl EventLoopExtWebSys for EventLoop { + type UserEvent = T; + + fn spawn_app + 'static>(self, mut app: A) { + self.event_loop.spawn(move |event, event_loop| { + event_loop::dispatch_event_for_app(&mut app, event_loop, event) + }); + } + + fn spawn(self, event_handler: F) + where + F: 'static + FnMut(Event, &ActiveEventLoop), + { + self.event_loop.spawn(event_handler) + } + + fn set_poll_strategy(&self, strategy: PollStrategy) { + self.event_loop.set_poll_strategy(strategy); + } + + fn poll_strategy(&self) -> PollStrategy { + self.event_loop.poll_strategy() + } + + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.event_loop.set_wait_until_strategy(strategy); + } + + fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.event_loop.wait_until_strategy() + } +} + +pub trait ActiveEventLoopExtWebSys { + /// Sets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn set_poll_strategy(&self, strategy: PollStrategy); + + /// Gets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn poll_strategy(&self) -> PollStrategy; + + /// Sets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy); + + /// Gets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn wait_until_strategy(&self) -> WaitUntilStrategy; + + /// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the + /// cursor has completely finished loading. + fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture; +} + +impl ActiveEventLoopExtWebSys for ActiveEventLoop { + #[inline] + fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture { + self.p.create_custom_cursor_async(source) + } + + #[inline] + fn set_poll_strategy(&self, strategy: PollStrategy) { + self.p.set_poll_strategy(strategy); + } + + #[inline] + fn poll_strategy(&self) -> PollStrategy { + self.p.poll_strategy() + } + + #[inline] + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.p.set_wait_until_strategy(strategy); + } + + #[inline] + fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.p.wait_until_strategy() + } +} + +/// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum PollStrategy { + /// Uses [`Window.requestIdleCallback()`] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy will wait for the browser to enter an idle period before running and might + /// be affected by browser throttling. + /// + /// [`Window.requestIdleCallback()`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + IdleCallback, + /// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy will run as fast as possible without disturbing users from interacting with + /// the page and is not affected by browser throttling. + /// + /// This is the default strategy. + /// + /// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + #[default] + Scheduler, +} + +/// Strategy used for [`ControlFlow::WaitUntil`][crate::event_loop::ControlFlow::WaitUntil]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum WaitUntilStrategy { + /// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy is commonly not affected by browser throttling unless the window is not + /// focused. + /// + /// This is the default strategy. + /// + /// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + #[default] + Scheduler, + /// Equal to [`Scheduler`][Self::Scheduler] but wakes up the event loop from a [worker]. + /// + /// This strategy is commonly not affected by browser throttling regardless of window focus. + /// + /// [worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API + Worker, +} + +pub trait CustomCursorExtWebSys { + /// Returns if this cursor is an animation. + fn is_animation(&self) -> bool; + + /// Creates a new cursor from a URL pointing to an image. + /// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url), + /// but browser support for image formats is inconsistent. Using [PNG] is recommended. + /// + /// [PNG]: https://en.wikipedia.org/wiki/PNG + fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource; + + /// Crates a new animated cursor from multiple [`CustomCursor`]s. + /// Supplied `cursors` can't be empty or other animations. + fn from_animation( + duration: Duration, + cursors: Vec, + ) -> Result; +} + +impl CustomCursorExtWebSys for CustomCursor { + fn is_animation(&self) -> bool { + self.inner.animation + } + + fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource { + CustomCursorSource { inner: PlatformCustomCursorSource::Url { url, hotspot_x, hotspot_y } } + } + + fn from_animation( + duration: Duration, + cursors: Vec, + ) -> Result { + if cursors.is_empty() { + return Err(BadAnimation::Empty); + } + + if cursors.iter().any(CustomCursor::is_animation) { + return Err(BadAnimation::Animation); + } + + Ok(CustomCursorSource { + inner: PlatformCustomCursorSource::Animation { duration, cursors }, + }) + } +} + +/// An error produced when using [`CustomCursor::from_animation`] with invalid arguments. +#[derive(Debug, Clone)] +pub enum BadAnimation { + /// Produced when no cursors were supplied. + Empty, + /// Produced when a supplied cursor is an animation. + Animation, +} + +impl fmt::Display for BadAnimation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Empty => write!(f, "No cursors supplied"), + Self::Animation => write!(f, "A supplied cursor is an animation"), + } + } +} + +impl Error for BadAnimation {} + +#[cfg(not(web_platform))] +struct PlatformCustomCursorFuture; + +#[derive(Debug)] +pub struct CustomCursorFuture(pub(crate) PlatformCustomCursorFuture); + +impl Future for CustomCursorFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.0).poll(cx).map_ok(|cursor| CustomCursor { inner: cursor }) + } +} + +#[derive(Clone, Debug)] +pub enum CustomCursorError { + Blob, + Decode(String), + Animation, +} + +impl Display for CustomCursorError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Blob => write!(f, "failed to create `Blob`"), + Self::Decode(error) => write!(f, "failed to decode image: {error}"), + Self::Animation => { + write!(f, "found `CustomCursor` that is an animation when building an animation") + }, + } + } +} + +impl Error for CustomCursorError {} diff --git a/third_party/winit-0.30.13/src/platform/windows.rs b/third_party/winit-0.30.13/src/platform/windows.rs new file mode 100644 index 0000000..e4593be --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/windows.rs @@ -0,0 +1,767 @@ +//! # Windows +//! +//! The supported OS version is Windows 7 or higher, though Windows 10 is +//! tested regularly. +use std::borrow::Borrow; +use std::ffi::c_void; +use std::path::Path; + +use crate::dpi::PhysicalSize; +use crate::event::DeviceId; +use crate::event_loop::EventLoopBuilder; +use crate::monitor::MonitorHandle; +use crate::window::{BadIcon, Icon, Window, WindowAttributes}; + +/// Window Handle type used by Win32 API +pub type HWND = isize; +/// Menu Handle type used by Win32 API +pub type HMENU = isize; +/// Monitor Handle type used by Win32 API +pub type HMONITOR = isize; + +/// Describes a system-drawn backdrop material of a window. +/// +/// For a detailed explanation, see [`DWM_SYSTEMBACKDROP_TYPE docs`]. +/// +/// [`DWM_SYSTEMBACKDROP_TYPE docs`]: https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwm_systembackdrop_type +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BackdropType { + /// Corresponds to `DWMSBT_AUTO`. + /// + /// Usually draws a default backdrop effect on the title bar. + #[default] + Auto = 0, + + /// Corresponds to `DWMSBT_NONE`. + None = 1, + + /// Corresponds to `DWMSBT_MAINWINDOW`. + /// + /// Draws the Mica backdrop material. + MainWindow = 2, + + /// Corresponds to `DWMSBT_TRANSIENTWINDOW`. + /// + /// Draws the Background Acrylic backdrop material. + TransientWindow = 3, + + /// Corresponds to `DWMSBT_TABBEDWINDOW`. + /// + /// Draws the Alt Mica backdrop material. + TabbedWindow = 4, +} + +/// Describes a color used by Windows +#[repr(transparent)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Color(u32); + +impl Color { + // Special constant only valid for the window border and therefore modeled using Option + // for user facing code + const NONE: Color = Color(0xfffffffe); + /// Use the system's default color + pub const SYSTEM_DEFAULT: Color = Color(0xffffffff); + + /// Create a new color from the given RGB values + pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self { + Self((r as u32) | ((g as u32) << 8) | ((b as u32) << 16)) + } +} + +impl Default for Color { + fn default() -> Self { + Self::SYSTEM_DEFAULT + } +} + +/// Describes how the corners of a window should look like. +/// +/// For a detailed explanation, see [`DWM_WINDOW_CORNER_PREFERENCE docs`]. +/// +/// [`DWM_WINDOW_CORNER_PREFERENCE docs`]: https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwm_window_corner_preference +#[repr(i32)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CornerPreference { + /// Corresponds to `DWMWCP_DEFAULT`. + /// + /// Let the system decide when to round window corners. + #[default] + Default = 0, + + /// Corresponds to `DWMWCP_DONOTROUND`. + /// + /// Never round window corners. + DoNotRound = 1, + + /// Corresponds to `DWMWCP_ROUND`. + /// + /// Round the corners, if appropriate. + Round = 2, + + /// Corresponds to `DWMWCP_ROUNDSMALL`. + /// + /// Round the corners if appropriate, with a small radius. + RoundSmall = 3, +} + +/// A wrapper around a [`Window`] that ignores thread-specific window handle limitations. +/// +/// See [`WindowBorrowExtWindows::any_thread`] for more information. +#[derive(Debug)] +pub struct AnyThread(W); + +impl> AnyThread { + /// Get a reference to the inner window. + #[inline] + pub fn get_ref(&self) -> &Window { + self.0.borrow() + } + + /// Get a reference to the inner object. + #[inline] + pub fn inner(&self) -> &W { + &self.0 + } + + /// Unwrap and get the inner window. + #[inline] + pub fn into_inner(self) -> W { + self.0 + } +} + +impl> AsRef for AnyThread { + fn as_ref(&self) -> &Window { + self.get_ref() + } +} + +impl> Borrow for AnyThread { + fn borrow(&self) -> &Window { + self.get_ref() + } +} + +impl> std::ops::Deref for AnyThread { + type Target = Window; + + fn deref(&self) -> &Self::Target { + self.get_ref() + } +} + +#[cfg(feature = "rwh_06")] +impl> rwh_06::HasWindowHandle for AnyThread { + fn window_handle(&self) -> Result, rwh_06::HandleError> { + // SAFETY: The top level user has asserted this is only used safely. + unsafe { self.get_ref().window_handle_any_thread() } + } +} + +/// 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, `winit` 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 winit::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "windows")] + /// use winit::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 winit's internal message dispatching. + /// + /// # Example + /// + /// ``` + /// # use windows_sys::Win32::UI::WindowsAndMessaging::{ACCEL, CreateAcceleratorTableW, TranslateAcceleratorW, DispatchMessageW, TranslateMessage, MSG}; + /// use winit::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "windows")] + /// use winit::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 translated = unsafe { + /// TranslateAcceleratorW( + /// (*msg).hwnd, + /// CreateAcceleratorTableW(accels.as_ptr() as _, 1), + /// msg, + /// ) == 1 + /// }; + /// translated + /// }); + /// ``` + fn with_msg_hook(&mut self, callback: F) -> &mut Self + where + F: FnMut(*const c_void) -> bool + 'static; +} + +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 c_void) -> bool + 'static, + { + self.platform_specific.msg_hook = Some(Box::new(callback)); + self + } +} + +/// Additional methods on `Window` that are specific to Windows. +pub trait WindowExtWindows { + /// 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 [`WindowAttributesExtWindows::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); + + /// Whether to show or hide the window icon in the taskbar. + fn set_skip_taskbar(&self, skip: bool); + + /// 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); + + /// Sets system-drawn backdrop type. + /// + /// Requires Windows 11 build 22523+. + fn set_system_backdrop(&self, backdrop_type: BackdropType); + + /// Sets the color of the window border. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_border_color(&self, color: Option); + + /// Sets the background color of the title bar. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_title_background_color(&self, color: Option); + + /// Sets the color of the window title. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_title_text_color(&self, color: Color); + + /// Sets the preferred style of the window corners. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_corner_preference(&self, preference: CornerPreference); + + /// Get the raw window handle for this [`Window`] without checking for thread affinity. + /// + /// Window handles in Win32 have a property called "thread affinity" that ties them to their + /// origin thread. Some operations can only happen on the window's origin thread, while others + /// can be called from any thread. For example, [`SetWindowSubclass`] is not thread safe while + /// [`GetDC`] is thread safe. + /// + /// In Rust terms, the window handle is `Send` sometimes but `!Send` other times. + /// + /// Therefore, in order to avoid confusing threading errors, [`Window`] only returns the + /// window handle when the [`window_handle`] function is called from the thread that created + /// the window. In other cases, it returns an [`Unavailable`] error. + /// + /// However in some cases you may already know that you are using the window handle for + /// operations that are guaranteed to be thread-safe. In which case this function aims + /// to provide an escape hatch so these functions are still accessible from other threads. + /// + /// # Safety + /// + /// It is the responsibility of the user to only pass the window handle into thread-safe + /// Win32 APIs. + /// + /// [`SetWindowSubclass`]: https://learn.microsoft.com/en-us/windows/win32/api/commctrl/nf-commctrl-setwindowsubclass + /// [`GetDC`]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdc + /// [`Window`]: crate::window::Window + /// [`window_handle`]: https://docs.rs/raw-window-handle/latest/raw_window_handle/trait.HasWindowHandle.html#tymethod.window_handle + /// [`Unavailable`]: https://docs.rs/raw-window-handle/latest/raw_window_handle/enum.HandleError.html#variant.Unavailable + /// + /// ## Example + /// + /// ```no_run + /// # use winit::window::Window; + /// # fn scope(window: Window) { + /// use std::thread; + /// use winit::platform::windows::WindowExtWindows; + /// use winit::raw_window_handle::HasWindowHandle; + /// + /// // We can get the window handle on the current thread. + /// let handle = window.window_handle().unwrap(); + /// + /// // However, on another thread, we can't! + /// thread::spawn(move || { + /// assert!(window.window_handle().is_err()); + /// + /// // We can use this function as an escape hatch. + /// let handle = unsafe { window.window_handle_any_thread().unwrap() }; + /// }); + /// # } + /// ``` + #[cfg(feature = "rwh_06")] + unsafe fn window_handle_any_thread( + &self, + ) -> Result, rwh_06::HandleError>; +} + +impl WindowExtWindows for Window { + #[inline] + fn set_enable(&self, enabled: bool) { + self.window.set_enable(enabled) + } + + #[inline] + fn set_taskbar_icon(&self, taskbar_icon: Option) { + self.window.set_taskbar_icon(taskbar_icon) + } + + #[inline] + fn set_skip_taskbar(&self, skip: bool) { + self.window.set_skip_taskbar(skip) + } + + #[inline] + fn set_undecorated_shadow(&self, shadow: bool) { + self.window.set_undecorated_shadow(shadow) + } + + #[inline] + fn set_system_backdrop(&self, backdrop_type: BackdropType) { + self.window.set_system_backdrop(backdrop_type) + } + + #[inline] + fn set_border_color(&self, color: Option) { + self.window.set_border_color(color.unwrap_or(Color::NONE)) + } + + #[inline] + fn set_title_background_color(&self, color: Option) { + // The windows docs don't mention NONE as a valid options but it works in practice and is + // useful to circumvent the Windows option "Show accent color on title bars and + // window borders" + self.window.set_title_background_color(color.unwrap_or(Color::NONE)) + } + + #[inline] + fn set_title_text_color(&self, color: Color) { + self.window.set_title_text_color(color) + } + + #[inline] + fn set_corner_preference(&self, preference: CornerPreference) { + self.window.set_corner_preference(preference) + } + + #[cfg(feature = "rwh_06")] + unsafe fn window_handle_any_thread( + &self, + ) -> Result, rwh_06::HandleError> { + unsafe { + let handle = self.window.rwh_06_no_thread_check()?; + + // SAFETY: The handle is valid in this context. + Ok(rwh_06::WindowHandle::borrow_raw(handle)) + } + } +} + +/// Additional methods for anything that dereference to [`Window`]. +/// +/// [`Window`]: crate::window::Window +pub trait WindowBorrowExtWindows: Borrow + Sized { + /// Create an object that allows accessing the inner window handle in a thread-unsafe way. + /// + /// It is possible to call [`window_handle_any_thread`] to get around Windows's thread + /// affinity limitations. However, it may be desired to pass the [`Window`] into something + /// that requires the [`HasWindowHandle`] trait, while ignoring thread affinity limitations. + /// + /// This function wraps anything that implements `Borrow` into a structure that + /// uses the inner window handle as a mean of implementing [`HasWindowHandle`]. It wraps + /// `Window`, `&Window`, `Arc`, and other reference types. + /// + /// # Safety + /// + /// It is the responsibility of the user to only pass the window handle into thread-safe + /// Win32 APIs. + /// + /// [`window_handle_any_thread`]: WindowExtWindows::window_handle_any_thread + /// [`Window`]: crate::window::Window + /// [`HasWindowHandle`]: rwh_06::HasWindowHandle + unsafe fn any_thread(self) -> AnyThread { + AnyThread(self) + } +} + +impl + Sized> WindowBorrowExtWindows for W {} + +/// Additional methods on `WindowAttributes` that are specific to Windows. +#[allow(rustdoc::broken_intra_doc_links)] +pub trait WindowAttributesExtWindows { + /// Set an owner to the window to be created. Can be used to create a dialog box, for example. + /// This only works when [`WindowAttributes::with_parent_window`] isn't called or set to `None`. + /// 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) -> Self; + + /// 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 [`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. + #[cfg_attr( + windows_platform, + doc = "[`CreateMenu`]: windows_sys::Win32::UI::WindowsAndMessaging::CreateMenu" + )] + #[cfg_attr(not(windows_platform), doc = "[`CreateMenu`]: #only-available-on-windows")] + fn with_menu(self, menu: HMENU) -> Self; + + /// This sets `ICON_BIG`. A good ceiling here is 256x256. + fn with_taskbar_icon(self, taskbar_icon: Option) -> Self; + + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + fn with_no_redirection_bitmap(self, flag: bool) -> Self; + + /// 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 winit 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 winit functions. See for more information. + fn with_drag_and_drop(self, flag: bool) -> Self; + + /// Whether show or hide the window icon in the taskbar. + fn with_skip_taskbar(self, skip: bool) -> Self; + + /// Customize the window class name. + fn with_class_name>(self, class_name: S) -> Self; + + /// 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) -> Self; + + /// Sets system-drawn backdrop type. + /// + /// Requires Windows 11 build 22523+. + fn with_system_backdrop(self, backdrop_type: BackdropType) -> Self; + + /// This sets or removes `WS_CLIPCHILDREN` style. + fn with_clip_children(self, flag: bool) -> Self; + + /// Sets the color of the window border. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_border_color(self, color: Option) -> Self; + + /// Sets the background color of the title bar. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_title_background_color(self, color: Option) -> Self; + + /// Sets the color of the window title. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_title_text_color(self, color: Color) -> Self; + + /// Sets the preferred style of the window corners. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_corner_preference(self, corners: CornerPreference) -> Self; +} + +impl WindowAttributesExtWindows for WindowAttributes { + #[inline] + fn with_owner_window(mut self, parent: HWND) -> Self { + self.platform_specific.owner = Some(parent); + self + } + + #[inline] + fn with_menu(mut self, menu: HMENU) -> Self { + self.platform_specific.menu = Some(menu); + self + } + + #[inline] + fn with_taskbar_icon(mut self, taskbar_icon: Option) -> Self { + self.platform_specific.taskbar_icon = taskbar_icon; + self + } + + #[inline] + fn with_no_redirection_bitmap(mut self, flag: bool) -> Self { + self.platform_specific.no_redirection_bitmap = flag; + self + } + + #[inline] + fn with_drag_and_drop(mut self, flag: bool) -> Self { + self.platform_specific.drag_and_drop = flag; + self + } + + #[inline] + fn with_skip_taskbar(mut self, skip: bool) -> Self { + self.platform_specific.skip_taskbar = skip; + self + } + + #[inline] + fn with_class_name>(mut self, class_name: S) -> Self { + self.platform_specific.class_name = class_name.into(); + self + } + + #[inline] + fn with_undecorated_shadow(mut self, shadow: bool) -> Self { + self.platform_specific.decoration_shadow = shadow; + self + } + + #[inline] + fn with_system_backdrop(mut self, backdrop_type: BackdropType) -> Self { + self.platform_specific.backdrop_type = backdrop_type; + self + } + + #[inline] + fn with_clip_children(mut self, flag: bool) -> Self { + self.platform_specific.clip_children = flag; + self + } + + #[inline] + fn with_border_color(mut self, color: Option) -> Self { + self.platform_specific.border_color = Some(color.unwrap_or(Color::NONE)); + self + } + + #[inline] + fn with_title_background_color(mut self, color: Option) -> Self { + self.platform_specific.title_background_color = Some(color.unwrap_or(Color::NONE)); + self + } + + #[inline] + fn with_title_text_color(mut self, color: Color) -> Self { + self.platform_specific.title_text_color = Some(color); + self + } + + #[inline] + fn with_corner_preference(mut self, corners: CornerPreference) -> Self { + self.platform_specific.corner_preference = Some(corners); + 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) -> HMONITOR; +} + +impl MonitorHandleExtWindows for MonitorHandle { + #[inline] + fn native_id(&self) -> String { + self.inner.native_identifier() + } + + #[inline] + fn hmonitor(&self) -> HMONITOR { + self.inner.hmonitor() + } +} + +/// 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. +/// +/// Windows icons can be created from files, or from the [`embedded resources`](https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files). +/// +/// The `ICON` resource definition statement use the following syntax: +/// ```rc +/// nameID ICON filename +/// ``` +/// `nameID` is a unique name or a 16-bit unsigned integer value identifying the resource, +/// `filename` is the name of the file that contains the resource. +/// +/// More information about the `ICON` resource can be found at [`Microsoft Learn`](https://learn.microsoft.com/en-us/windows/win32/menurc/icon-resource) portal. +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 by its ordinal id. + /// + /// The valid `ordinal` values range from 1 to [`u16::MAX`] (inclusive). The value `0` is an + /// invalid ordinal id, but it can be used with [`from_resource_name`] as `"0"`. + /// + /// [`from_resource_name`]: IconExtWindows::from_resource_name + /// + /// 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; + + /// Create an icon from a resource embedded in this executable or library by its name. + /// + /// 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. + /// + /// # Notes + /// + /// Consider the following resource definition statements: + /// ```rc + /// app ICON "app.ico" + /// 1 ICON "a.ico" + /// 0027 ICON "custom.ico" + /// 0 ICON "alt.ico" + /// ``` + /// + /// Due to some internal implementation details of the resource embedding/loading process on + /// Windows platform, strings that can be interpreted as 16-bit unsigned integers (`"1"`, + /// `"002"`, etc.) cannot be used as valid resource names, and instead should be passed into + /// [`from_resource`]: + /// + /// [`from_resource`]: IconExtWindows::from_resource + /// + /// ```rust,no_run + /// use winit::platform::windows::IconExtWindows; + /// use winit::window::Icon; + /// + /// assert!(Icon::from_resource_name("app", None).is_ok()); + /// assert!(Icon::from_resource(1, None).is_ok()); + /// assert!(Icon::from_resource(27, None).is_ok()); + /// assert!(Icon::from_resource_name("27", None).is_err()); + /// assert!(Icon::from_resource_name("0027", None).is_err()); + /// ``` + /// + /// While `0` cannot be used as an ordinal id (see [`from_resource`]), it can be used as a + /// name: + /// + /// [`from_resource`]: IconExtWindows::from_resource + /// + /// ```rust,no_run + /// # use winit::platform::windows::IconExtWindows; + /// # use winit::window::Icon; + /// assert!(Icon::from_resource_name("0", None).is_ok()); + /// assert!(Icon::from_resource(0, None).is_err()); + /// ``` + fn from_resource_name(name: &str, size: Option>) -> Result; +} + +impl IconExtWindows for Icon { + fn from_path>( + path: P, + size: Option>, + ) -> Result { + let win_icon = crate::platform_impl::WinIcon::from_path(path, size)?; + Ok(Icon { inner: win_icon }) + } + + fn from_resource(ordinal: u16, size: Option>) -> Result { + let win_icon = crate::platform_impl::WinIcon::from_resource(ordinal, size)?; + Ok(Icon { inner: win_icon }) + } + + fn from_resource_name(name: &str, size: Option>) -> Result { + let win_icon = crate::platform_impl::WinIcon::from_resource_name(name, size)?; + Ok(Icon { inner: win_icon }) + } +} diff --git a/third_party/winit-0.30.13/src/platform/x11.rs b/third_party/winit-0.30.13/src/platform/x11.rs new file mode 100644 index 0000000..4ab900c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform/x11.rs @@ -0,0 +1,254 @@ +//! # X11 +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::event_loop::{ActiveEventLoop, EventLoop, EventLoopBuilder}; +use crate::monitor::MonitorHandle; +use crate::window::{Window, WindowAttributes}; + +use crate::dpi::Size; + +/// X window type. Maps directly to +/// [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html). +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum WindowType { + /// A desktop feature. This can include a single window containing desktop icons with the same + /// dimensions as the screen, allowing the desktop environment to have full control of the + /// desktop, without the need for proxying root window clicks. + Desktop, + /// A dock or panel feature. Typically a Window Manager would keep such windows on top of all + /// other windows. + Dock, + /// Toolbar windows. "Torn off" from the main application. + Toolbar, + /// Pinnable menu windows. "Torn off" from the main application. + Menu, + /// A small persistent utility window, such as a palette or toolbox. + Utility, + /// The window is a splash screen displayed as an application is starting up. + Splash, + /// This is a dialog window. + Dialog, + /// A dropdown menu that usually appears when the user clicks on an item in a menu bar. + /// This property is typically used on override-redirect windows. + DropdownMenu, + /// A popup menu that usually appears when the user right clicks on an object. + /// This property is typically used on override-redirect windows. + PopupMenu, + /// A tooltip window. Usually used to show additional information when hovering over an object + /// with the cursor. This property is typically used on override-redirect windows. + Tooltip, + /// The window is a notification. + /// This property is typically used on override-redirect windows. + Notification, + /// This should be used on the windows that are popped up by combo boxes. + /// This property is typically used on override-redirect windows. + Combo, + /// This indicates the window is being dragged. + /// This property is typically used on override-redirect windows. + Dnd, + /// This is a normal, top-level window. + #[default] + Normal, +} + +/// The first argument in the provided hook will be the pointer to `XDisplay` +/// and the second one the pointer to [`XErrorEvent`]. The returned `bool` is an +/// indicator whether the error was handled by the callback. +/// +/// [`XErrorEvent`]: https://linux.die.net/man/3/xerrorevent +pub type XlibErrorHook = + Box bool + Send + Sync>; + +/// A unique identifier for an X11 visual. +pub type XVisualID = u32; + +/// A unique identifier for an X11 window. +pub type XWindow = u32; + +/// Hook to winit's xlib error handling callback. +/// +/// This method is provided as a safe way to handle the errors coming from X11 +/// when using xlib in external crates, like glutin for GLX access. Trying to +/// handle errors by speculating with `XSetErrorHandler` is [`unsafe`]. +/// +/// **Be aware that your hook is always invoked and returning `true` from it will +/// prevent `winit` from getting the error itself. It's wise to always return +/// `false` if you're not initiated the `Sync`.** +/// +/// [`unsafe`]: https://www.remlab.net/op/xlib.shtml +#[inline] +pub fn register_xlib_error_hook(hook: XlibErrorHook) { + // Append new hook. + crate::platform_impl::XLIB_ERROR_HOOKS.lock().unwrap().push(hook); +} + +/// Additional methods on [`ActiveEventLoop`] that are specific to X11. +pub trait ActiveEventLoopExtX11 { + /// True if the [`ActiveEventLoop`] uses X11. + fn is_x11(&self) -> bool; +} + +impl ActiveEventLoopExtX11 for ActiveEventLoop { + #[inline] + fn is_x11(&self) -> bool { + !self.p.is_wayland() + } +} + +/// Additional methods on [`EventLoop`] that are specific to X11. +pub trait EventLoopExtX11 { + /// True if the [`EventLoop`] uses X11. + fn is_x11(&self) -> bool; +} + +impl EventLoopExtX11 for EventLoop { + #[inline] + fn is_x11(&self) -> bool { + !self.event_loop.is_wayland() + } +} + +/// Additional methods on [`EventLoopBuilder`] that are specific to X11. +pub trait EventLoopBuilderExtX11 { + /// Force using X11. + fn with_x11(&mut self) -> &mut Self; + + /// 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. + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self; +} + +impl EventLoopBuilderExtX11 for EventLoopBuilder { + #[inline] + fn with_x11(&mut self) -> &mut Self { + self.platform_specific.forced_backend = Some(crate::platform_impl::Backend::X); + self + } + + #[inline] + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self { + self.platform_specific.any_thread = any_thread; + self + } +} + +/// Additional methods on [`Window`] that are specific to X11. +pub trait WindowExtX11 {} + +impl WindowExtX11 for Window {} + +/// Additional methods on [`WindowAttributes`] that are specific to X11. +pub trait WindowAttributesExtX11 { + /// Create this window with a specific X11 visual. + fn with_x11_visual(self, visual_id: XVisualID) -> Self; + + fn with_x11_screen(self, screen_id: i32) -> Self; + + /// Build window with the given `general` and `instance` names. + /// + /// The `general` sets general class of `WM_CLASS(STRING)`, while `instance` set the + /// instance part of it. The resulted property looks like `WM_CLASS(STRING) = "instance", + /// "general"`. + /// + /// For details about application ID conventions, see the + /// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id) + fn with_name(self, general: impl Into, instance: impl Into) -> Self; + + /// Build window with override-redirect flag; defaults to false. + fn with_override_redirect(self, override_redirect: bool) -> Self; + + /// Build window with `_NET_WM_WINDOW_TYPE` hints; defaults to `Normal`. + fn with_x11_window_type(self, x11_window_type: Vec) -> Self; + + /// Build window with base size hint. + /// + /// ``` + /// # use winit::dpi::{LogicalSize, PhysicalSize}; + /// # use winit::window::Window; + /// # use winit::platform::x11::WindowAttributesExtX11; + /// // Specify the size in logical dimensions like this: + /// Window::default_attributes().with_base_size(LogicalSize::new(400.0, 200.0)); + /// + /// // Or specify the size in physical dimensions like this: + /// Window::default_attributes().with_base_size(PhysicalSize::new(400, 200)); + /// ``` + fn with_base_size>(self, base_size: S) -> Self; + + /// Embed this window into another parent window. + /// + /// # Example + /// + /// ```no_run + /// use winit::window::Window; + /// use winit::event_loop::ActiveEventLoop; + /// use winit::platform::x11::{XWindow, WindowAttributesExtX11}; + /// # fn create_window(event_loop: &ActiveEventLoop) -> Result<(), Box> { + /// let parent_window_id = std::env::args().nth(1).unwrap().parse::()?; + /// let window_attributes = Window::default_attributes().with_embed_parent_window(parent_window_id); + /// let window = event_loop.create_window(window_attributes)?; + /// # Ok(()) } + /// ``` + fn with_embed_parent_window(self, parent_window_id: XWindow) -> Self; +} + +impl WindowAttributesExtX11 for WindowAttributes { + #[inline] + fn with_x11_visual(mut self, visual_id: XVisualID) -> Self { + self.platform_specific.x11.visual_id = Some(visual_id); + self + } + + #[inline] + fn with_x11_screen(mut self, screen_id: i32) -> Self { + self.platform_specific.x11.screen_id = Some(screen_id); + self + } + + #[inline] + fn with_name(mut self, general: impl Into, instance: impl Into) -> Self { + self.platform_specific.name = + Some(crate::platform_impl::ApplicationName::new(general.into(), instance.into())); + self + } + + #[inline] + fn with_override_redirect(mut self, override_redirect: bool) -> Self { + self.platform_specific.x11.override_redirect = override_redirect; + self + } + + #[inline] + fn with_x11_window_type(mut self, x11_window_types: Vec) -> Self { + self.platform_specific.x11.x11_window_types = x11_window_types; + self + } + + #[inline] + fn with_base_size>(mut self, base_size: S) -> Self { + self.platform_specific.x11.base_size = Some(base_size.into()); + self + } + + #[inline] + fn with_embed_parent_window(mut self, parent_window_id: XWindow) -> Self { + self.platform_specific.x11.embed_window = Some(parent_window_id); + self + } +} + +/// Additional methods on `MonitorHandle` that are specific to X11. +pub trait MonitorHandleExtX11 { + /// Returns the inner identifier of the monitor. + fn native_id(&self) -> u32; +} + +impl MonitorHandleExtX11 for MonitorHandle { + #[inline] + fn native_id(&self) -> u32 { + self.inner.native_identifier() + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/android/keycodes.rs b/third_party/winit-0.30.13/src/platform_impl/android/keycodes.rs new file mode 100644 index 0000000..207d549 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/android/keycodes.rs @@ -0,0 +1,608 @@ +use android_activity::input::{KeyAction, KeyEvent, KeyMapChar, Keycode}; +use android_activity::AndroidApp; + +use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; + +pub fn to_physical_key(keycode: Keycode) -> PhysicalKey { + PhysicalKey::Code(match keycode { + Keycode::A => KeyCode::KeyA, + Keycode::B => KeyCode::KeyB, + Keycode::C => KeyCode::KeyC, + Keycode::D => KeyCode::KeyD, + Keycode::E => KeyCode::KeyE, + Keycode::F => KeyCode::KeyF, + Keycode::G => KeyCode::KeyG, + Keycode::H => KeyCode::KeyH, + Keycode::I => KeyCode::KeyI, + Keycode::J => KeyCode::KeyJ, + Keycode::K => KeyCode::KeyK, + Keycode::L => KeyCode::KeyL, + Keycode::M => KeyCode::KeyM, + Keycode::N => KeyCode::KeyN, + Keycode::O => KeyCode::KeyO, + Keycode::P => KeyCode::KeyP, + Keycode::Q => KeyCode::KeyQ, + Keycode::R => KeyCode::KeyR, + Keycode::S => KeyCode::KeyS, + Keycode::T => KeyCode::KeyT, + Keycode::U => KeyCode::KeyU, + Keycode::V => KeyCode::KeyV, + Keycode::W => KeyCode::KeyW, + Keycode::X => KeyCode::KeyX, + Keycode::Y => KeyCode::KeyY, + Keycode::Z => KeyCode::KeyZ, + + Keycode::Keycode0 => KeyCode::Digit0, + Keycode::Keycode1 => KeyCode::Digit1, + Keycode::Keycode2 => KeyCode::Digit2, + Keycode::Keycode3 => KeyCode::Digit3, + Keycode::Keycode4 => KeyCode::Digit4, + Keycode::Keycode5 => KeyCode::Digit5, + Keycode::Keycode6 => KeyCode::Digit6, + Keycode::Keycode7 => KeyCode::Digit7, + Keycode::Keycode8 => KeyCode::Digit8, + Keycode::Keycode9 => KeyCode::Digit9, + + Keycode::Numpad0 => KeyCode::Numpad0, + Keycode::Numpad1 => KeyCode::Numpad1, + Keycode::Numpad2 => KeyCode::Numpad2, + Keycode::Numpad3 => KeyCode::Numpad3, + Keycode::Numpad4 => KeyCode::Numpad4, + Keycode::Numpad5 => KeyCode::Numpad5, + Keycode::Numpad6 => KeyCode::Numpad6, + Keycode::Numpad7 => KeyCode::Numpad7, + Keycode::Numpad8 => KeyCode::Numpad8, + Keycode::Numpad9 => KeyCode::Numpad9, + + Keycode::NumpadAdd => KeyCode::NumpadAdd, + Keycode::NumpadSubtract => KeyCode::NumpadSubtract, + Keycode::NumpadMultiply => KeyCode::NumpadMultiply, + Keycode::NumpadDivide => KeyCode::NumpadDivide, + Keycode::NumpadEnter => KeyCode::NumpadEnter, + Keycode::NumpadEquals => KeyCode::NumpadEqual, + Keycode::NumpadComma => KeyCode::NumpadComma, + Keycode::NumpadDot => KeyCode::NumpadDecimal, + Keycode::NumLock => KeyCode::NumLock, + + Keycode::DpadLeft => KeyCode::ArrowLeft, + Keycode::DpadRight => KeyCode::ArrowRight, + Keycode::DpadUp => KeyCode::ArrowUp, + Keycode::DpadDown => KeyCode::ArrowDown, + + Keycode::F1 => KeyCode::F1, + Keycode::F2 => KeyCode::F2, + Keycode::F3 => KeyCode::F3, + Keycode::F4 => KeyCode::F4, + Keycode::F5 => KeyCode::F5, + Keycode::F6 => KeyCode::F6, + Keycode::F7 => KeyCode::F7, + Keycode::F8 => KeyCode::F8, + Keycode::F9 => KeyCode::F9, + Keycode::F10 => KeyCode::F10, + Keycode::F11 => KeyCode::F11, + Keycode::F12 => KeyCode::F12, + + Keycode::Space => KeyCode::Space, + Keycode::Escape => KeyCode::Escape, + Keycode::Enter => KeyCode::Enter, // not on the Numpad + Keycode::Tab => KeyCode::Tab, + + Keycode::PageUp => KeyCode::PageUp, + Keycode::PageDown => KeyCode::PageDown, + Keycode::MoveHome => KeyCode::Home, + Keycode::MoveEnd => KeyCode::End, + Keycode::Insert => KeyCode::Insert, + + Keycode::Del => KeyCode::Backspace, // Backspace (above Enter) + Keycode::ForwardDel => KeyCode::Delete, // Delete (below Insert) + + Keycode::Copy => KeyCode::Copy, + Keycode::Paste => KeyCode::Paste, + Keycode::Cut => KeyCode::Cut, + + Keycode::VolumeUp => KeyCode::AudioVolumeUp, + Keycode::VolumeDown => KeyCode::AudioVolumeDown, + Keycode::VolumeMute => KeyCode::AudioVolumeMute, + // Keycode::Mute => None, // Microphone mute + Keycode::MediaPlayPause => KeyCode::MediaPlayPause, + Keycode::MediaStop => KeyCode::MediaStop, + Keycode::MediaNext => KeyCode::MediaTrackNext, + Keycode::MediaPrevious => KeyCode::MediaTrackPrevious, + + Keycode::Plus => KeyCode::Equal, + Keycode::Minus => KeyCode::Minus, + // Winit doesn't differentiate both '+' and '=', considering they are usually + // on the same physical key + Keycode::Equals => KeyCode::Equal, + Keycode::Semicolon => KeyCode::Semicolon, + Keycode::Slash => KeyCode::Slash, + Keycode::Backslash => KeyCode::Backslash, + Keycode::Comma => KeyCode::Comma, + Keycode::Period => KeyCode::Period, + Keycode::Apostrophe => KeyCode::Quote, + Keycode::Grave => KeyCode::Backquote, + + // Winit doesn't expose a SysRq code, so map to PrintScreen since it's + // usually the same physical key + Keycode::Sysrq => KeyCode::PrintScreen, + // These are usually the same (Pause/Break) + Keycode::Break => KeyCode::Pause, + // These are exactly the same + Keycode::ScrollLock => KeyCode::ScrollLock, + + Keycode::Yen => KeyCode::IntlYen, + Keycode::Kana => KeyCode::Lang1, + Keycode::KatakanaHiragana => KeyCode::KanaMode, + + Keycode::CtrlLeft => KeyCode::ControlLeft, + Keycode::CtrlRight => KeyCode::ControlRight, + + Keycode::ShiftLeft => KeyCode::ShiftLeft, + Keycode::ShiftRight => KeyCode::ShiftRight, + + Keycode::AltLeft => KeyCode::AltLeft, + Keycode::AltRight => KeyCode::AltRight, + + Keycode::MetaLeft => KeyCode::SuperLeft, + Keycode::MetaRight => KeyCode::SuperRight, + + Keycode::LeftBracket => KeyCode::BracketLeft, + Keycode::RightBracket => KeyCode::BracketRight, + + Keycode::Power => KeyCode::Power, + Keycode::Sleep => KeyCode::Sleep, // what about SoftSleep? + Keycode::Wakeup => KeyCode::WakeUp, + + keycode => return PhysicalKey::Unidentified(NativeKeyCode::Android(keycode.into())), + }) +} + +/// Tries to map the `key_event` to a `KeyMapChar` containing a unicode character or dead key accent +/// +/// This takes a `KeyEvent` and looks up its corresponding `KeyCharacterMap` and +/// uses that to try and map the `key_code` + `meta_state` to a unicode +/// character or a dead key that can be combined with the next key press. +pub fn character_map_and_combine_key( + app: &AndroidApp, + key_event: &KeyEvent<'_>, + combining_accent: &mut Option, +) -> Option { + let device_id = key_event.device_id(); + + let key_map = match app.device_key_character_map(device_id) { + Ok(key_map) => key_map, + Err(err) => { + tracing::warn!("Failed to look up `KeyCharacterMap` for device {device_id}: {err:?}"); + return None; + }, + }; + + match key_map.get(key_event.key_code(), key_event.meta_state()) { + Ok(KeyMapChar::Unicode(unicode)) => { + // Only do dead key combining on key down + if key_event.action() == KeyAction::Down { + let combined_unicode = if let Some(accent) = combining_accent { + match key_map.get_dead_char(*accent, unicode) { + Ok(Some(key)) => Some(key), + Ok(None) => None, + Err(err) => { + tracing::warn!( + "KeyEvent: Failed to combine 'dead key' accent '{accent}' with \ + '{unicode}': {err:?}" + ); + None + }, + } + } else { + Some(unicode) + }; + *combining_accent = None; + combined_unicode.map(KeyMapChar::Unicode) + } else { + Some(KeyMapChar::Unicode(unicode)) + } + }, + Ok(KeyMapChar::CombiningAccent(accent)) => { + if key_event.action() == KeyAction::Down { + *combining_accent = Some(accent); + } + Some(KeyMapChar::CombiningAccent(accent)) + }, + Ok(KeyMapChar::None) => { + // Leave any combining_accent state in tact (seems to match how other + // Android apps work) + None + }, + Err(err) => { + tracing::warn!("KeyEvent: Failed to get key map character: {err:?}"); + *combining_accent = None; + None + }, + } +} + +pub fn to_logical(key_char: Option, keycode: Keycode) -> Key { + use android_activity::input::Keycode::*; + + let native = NativeKey::Android(keycode.into()); + + match key_char { + Some(KeyMapChar::Unicode(c)) => Key::Character(smol_str::SmolStr::from_iter([c])), + Some(KeyMapChar::CombiningAccent(c)) => Key::Dead(Some(c)), + None | Some(KeyMapChar::None) => match keycode { + // Using `BrowserHome` instead of `GoHome` according to + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + Home => Key::Named(NamedKey::BrowserHome), + Back => Key::Named(NamedKey::BrowserBack), + Call => Key::Named(NamedKey::Call), + Endcall => Key::Named(NamedKey::EndCall), + + //------------------------------------------------------------------------------- + // These should be redundant because they should have already been matched + // as `KeyMapChar::Unicode`, but also matched here as a fallback + Keycode0 => Key::Character("0".into()), + Keycode1 => Key::Character("1".into()), + Keycode2 => Key::Character("2".into()), + Keycode3 => Key::Character("3".into()), + Keycode4 => Key::Character("4".into()), + Keycode5 => Key::Character("5".into()), + Keycode6 => Key::Character("6".into()), + Keycode7 => Key::Character("7".into()), + Keycode8 => Key::Character("8".into()), + Keycode9 => Key::Character("9".into()), + Star => Key::Character("*".into()), + Pound => Key::Character("#".into()), + A => Key::Character("a".into()), + B => Key::Character("b".into()), + C => Key::Character("c".into()), + D => Key::Character("d".into()), + E => Key::Character("e".into()), + F => Key::Character("f".into()), + G => Key::Character("g".into()), + H => Key::Character("h".into()), + I => Key::Character("i".into()), + J => Key::Character("j".into()), + K => Key::Character("k".into()), + L => Key::Character("l".into()), + M => Key::Character("m".into()), + N => Key::Character("n".into()), + O => Key::Character("o".into()), + P => Key::Character("p".into()), + Q => Key::Character("q".into()), + R => Key::Character("r".into()), + S => Key::Character("s".into()), + T => Key::Character("t".into()), + U => Key::Character("u".into()), + V => Key::Character("v".into()), + W => Key::Character("w".into()), + X => Key::Character("x".into()), + Y => Key::Character("y".into()), + Z => Key::Character("z".into()), + Comma => Key::Character(",".into()), + Period => Key::Character(".".into()), + Grave => Key::Character("`".into()), + Minus => Key::Character("-".into()), + Equals => Key::Character("=".into()), + LeftBracket => Key::Character("[".into()), + RightBracket => Key::Character("]".into()), + Backslash => Key::Character("\\".into()), + Semicolon => Key::Character(";".into()), + Apostrophe => Key::Character("'".into()), + Slash => Key::Character("/".into()), + At => Key::Character("@".into()), + Plus => Key::Character("+".into()), + //------------------------------------------------------------------------------- + DpadUp => Key::Named(NamedKey::ArrowUp), + DpadDown => Key::Named(NamedKey::ArrowDown), + DpadLeft => Key::Named(NamedKey::ArrowLeft), + DpadRight => Key::Named(NamedKey::ArrowRight), + DpadCenter => Key::Named(NamedKey::Enter), + + VolumeUp => Key::Named(NamedKey::AudioVolumeUp), + VolumeDown => Key::Named(NamedKey::AudioVolumeDown), + Power => Key::Named(NamedKey::Power), + Camera => Key::Named(NamedKey::Camera), + Clear => Key::Named(NamedKey::Clear), + + AltLeft => Key::Named(NamedKey::Alt), + AltRight => Key::Named(NamedKey::Alt), + ShiftLeft => Key::Named(NamedKey::Shift), + ShiftRight => Key::Named(NamedKey::Shift), + Tab => Key::Named(NamedKey::Tab), + Space => Key::Named(NamedKey::Space), + Sym => Key::Named(NamedKey::Symbol), + Explorer => Key::Named(NamedKey::LaunchWebBrowser), + Envelope => Key::Named(NamedKey::LaunchMail), + Enter => Key::Named(NamedKey::Enter), + Del => Key::Named(NamedKey::Backspace), + + // According to https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_NUM + Num => Key::Named(NamedKey::Alt), + + Headsethook => Key::Named(NamedKey::HeadsetHook), + Focus => Key::Named(NamedKey::CameraFocus), + + Notification => Key::Named(NamedKey::Notification), + Search => Key::Named(NamedKey::BrowserSearch), + MediaPlayPause => Key::Named(NamedKey::MediaPlayPause), + MediaStop => Key::Named(NamedKey::MediaStop), + MediaNext => Key::Named(NamedKey::MediaTrackNext), + MediaPrevious => Key::Named(NamedKey::MediaTrackPrevious), + MediaRewind => Key::Named(NamedKey::MediaRewind), + MediaFastForward => Key::Named(NamedKey::MediaFastForward), + Mute => Key::Named(NamedKey::MicrophoneVolumeMute), + PageUp => Key::Named(NamedKey::PageUp), + PageDown => Key::Named(NamedKey::PageDown), + + Escape => Key::Named(NamedKey::Escape), + ForwardDel => Key::Named(NamedKey::Delete), + CtrlLeft => Key::Named(NamedKey::Control), + CtrlRight => Key::Named(NamedKey::Control), + CapsLock => Key::Named(NamedKey::CapsLock), + ScrollLock => Key::Named(NamedKey::ScrollLock), + MetaLeft => Key::Named(NamedKey::Super), + MetaRight => Key::Named(NamedKey::Super), + Function => Key::Named(NamedKey::Fn), + Sysrq => Key::Named(NamedKey::PrintScreen), + Break => Key::Named(NamedKey::Pause), + MoveHome => Key::Named(NamedKey::Home), + MoveEnd => Key::Named(NamedKey::End), + Insert => Key::Named(NamedKey::Insert), + Forward => Key::Named(NamedKey::BrowserForward), + MediaPlay => Key::Named(NamedKey::MediaPlay), + MediaPause => Key::Named(NamedKey::MediaPause), + MediaClose => Key::Named(NamedKey::MediaClose), + MediaEject => Key::Named(NamedKey::Eject), + MediaRecord => Key::Named(NamedKey::MediaRecord), + F1 => Key::Named(NamedKey::F1), + F2 => Key::Named(NamedKey::F2), + F3 => Key::Named(NamedKey::F3), + F4 => Key::Named(NamedKey::F4), + F5 => Key::Named(NamedKey::F5), + F6 => Key::Named(NamedKey::F6), + F7 => Key::Named(NamedKey::F7), + F8 => Key::Named(NamedKey::F8), + F9 => Key::Named(NamedKey::F9), + F10 => Key::Named(NamedKey::F10), + F11 => Key::Named(NamedKey::F11), + F12 => Key::Named(NamedKey::F12), + NumLock => Key::Named(NamedKey::NumLock), + Numpad0 => Key::Character("0".into()), + Numpad1 => Key::Character("1".into()), + Numpad2 => Key::Character("2".into()), + Numpad3 => Key::Character("3".into()), + Numpad4 => Key::Character("4".into()), + Numpad5 => Key::Character("5".into()), + Numpad6 => Key::Character("6".into()), + Numpad7 => Key::Character("7".into()), + Numpad8 => Key::Character("8".into()), + Numpad9 => Key::Character("9".into()), + NumpadDivide => Key::Character("/".into()), + NumpadMultiply => Key::Character("*".into()), + NumpadSubtract => Key::Character("-".into()), + NumpadAdd => Key::Character("+".into()), + NumpadDot => Key::Character(".".into()), + NumpadComma => Key::Character(",".into()), + NumpadEnter => Key::Named(NamedKey::Enter), + NumpadEquals => Key::Character("=".into()), + NumpadLeftParen => Key::Character("(".into()), + NumpadRightParen => Key::Character(")".into()), + + VolumeMute => Key::Named(NamedKey::AudioVolumeMute), + Info => Key::Named(NamedKey::Info), + ChannelUp => Key::Named(NamedKey::ChannelUp), + ChannelDown => Key::Named(NamedKey::ChannelDown), + ZoomIn => Key::Named(NamedKey::ZoomIn), + ZoomOut => Key::Named(NamedKey::ZoomOut), + Tv => Key::Named(NamedKey::TV), + Guide => Key::Named(NamedKey::Guide), + Dvr => Key::Named(NamedKey::DVR), + Bookmark => Key::Named(NamedKey::BrowserFavorites), + Captions => Key::Named(NamedKey::ClosedCaptionToggle), + Settings => Key::Named(NamedKey::Settings), + TvPower => Key::Named(NamedKey::TVPower), + TvInput => Key::Named(NamedKey::TVInput), + StbPower => Key::Named(NamedKey::STBPower), + StbInput => Key::Named(NamedKey::STBInput), + AvrPower => Key::Named(NamedKey::AVRPower), + AvrInput => Key::Named(NamedKey::AVRInput), + ProgRed => Key::Named(NamedKey::ColorF0Red), + ProgGreen => Key::Named(NamedKey::ColorF1Green), + ProgYellow => Key::Named(NamedKey::ColorF2Yellow), + ProgBlue => Key::Named(NamedKey::ColorF3Blue), + AppSwitch => Key::Named(NamedKey::AppSwitch), + LanguageSwitch => Key::Named(NamedKey::GroupNext), + MannerMode => Key::Named(NamedKey::MannerMode), + Keycode3dMode => Key::Named(NamedKey::TV3DMode), + Contacts => Key::Named(NamedKey::LaunchContacts), + Calendar => Key::Named(NamedKey::LaunchCalendar), + Music => Key::Named(NamedKey::LaunchMusicPlayer), + Calculator => Key::Named(NamedKey::LaunchApplication2), + ZenkakuHankaku => Key::Named(NamedKey::ZenkakuHankaku), + Eisu => Key::Named(NamedKey::Eisu), + Muhenkan => Key::Named(NamedKey::NonConvert), + Henkan => Key::Named(NamedKey::Convert), + KatakanaHiragana => Key::Named(NamedKey::HiraganaKatakana), + Kana => Key::Named(NamedKey::KanjiMode), + BrightnessDown => Key::Named(NamedKey::BrightnessDown), + BrightnessUp => Key::Named(NamedKey::BrightnessUp), + MediaAudioTrack => Key::Named(NamedKey::MediaAudioTrack), + Sleep => Key::Named(NamedKey::Standby), + Wakeup => Key::Named(NamedKey::WakeUp), + Pairing => Key::Named(NamedKey::Pairing), + MediaTopMenu => Key::Named(NamedKey::MediaTopMenu), + LastChannel => Key::Named(NamedKey::MediaLast), + TvDataService => Key::Named(NamedKey::TVDataService), + VoiceAssist => Key::Named(NamedKey::VoiceDial), + TvRadioService => Key::Named(NamedKey::TVRadioService), + TvTeletext => Key::Named(NamedKey::Teletext), + TvNumberEntry => Key::Named(NamedKey::TVNumberEntry), + TvTerrestrialAnalog => Key::Named(NamedKey::TVTerrestrialAnalog), + TvTerrestrialDigital => Key::Named(NamedKey::TVTerrestrialDigital), + TvSatellite => Key::Named(NamedKey::TVSatellite), + TvSatelliteBs => Key::Named(NamedKey::TVSatelliteBS), + TvSatelliteCs => Key::Named(NamedKey::TVSatelliteCS), + TvSatelliteService => Key::Named(NamedKey::TVSatelliteToggle), + TvNetwork => Key::Named(NamedKey::TVNetwork), + TvAntennaCable => Key::Named(NamedKey::TVAntennaCable), + TvInputHdmi1 => Key::Named(NamedKey::TVInputHDMI1), + TvInputHdmi2 => Key::Named(NamedKey::TVInputHDMI2), + TvInputHdmi3 => Key::Named(NamedKey::TVInputHDMI3), + TvInputHdmi4 => Key::Named(NamedKey::TVInputHDMI4), + TvInputComposite1 => Key::Named(NamedKey::TVInputComposite1), + TvInputComposite2 => Key::Named(NamedKey::TVInputComposite2), + TvInputComponent1 => Key::Named(NamedKey::TVInputComponent1), + TvInputComponent2 => Key::Named(NamedKey::TVInputComponent2), + TvInputVga1 => Key::Named(NamedKey::TVInputVGA1), + TvAudioDescription => Key::Named(NamedKey::TVAudioDescription), + TvAudioDescriptionMixUp => Key::Named(NamedKey::TVAudioDescriptionMixUp), + TvAudioDescriptionMixDown => Key::Named(NamedKey::TVAudioDescriptionMixDown), + TvZoomMode => Key::Named(NamedKey::ZoomToggle), + TvContentsMenu => Key::Named(NamedKey::TVContentsMenu), + TvMediaContextMenu => Key::Named(NamedKey::TVMediaContext), + TvTimerProgramming => Key::Named(NamedKey::TVTimer), + Help => Key::Named(NamedKey::Help), + NavigatePrevious => Key::Named(NamedKey::NavigatePrevious), + NavigateNext => Key::Named(NamedKey::NavigateNext), + NavigateIn => Key::Named(NamedKey::NavigateIn), + NavigateOut => Key::Named(NamedKey::NavigateOut), + MediaSkipForward => Key::Named(NamedKey::MediaSkipForward), + MediaSkipBackward => Key::Named(NamedKey::MediaSkipBackward), + MediaStepForward => Key::Named(NamedKey::MediaStepForward), + MediaStepBackward => Key::Named(NamedKey::MediaStepBackward), + Cut => Key::Named(NamedKey::Cut), + Copy => Key::Named(NamedKey::Copy), + Paste => Key::Named(NamedKey::Paste), + Refresh => Key::Named(NamedKey::BrowserRefresh), + + // ----------------------------------------------------------------- + // Keycodes that don't have a logical Key mapping + // ----------------------------------------------------------------- + Unknown => Key::Unidentified(native), + + // Can be added on demand + SoftLeft => Key::Unidentified(native), + SoftRight => Key::Unidentified(native), + + Menu => Key::Unidentified(native), + + 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), + // ----------------------------------------------------------------- + Window => Key::Unidentified(native), + + 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), + + Yen => Key::Unidentified(native), + Ro => Key::Unidentified(native), + + Assist => Key::Unidentified(native), + + Keycode11 => Key::Unidentified(native), + Keycode12 => Key::Unidentified(native), + + 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), + + SoftSleep => Key::Unidentified(native), + + SystemNavigationUp => Key::Unidentified(native), + SystemNavigationDown => Key::Unidentified(native), + SystemNavigationLeft => Key::Unidentified(native), + SystemNavigationRight => Key::Unidentified(native), + + AllApps => Key::Unidentified(native), + ThumbsUp => Key::Unidentified(native), + ThumbsDown => Key::Unidentified(native), + ProfileSwitch => Key::Unidentified(native), + + // It's always possible that new versions of Android could introduce + // key codes we can't know about at compile time. + _ => Key::Unidentified(native), + }, + } +} + +pub fn to_location(keycode: Keycode) -> KeyLocation { + use android_activity::input::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, + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/android/mod.rs b/third_party/winit-0.30.13/src/platform_impl/android/mod.rs new file mode 100644 index 0000000..c6a7416 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/android/mod.rs @@ -0,0 +1,1141 @@ +use std::cell::Cell; +use std::collections::VecDeque; +use std::hash::Hash; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::{Duration, Instant}; + +use android_activity::input::{InputEvent, KeyAction, Keycode, MotionAction}; +use android_activity::{ + AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, Rect, +}; +use tracing::{debug, trace, warn}; + +use crate::cursor::Cursor; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error; +use crate::error::EventLoopError; +use crate::event::{self, Force, InnerSizeWriter, StartCause}; +use crate::event_loop::{self, ActiveEventLoop as RootAEL, ControlFlow, DeviceEvents}; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::Fullscreen; +use crate::window::{ + self, CursorGrabMode, CustomCursor, CustomCursorSource, ImePurpose, ResizeDirection, Theme, + WindowButtons, WindowLevel, +}; + +mod keycodes; + +pub(crate) use crate::cursor::{ + NoCustomCursor as PlatformCustomCursor, NoCustomCursor as PlatformCustomCursorSource, +}; +pub(crate) use crate::icon::NoIcon as PlatformIcon; + +static HAS_FOCUS: AtomicBool = AtomicBool::new(true); + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout)))) +} + +struct PeekableReceiver { + recv: mpsc::Receiver, + first: Option, +} + +impl PeekableReceiver { + pub fn from_recv(recv: mpsc::Receiver) -> Self { + Self { recv, first: None } + } + + pub fn has_incoming(&mut self) -> bool { + if self.first.is_some() { + return true; + } + match self.recv.try_recv() { + Ok(v) => { + self.first = Some(v); + true + }, + Err(mpsc::TryRecvError::Empty) => false, + Err(mpsc::TryRecvError::Disconnected) => { + warn!("Channel was disconnected when checking incoming"); + false + }, + } + } + + pub fn try_recv(&mut self) -> Result { + if let Some(first) = self.first.take() { + return Ok(first); + } + self.recv.try_recv() + } +} + +#[derive(Clone)] +struct SharedFlagSetter { + flag: Arc, +} +impl SharedFlagSetter { + pub fn set(&self) -> bool { + self.flag.compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed).is_ok() + } +} + +struct SharedFlag { + flag: Arc, +} + +// Used for queuing redraws from arbitrary threads. We don't care how many +// times a redraw is requested (so don't actually need to queue any data, +// we just need to know at the start of a main loop iteration if a redraw +// was queued and be able to read and clear the state atomically) +impl SharedFlag { + pub fn new() -> Self { + Self { flag: Arc::new(AtomicBool::new(false)) } + } + + pub fn setter(&self) -> SharedFlagSetter { + SharedFlagSetter { flag: self.flag.clone() } + } + + pub fn get_and_reset(&self) -> bool { + self.flag.swap(false, std::sync::atomic::Ordering::AcqRel) + } +} + +#[derive(Clone)] +pub struct RedrawRequester { + flag: SharedFlagSetter, + waker: AndroidAppWaker, +} + +impl RedrawRequester { + fn new(flag: &SharedFlag, waker: AndroidAppWaker) -> Self { + RedrawRequester { flag: flag.setter(), waker } + } + + pub fn request_redraw(&self) { + if self.flag.set() { + // Only explicitly try to wake up the main loop when the flag + // value changes + self.waker.wake(); + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra {} + +pub struct EventLoop { + pub(crate) android_app: AndroidApp, + window_target: event_loop::ActiveEventLoop, + redraw_flag: SharedFlag, + user_events_sender: mpsc::Sender, + user_events_receiver: PeekableReceiver, // must wake looper whenever something gets sent + loop_running: bool, // Dispatched `NewEvents` + running: bool, + pending_redraw: bool, + cause: StartCause, + ignore_volume_keys: bool, + combining_accent: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) android_app: Option, + pub(crate) ignore_volume_keys: bool, +} + +impl Default for PlatformSpecificEventLoopAttributes { + fn default() -> Self { + Self { android_app: Default::default(), ignore_volume_keys: true } + } +} + +impl EventLoop { + pub(crate) fn new( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { + let (user_events_sender, user_events_receiver) = mpsc::channel(); + + let android_app = attributes.android_app.as_ref().expect( + "An `AndroidApp` as passed to android_main() is required to create an `EventLoop` on \ + Android", + ); + let redraw_flag = SharedFlag::new(); + + Ok(Self { + android_app: android_app.clone(), + window_target: event_loop::ActiveEventLoop { + p: ActiveEventLoop { + app: android_app.clone(), + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(false), + redraw_requester: RedrawRequester::new( + &redraw_flag, + android_app.create_waker(), + ), + }, + _marker: PhantomData, + }, + redraw_flag, + user_events_sender, + user_events_receiver: PeekableReceiver::from_recv(user_events_receiver), + loop_running: false, + running: false, + pending_redraw: false, + cause: StartCause::Init, + ignore_volume_keys: attributes.ignore_volume_keys, + combining_accent: None, + }) + } + + fn single_iteration(&mut self, main_event: Option>, callback: &mut F) + where + F: FnMut(event::Event, &RootAEL), + { + trace!("Mainloop iteration"); + + let cause = self.cause; + let mut pending_redraw = self.pending_redraw; + let mut resized = false; + + callback(event::Event::NewEvents(cause), self.window_target()); + + if let Some(event) = main_event { + trace!("Handling main event {:?}", event); + + match event { + MainEvent::InitWindow { .. } => { + callback(event::Event::Resumed, self.window_target()); + }, + MainEvent::TerminateWindow { .. } => { + callback(event::Event::Suspended, self.window_target()); + }, + MainEvent::WindowResized { .. } => resized = true, + MainEvent::RedrawNeeded { .. } => pending_redraw = true, + MainEvent::ContentRectChanged { .. } => { + warn!("TODO: find a way to notify application of content rect change"); + }, + MainEvent::GainedFocus => { + HAS_FOCUS.store(true, Ordering::Relaxed); + callback( + event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Focused(true), + }, + self.window_target(), + ); + }, + MainEvent::LostFocus => { + HAS_FOCUS.store(false, Ordering::Relaxed); + callback( + event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Focused(false), + }, + self.window_target(), + ); + }, + MainEvent::ConfigChanged { .. } => { + let monitor = MonitorHandle::new(self.android_app.clone()); + let old_scale_factor = monitor.scale_factor(); + let scale_factor = monitor.scale_factor(); + if (scale_factor - old_scale_factor).abs() < f64::EPSILON { + let new_inner_size = Arc::new(Mutex::new( + MonitorHandle::new(self.android_app.clone()).size(), + )); + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::ScaleFactorChanged { + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &new_inner_size, + )), + scale_factor, + }, + }; + callback(event, self.window_target()); + } + }, + MainEvent::LowMemory => { + callback(event::Event::MemoryWarning, self.window_target()); + }, + MainEvent::Start => { + // XXX: how to forward this state to applications? + warn!("TODO: forward onStart notification to application"); + }, + MainEvent::Resume { .. } => { + debug!("App Resumed - is running"); + self.running = true; + }, + MainEvent::SaveState { .. } => { + // XXX: how to forward this state to applications? + // XXX: also how do we expose state restoration to apps? + warn!("TODO: forward saveState notification to application"); + }, + MainEvent::Pause => { + debug!("App Paused - stopped running"); + self.running = false; + }, + MainEvent::Stop => { + // XXX: how to forward this state to applications? + warn!("TODO: forward onStop notification to application"); + }, + MainEvent::Destroy => { + // XXX: maybe exit mainloop to drop things before being + // killed by the OS? + warn!("TODO: forward onDestroy notification to application"); + }, + MainEvent::InsetsChanged { .. } => { + // XXX: how to forward this state to applications? + warn!("TODO: handle Android InsetsChanged notification"); + }, + unknown => { + trace!("Unknown MainEvent {unknown:?} (ignored)"); + }, + } + } else { + trace!("No main event to handle"); + } + + // temporarily decouple `android_app` from `self` so we aren't holding + // a borrow of `self` while iterating + let android_app = self.android_app.clone(); + + // Process input events + match android_app.input_events_iter() { + Ok(mut input_iter) => loop { + let read_event = + input_iter.next(|event| self.handle_input_event(&android_app, event, callback)); + + if !read_event { + break; + } + }, + Err(err) => { + tracing::warn!("Failed to get input events iterator: {err:?}"); + }, + } + + // Empty the user event buffer + { + while let Ok(event) = self.user_events_receiver.try_recv() { + callback(crate::event::Event::UserEvent(event), self.window_target()); + } + } + + if self.running { + if resized { + let size = if let Some(native_window) = self.android_app.native_window().as_ref() { + let width = native_window.width() as _; + let height = native_window.height() as _; + PhysicalSize::new(width, height) + } else { + PhysicalSize::new(0, 0) + }; + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Resized(size), + }; + callback(event, self.window_target()); + } + + pending_redraw |= self.redraw_flag.get_and_reset(); + if pending_redraw { + pending_redraw = false; + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::RedrawRequested, + }; + callback(event, self.window_target()); + } + } + + // This is always the last event we dispatch before poll again + callback(event::Event::AboutToWait, self.window_target()); + + self.pending_redraw = pending_redraw; + } + + fn handle_input_event( + &mut self, + android_app: &AndroidApp, + event: &InputEvent<'_>, + callback: &mut F, + ) -> InputStatus + where + F: FnMut(event::Event, &RootAEL), + { + let mut input_status = InputStatus::Handled; + match event { + InputEvent::MotionEvent(motion_event) => { + let window_id = window::WindowId(WindowId); + let device_id = event::DeviceId(DeviceId(motion_event.device_id())); + + 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), + _ => { + 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 _ }; + trace!( + "Input event {device_id:?}, {phase:?}, loc={location:?}, \ + pointer={pointer:?}" + ); + let event = event::Event::WindowEvent { + window_id, + event: event::WindowEvent::Touch(event::Touch { + device_id, + phase, + location, + id: pointer.pointer_id() as u64, + force: Some(Force::Normalized(pointer.pressure() as f64)), + }), + }; + callback(event, self.window_target()); + } + } + }, + InputEvent::KeyEvent(key) => { + match key.key_code() { + // Flag keys related to volume as unhandled. While winit does not have a way for + // applications to configure what keys to flag as handled, + // this appears to be a good default until winit + // can be configured. + Keycode::VolumeUp | Keycode::VolumeDown | Keycode::VolumeMute + if self.ignore_volume_keys => + { + input_status = InputStatus::Unhandled + }, + keycode => { + let state = match key.action() { + KeyAction::Down => event::ElementState::Pressed, + KeyAction::Up => event::ElementState::Released, + _ => event::ElementState::Released, + }; + + let key_char = keycodes::character_map_and_combine_key( + android_app, + key, + &mut self.combining_accent, + ); + + let logical_key = keycodes::to_logical(key_char, keycode); + let text = if state == event::ElementState::Pressed { + logical_key.to_text().map(smol_str::SmolStr::new) + } else { + None + }; + + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::KeyboardInput { + device_id: event::DeviceId(DeviceId(key.device_id())), + event: event::KeyEvent { + state, + physical_key: keycodes::to_physical_key(keycode), + logical_key, + location: keycodes::to_location(keycode), + repeat: key.repeat_count() > 0, + text, + platform_specific: KeyEventExtra {}, + }, + is_synthetic: false, + }, + }; + callback(event, self.window_target()); + }, + } + }, + _ => { + warn!("Unknown android_activity input event {event:?}") + }, + } + + input_status + } + + pub fn run(mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(event::Event, &event_loop::ActiveEventLoop), + { + self.run_on_demand(event_handler) + } + + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(event::Event, &event_loop::ActiveEventLoop), + { + loop { + match self.pump_events(None, &mut event_handler) { + PumpStatus::Exit(0) => { + break Ok(()); + }, + PumpStatus::Exit(code) => { + break Err(EventLoopError::ExitFailure(code)); + }, + _ => { + continue; + }, + } + } + } + + pub fn pump_events(&mut self, timeout: Option, mut callback: F) -> PumpStatus + where + F: FnMut(event::Event, &RootAEL), + { + if !self.loop_running { + self.loop_running = true; + + // Reset the internal state for the loop as we start running to + // ensure consistent behaviour in case the loop runs and exits more + // than once + self.pending_redraw = false; + self.cause = StartCause::Init; + + // run the initial loop iteration + self.single_iteration(None, &mut callback); + } + + // Consider the possibility that the `StartCause::Init` iteration could + // request to Exit + if !self.exiting() { + self.poll_events_with_timeout(timeout, &mut callback); + } + if self.exiting() { + self.loop_running = false; + + callback(event::Event::LoopExiting, self.window_target()); + + PumpStatus::Exit(0) + } else { + PumpStatus::Continue + } + } + + fn poll_events_with_timeout(&mut self, mut timeout: Option, mut callback: F) + where + F: FnMut(event::Event, &RootAEL), + { + let start = Instant::now(); + + self.pending_redraw |= self.redraw_flag.get_and_reset(); + + timeout = + if self.running && (self.pending_redraw || self.user_events_receiver.has_incoming()) { + // If we already have work to do then we don't want to block on the next poll + Some(Duration::ZERO) + } else { + let control_flow_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + Some(wait_deadline.saturating_duration_since(start)) + }, + }; + + min_timeout(control_flow_timeout, timeout) + }; + + let app = self.android_app.clone(); // Don't borrow self as part of poll expression + app.poll_events(timeout, |poll_event| { + let mut main_event = None; + + match poll_event { + android_activity::PollEvent::Wake => { + // In the X11 backend it's noted that too many false-positive wake ups + // would cause the event loop to run continuously. They handle this by + // re-checking for pending events (assuming they cover all + // valid reasons for a wake up). + // + // For now, user_events and redraw_requests are the only reasons to expect + // a wake up here so we can ignore the wake up if there are no events/requests. + // We also ignore wake ups while suspended. + self.pending_redraw |= self.redraw_flag.get_and_reset(); + if !self.running + || (!self.pending_redraw && !self.user_events_receiver.has_incoming()) + { + return; + } + }, + android_activity::PollEvent::Timeout => {}, + android_activity::PollEvent::Main(event) => { + main_event = Some(event); + }, + unknown_event => { + warn!("Unknown poll event {unknown_event:?} (ignored)"); + }, + } + + self.cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { start, requested_resume: None }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() < deadline { + StartCause::WaitCancelled { start, requested_resume: Some(deadline) } + } else { + StartCause::ResumeTimeReached { start, requested_resume: deadline } + } + }, + }; + + self.single_iteration(main_event, &mut callback); + }); + } + + pub fn window_target(&self) -> &event_loop::ActiveEventLoop { + &self.window_target + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + user_events_sender: self.user_events_sender.clone(), + waker: self.android_app.create_waker(), + } + } + + fn control_flow(&self) -> ControlFlow { + self.window_target.p.control_flow() + } + + fn exiting(&self) -> bool { + self.window_target.p.exiting() + } +} + +pub struct EventLoopProxy { + user_events_sender: mpsc::Sender, + waker: AndroidAppWaker, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { + user_events_sender: self.user_events_sender.clone(), + waker: self.waker.clone(), + } + } +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), event_loop::EventLoopClosed> { + self.user_events_sender.send(event).map_err(|err| event_loop::EventLoopClosed(err.0))?; + self.waker.wake(); + Ok(()) + } +} + +pub struct ActiveEventLoop { + pub(crate) app: AndroidApp, + control_flow: Cell, + exit: Cell, + redraw_requester: RedrawRequester, +} + +impl ActiveEventLoop { + pub fn primary_monitor(&self) -> Option { + Some(MonitorHandle::new(self.app.clone())) + } + + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> CustomCursor { + let _ = source.inner; + CustomCursor { inner: PlatformCustomCursor } + } + + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle::new(self.app.clone())); + v + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Android(rwh_05::AndroidDisplayHandle::empty()) + } + + #[inline] + pub fn system_theme(&self) -> Option { + None + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Android(rwh_06::AndroidDisplayHandle::new())) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(true) + } + + pub(crate) fn clear_exit(&self) { + self.exit.set(false) + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get() + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::AndroidDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::AndroidDisplayHandle::new().into()) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub(crate) struct WindowId; + +impl WindowId { + pub const fn dummy() -> Self { + WindowId + } +} + +impl From for u64 { + fn from(_: WindowId) -> Self { + 0 + } +} + +impl From for WindowId { + fn from(_: u64) -> Self { + Self + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct DeviceId(i32); + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId(0) + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct PlatformSpecificWindowAttributes; + +pub(crate) struct Window { + app: AndroidApp, + redraw_requester: RedrawRequester, +} + +impl Window { + pub(crate) fn new( + el: &ActiveEventLoop, + _window_attrs: window::WindowAttributes, + ) -> Result { + // FIXME this ignores requested window attributes + + Ok(Self { app: el.app.clone(), redraw_requester: el.redraw_requester.clone() }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + f(self) + } + + pub fn id(&self) -> WindowId { + WindowId + } + + pub fn primary_monitor(&self) -> Option { + Some(MonitorHandle::new(self.app.clone())) + } + + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle::new(self.app.clone())); + v + } + + pub fn current_monitor(&self) -> Option { + Some(MonitorHandle::new(self.app.clone())) + } + + pub fn scale_factor(&self) -> f64 { + MonitorHandle::new(self.app.clone()).scale_factor() + } + + pub fn request_redraw(&self) { + self.redraw_requester.request_redraw() + } + + pub fn pre_present_notify(&self) {} + + 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 request_inner_size(&self, _size: Size) -> Option> { + Some(self.inner_size()) + } + + pub fn outer_size(&self) -> PhysicalSize { + MonitorHandle::new(self.app.clone()).size() + } + + pub fn set_min_inner_size(&self, _: Option) {} + + pub fn set_max_inner_size(&self, _: Option) {} + + pub fn resize_increments(&self) -> Option> { + None + } + + pub fn set_resize_increments(&self, _increments: Option) {} + + pub fn set_title(&self, _title: &str) {} + + pub fn set_transparent(&self, _transparent: bool) {} + + pub fn set_blur(&self, _blur: bool) {} + + pub fn set_visible(&self, _visibility: bool) {} + + pub fn is_visible(&self) -> Option { + None + } + + pub fn set_resizable(&self, _resizeable: bool) {} + + pub fn is_resizable(&self) -> bool { + false + } + + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {} + + pub fn enabled_buttons(&self) -> WindowButtons { + WindowButtons::all() + } + + pub fn set_minimized(&self, _minimized: bool) {} + + pub fn is_minimized(&self) -> Option { + None + } + + pub fn set_maximized(&self, _maximized: bool) {} + + pub fn is_maximized(&self) -> bool { + 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 is_decorated(&self) -> bool { + true + } + + pub fn set_window_level(&self, _level: WindowLevel) {} + + pub fn set_window_icon(&self, _window_icon: Option) {} + + pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) {} + + pub fn set_ime_allowed(&self, allowed: bool) { + if allowed { + self.app.show_soft_input(true); + } else { + self.app.hide_soft_input(true); + } + } + + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + pub fn focus_window(&self) {} + + pub fn request_user_attention(&self, _request_type: Option) {} + + pub fn set_cursor(&self, _: Cursor) {} + + pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported(error::NotSupportedError::new())) + } + + pub fn set_cursor_grab(&self, _: CursorGrabMode) -> 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())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported(error::NotSupportedError::new())) + } + + #[cfg(feature = "rwh_04")] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + use rwh_04::HasRawWindowHandle; + + if let Some(native_window) = self.app.native_window().as_ref() { + native_window.raw_window_handle() + } 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." + ); + } + } + + #[cfg(feature = "rwh_05")] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + use rwh_05::HasRawWindowHandle; + + if let Some(native_window) = self.app.native_window().as_ref() { + native_window.raw_window_handle() + } 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." + ); + } + } + + #[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")] + // Allow the usage of HasRawWindowHandle inside this function + #[allow(deprecated)] + pub fn raw_window_handle_rwh_06(&self) -> Result { + use rwh_06::HasRawWindowHandle; + + if let Some(native_window) = self.app.native_window().as_ref() { + native_window.raw_window_handle() + } else { + tracing::error!( + "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." + ); + 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) -> ConfigurationRef { + self.app.config() + } + + pub fn content_rect(&self) -> Rect { + self.app.content_rect() + } + + pub fn set_theme(&self, _theme: Option) {} + + pub fn theme(&self) -> Option { + None + } + + pub fn set_content_protected(&self, _protected: bool) {} + + pub fn has_focus(&self) -> bool { + HAS_FOCUS.load(Ordering::Relaxed) + } + + pub fn title(&self) -> String { + String::new() + } + + pub fn reset_dead_keys(&self) {} +} + +#[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") + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct MonitorHandle { + app: AndroidApp, +} +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 { + std::cmp::Ordering::Equal + } +} + +impl MonitorHandle { + pub(crate) fn new(app: AndroidApp) -> Self { + Self { app } + } + + pub fn name(&self) -> Option { + Some("Android Device".to_owned()) + } + + pub fn size(&self) -> PhysicalSize { + if let Some(native_window) = self.app.native_window() { + PhysicalSize::new(native_window.width() as _, native_window.height() as _) + } else { + PhysicalSize::new(0, 0) + } + } + + pub fn position(&self) -> PhysicalPosition { + (0, 0).into() + } + + pub fn scale_factor(&self) -> f64 { + self.app.config().density().map(|dpi| dpi as f64 / 160.0).unwrap_or(1.0) + } + + pub fn refresh_rate_millihertz(&self) -> Option { + // FIXME no way to get real refresh rate for now. + None + } + + pub fn video_modes(&self) -> impl Iterator { + let size = self.size().into(); + // FIXME this is not the real refresh rate + // (it is guaranteed to support 32 bit color though) + std::iter::once(VideoModeHandle { + size, + bit_depth: 32, + refresh_rate_millihertz: 60000, + monitor: self.clone(), + }) + } +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VideoModeHandle { + size: (u32, u32), + bit_depth: u16, + refresh_rate_millihertz: u32, + monitor: MonitorHandle, +} + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/ios/app_state.rs b/third_party/winit-0.30.13/src/platform_impl/ios/app_state.rs new file mode 100644 index 0000000..e34bf43 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/ios/app_state.rs @@ -0,0 +1,925 @@ +#![deny(unused_results)] + +use std::cell::{RefCell, RefMut}; +use std::collections::HashSet; +use std::os::raw::c_void; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::Instant; +use std::{fmt, mem, ptr}; + +use core_foundation::base::CFRelease; +use core_foundation::date::CFAbsoluteTimeGetCurrent; +use core_foundation::runloop::{ + kCFRunLoopCommonModes, CFRunLoopAddTimer, CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate, + CFRunLoopTimerInvalidate, CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, +}; +use objc2::rc::Retained; +use objc2::runtime::AnyObject; +use objc2::{msg_send, sel}; +use objc2_foundation::{ + CGRect, CGSize, MainThreadMarker, NSInteger, NSObjectProtocol, NSOperatingSystemVersion, + NSProcessInfo, +}; +use objc2_ui_kit::{UIApplication, UICoordinateSpace, UIView, UIWindow}; + +use super::window::WinitUIWindow; +use crate::dpi::PhysicalSize; +use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow}; +use crate::window::WindowId as RootWindowId; + +macro_rules! bug { + ($($msg:tt)*) => { + panic!("winit iOS bug, file an issue: {}", format!($($msg)*)) + }; +} + +macro_rules! bug_assert { + ($test:expr, $($msg:tt)*) => { + assert!($test, "winit iOS bug, file an issue: {}", format!($($msg)*)) + }; +} + +#[derive(Debug)] +pub(crate) struct HandlePendingUserEvents; + +pub(crate) struct EventLoopHandler { + #[allow(clippy::type_complexity)] + pub(crate) handler: Box, &RootActiveEventLoop)>, + pub(crate) event_loop: RootActiveEventLoop, +} + +impl fmt::Debug for EventLoopHandler { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EventLoopHandler") + .field("handler", &"...") + .field("event_loop", &self.event_loop) + .finish() + } +} + +impl EventLoopHandler { + fn handle_event(&mut self, event: Event) { + (self.handler)(event, &self.event_loop) + } +} + +#[derive(Debug)] +pub(crate) enum EventWrapper { + StaticEvent(Event), + ScaleFactorChanged(ScaleFactorChanged), +} + +#[derive(Debug)] +pub struct ScaleFactorChanged { + pub(super) window: Retained, + pub(super) suggested_size: PhysicalSize, + pub(super) scale_factor: f64, +} + +enum UserCallbackTransitionResult<'a> { + Success { + handler: EventLoopHandler, + active_control_flow: ControlFlow, + processing_redraws: bool, + }, + ReentrancyPrevented { + queued_events: &'a mut Vec, + }, +} + +impl Event { + fn is_redraw(&self) -> bool { + matches!(self, Event::WindowEvent { event: WindowEvent::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_handler: EventLoopHandler, + queued_gpu_redraws: HashSet>, + }, + ProcessingEvents { + handler: EventLoopHandler, + 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 { + handler: EventLoopHandler, + active_control_flow: ControlFlow, + }, + Waiting { + waiting_handler: EventLoopHandler, + start: Instant, + }, + PollFinished { + waiting_handler: EventLoopHandler, + }, + Terminated, +} + +pub(crate) struct AppState { + // This should never be `None`, except for briefly during a state transition. + app_state: Option, + control_flow: ControlFlow, + waker: EventLoopWaker, +} + +impl AppState { + pub(crate) fn get_mut(_mtm: MainThreadMarker) -> 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); + + #[allow(unknown_lints)] // New lint below + #[allow(static_mut_refs)] // TODO: Use `MainThreadBound` instead. + let mut guard = unsafe { APP_STATE.borrow_mut() }; + if guard.is_none() { + #[inline(never)] + #[cold] + fn init_guard(guard: &mut RefMut<'static, Option>) { + let waker = EventLoopWaker::new(unsafe { 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 has_terminated(&self) -> bool { + matches!(self.state(), AppStateImpl::Terminated) + } + + fn will_launch_transition(&mut self, queued_handler: EventLoopHandler) { + 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_handler, + queued_gpu_redraws, + }); + } + + fn did_finish_launching_transition( + &mut self, + ) -> (Vec>, Vec) { + let (windows, events, handler, queued_gpu_redraws) = match self.take_state() { + AppStateImpl::Launching { + queued_windows, + queued_events, + queued_handler, + queued_gpu_redraws, + } => (queued_windows, queued_events, queued_handler, queued_gpu_redraws), + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::ProcessingEvents { + handler, + active_control_flow: self.control_flow, + 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() || self.has_terminated() { + return None; + } + + let (handler, event) = match (self.control_flow, self.take_state()) { + (ControlFlow::Poll, AppStateImpl::PollFinished { waiting_handler }) => { + (waiting_handler, EventWrapper::StaticEvent(Event::NewEvents(StartCause::Poll))) + }, + (ControlFlow::Wait, AppStateImpl::Waiting { waiting_handler, start }) => ( + waiting_handler, + EventWrapper::StaticEvent(Event::NewEvents(StartCause::WaitCancelled { + start, + requested_resume: None, + })), + ), + ( + ControlFlow::WaitUntil(requested_resume), + AppStateImpl::Waiting { waiting_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_handler, event) + }, + s => bug!("`EventHandler` unexpectedly woke up {:?}", s), + }; + + self.set_state(AppStateImpl::ProcessingEvents { + 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<'_>, + >(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 (handler, queued_gpu_redraws, active_control_flow, processing_redraws) = match self + .take_state() + { + AppStateImpl::Launching { .. } + | AppStateImpl::NotLaunched { .. } + | AppStateImpl::InUserCallback { .. } => unreachable!(), + AppStateImpl::ProcessingEvents { handler, queued_gpu_redraws, active_control_flow } => { + (handler, queued_gpu_redraws, active_control_flow, false) + }, + AppStateImpl::ProcessingRedraws { handler, active_control_flow } => { + (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 { handler, active_control_flow, processing_redraws } + } + + fn main_events_cleared_transition(&mut self) -> HashSet> { + let (handler, queued_gpu_redraws, active_control_flow) = match self.take_state() { + AppStateImpl::ProcessingEvents { handler, queued_gpu_redraws, active_control_flow } => { + (handler, queued_gpu_redraws, active_control_flow) + }, + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::ProcessingRedraws { handler, active_control_flow }); + queued_gpu_redraws + } + + fn events_cleared_transition(&mut self) { + if !self.has_launched() || self.has_terminated() { + return; + } + let (waiting_handler, old) = match self.take_state() { + AppStateImpl::ProcessingRedraws { handler, active_control_flow } => { + (handler, active_control_flow) + }, + s => bug!("unexpected state {:?}", s), + }; + + let new = self.control_flow; + match (old, new) { + (ControlFlow::Wait, ControlFlow::Wait) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { waiting_handler, start }); + self.waker.stop() + }, + (ControlFlow::WaitUntil(old_instant), ControlFlow::WaitUntil(new_instant)) + if old_instant == new_instant => + { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { waiting_handler, start }); + }, + (_, ControlFlow::Wait) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { waiting_handler, start }); + self.waker.stop() + }, + (_, ControlFlow::WaitUntil(new_instant)) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { waiting_handler, start }); + self.waker.start_at(new_instant) + }, + // Unlike on macOS, handle Poll to Poll transition here to call the waker + (_, ControlFlow::Poll) => { + self.set_state(AppStateImpl::PollFinished { waiting_handler }); + self.waker.start() + }, + } + } + + fn terminated_transition(&mut self) -> EventLoopHandler { + match self.replace_state(AppStateImpl::Terminated) { + AppStateImpl::ProcessingEvents { handler, .. } => handler, + s => bug!("`LoopExiting` happened while not processing events {:?}", s), + } + } + + pub(crate) fn set_control_flow(&mut self, control_flow: ControlFlow) { + self.control_flow = control_flow; + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow + } +} + +pub(crate) fn set_key_window(mtm: MainThreadMarker, window: &Retained) { + let mut this = AppState::get_mut(mtm); + match this.state_mut() { + &mut AppStateImpl::NotLaunched { ref mut queued_windows, .. } => { + return queued_windows.push(window.clone()) + }, + &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); + window.makeKeyAndVisible(); +} + +pub(crate) fn queue_gl_or_metal_redraw(mtm: MainThreadMarker, window: Retained) { + let mut this = AppState::get_mut(mtm); + 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, .. } => { + let _ = 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") + }, + } +} + +pub(crate) fn will_launch(mtm: MainThreadMarker, queued_handler: EventLoopHandler) { + AppState::get_mut(mtm).will_launch_transition(queued_handler) +} + +pub fn did_finish_launching(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + let windows = match this.state_mut() { + AppStateImpl::Launching { queued_windows, .. } => mem::take(queued_windows), + s => bug!("unexpected state {:?}", s), + }; + + this.waker.start(); + + // have to drop RefMut because the window setup code below can trigger new events + drop(this); + + for window in windows { + // 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 initialization + // completed. This may result in incorrect visual appearance. + // ``` + let screen = window.screen(); + let _: () = unsafe { msg_send![&window, setScreen: ptr::null::()] }; + window.setScreen(&screen); + + let controller = window.rootViewController(); + window.setRootViewController(None); + window.setRootViewController(controller.as_deref()); + + window.makeKeyAndVisible(); + } + + let (windows, events) = AppState::get_mut(mtm).did_finish_launching_transition(); + + let events = std::iter::once(EventWrapper::StaticEvent(Event::NewEvents(StartCause::Init))) + .chain(events); + handle_nonuser_events(mtm, 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 { + window.makeKeyAndVisible(); + } +} + +// AppState::did_finish_launching handles the special transition `Init` +pub fn handle_wakeup_transition(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + let wakeup_event = match this.wakeup_transition() { + None => return, + Some(wakeup_event) => wakeup_event, + }; + drop(this); + + handle_nonuser_event(mtm, wakeup_event) +} + +pub(crate) fn handle_nonuser_event(mtm: MainThreadMarker, event: EventWrapper) { + handle_nonuser_events(mtm, std::iter::once(event)) +} + +pub(crate) fn handle_nonuser_events>( + mtm: MainThreadMarker, + events: I, +) { + let mut this = AppState::get_mut(mtm); + if this.has_terminated() { + return; + } + + let (mut handler, active_control_flow, processing_redraws) = + match this.try_user_callback_transition() { + UserCallbackTransitionResult::ReentrancyPrevented { queued_events } => { + queued_events.extend(events); + return; + }, + UserCallbackTransitionResult::Success { + handler, + active_control_flow, + processing_redraws, + } => (handler, active_control_flow, processing_redraws), + }; + drop(this); + + for wrapper in events { + match wrapper { + EventWrapper::StaticEvent(event) => { + if !processing_redraws && event.is_redraw() { + tracing::info!("processing `RedrawRequested` during the main event loop"); + } else if processing_redraws && !event.is_redraw() { + tracing::warn!( + "processing non `RedrawRequested` event after the main event loop: {:#?}", + event + ); + } + handler.handle_event(event) + }, + EventWrapper::ScaleFactorChanged(event) => handle_hidpi_proxy(&mut handler, event), + } + } + + loop { + let mut this = AppState::get_mut(mtm); + 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 { handler, active_control_flow } + } else { + AppStateImpl::ProcessingEvents { handler, queued_gpu_redraws, active_control_flow } + }); + break; + } + drop(this); + + for wrapper in queued_events { + match wrapper { + EventWrapper::StaticEvent(event) => { + if !processing_redraws && event.is_redraw() { + tracing::info!("processing `RedrawRequested` during the main event loop"); + } else if processing_redraws && !event.is_redraw() { + tracing::warn!( + "processing non-`RedrawRequested` event after the main event loop: \ + {:#?}", + event + ); + } + handler.handle_event(event) + }, + EventWrapper::ScaleFactorChanged(event) => handle_hidpi_proxy(&mut handler, event), + } + } + } +} + +fn handle_user_events(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + let (mut handler, active_control_flow, processing_redraws) = + match this.try_user_callback_transition() { + UserCallbackTransitionResult::ReentrancyPrevented { .. } => { + bug!("unexpected attempted to process an event") + }, + UserCallbackTransitionResult::Success { + handler, + active_control_flow, + processing_redraws, + } => (handler, active_control_flow, processing_redraws), + }; + if processing_redraws { + bug!("user events attempted to be sent out while `ProcessingRedraws`"); + } + drop(this); + + handler.handle_event(Event::UserEvent(HandlePendingUserEvents)); + + loop { + let mut this = AppState::get_mut(mtm); + 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 { + handler, + queued_gpu_redraws, + active_control_flow, + }); + break; + } + drop(this); + + for wrapper in queued_events { + match wrapper { + EventWrapper::StaticEvent(event) => handler.handle_event(event), + EventWrapper::ScaleFactorChanged(event) => handle_hidpi_proxy(&mut handler, event), + } + } + + handler.handle_event(Event::UserEvent(HandlePendingUserEvents)); + } +} + +pub(crate) fn send_occluded_event_for_all_windows(application: &UIApplication, occluded: bool) { + let mtm = MainThreadMarker::from(application); + + let mut events = Vec::new(); + #[allow(deprecated)] + for window in application.windows().iter() { + if window.is_kind_of::() { + // SAFETY: We just checked that the window is a `winit` window + let window = unsafe { + let ptr: *const UIWindow = window; + let ptr: *const WinitUIWindow = ptr.cast(); + &*ptr + }; + events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Occluded(occluded), + })); + } + } + handle_nonuser_events(mtm, events); +} + +pub fn handle_main_events_cleared(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + if !this.has_launched() || this.has_terminated() { + return; + } + match this.state_mut() { + AppStateImpl::ProcessingEvents { .. } => {}, + _ => bug!("`ProcessingRedraws` happened unexpectedly"), + }; + drop(this); + + handle_user_events(mtm); + + let mut this = AppState::get_mut(mtm); + let redraw_events: Vec = this + .main_events_cleared_transition() + .into_iter() + .map(|window| { + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::RedrawRequested, + }) + }) + .collect(); + drop(this); + + handle_nonuser_events(mtm, redraw_events); + handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::AboutToWait)); +} + +pub fn handle_events_cleared(mtm: MainThreadMarker) { + AppState::get_mut(mtm).events_cleared_transition(); +} + +pub(crate) fn terminated(application: &UIApplication) { + let mtm = MainThreadMarker::from(application); + + let mut events = Vec::new(); + #[allow(deprecated)] + for window in application.windows().iter() { + if window.is_kind_of::() { + // SAFETY: We just checked that the window is a `winit` window + let window = unsafe { + let ptr: *const UIWindow = window; + let ptr: *const WinitUIWindow = ptr.cast(); + &*ptr + }; + events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Destroyed, + })); + } + } + handle_nonuser_events(mtm, events); + + let mut this = AppState::get_mut(mtm); + let mut handler = this.terminated_transition(); + drop(this); + + handler.handle_event(Event::LoopExiting) +} + +fn handle_hidpi_proxy(handler: &mut EventLoopHandler, event: ScaleFactorChanged) { + let ScaleFactorChanged { suggested_size, scale_factor, window } = event; + let new_inner_size = Arc::new(Mutex::new(suggested_size)); + let event = Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)), + }, + }; + handler.handle_event(event); + let (view, screen_frame) = get_view_and_screen_frame(&window); + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + let logical_size = physical_size.to_logical(scale_factor); + let size = CGSize::new(logical_size.width, logical_size.height); + let new_frame: CGRect = CGRect::new(screen_frame.origin, size); + view.setFrame(new_frame); +} + +fn get_view_and_screen_frame(window: &WinitUIWindow) -> (Retained, CGRect) { + let view_controller = window.rootViewController().unwrap(); + let view = view_controller.view().unwrap(); + let bounds = window.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = window.convertRect_toCoordinateSpace(bounds, &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 OSCapabilities { + fn from_os_version(os_version: NSOperatingSystemVersion) -> Self { + $(let $name = meets_requirements(os_version, $major, $minor);)* + Self { $($name,)* os_version, } + } + } + + impl OSCapabilities {$( + $(#[$attr])* + pub fn $error_name(&self, extra_msg: &str) { + tracing::warn!( + concat!("`", $objc_call, "` requires iOS {}.{}+. This device is running iOS {}.{}.{}. {}"), + $major, $minor, self.os_version.majorVersion, self.os_version.minorVersion, self.os_version.patchVersion, + extra_msg + ) + } + )*} + }; +} + +os_capabilities! { + /// + #[allow(unused)] // error message unused + safe_area_err_msg: "-[UIView safeAreaInsets]", + safe_area: 11-0, + /// + home_indicator_hidden_err_msg: "-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]", + home_indicator_hidden: 11-0, + /// + defer_system_gestures_err_msg: "-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystem]", + defer_system_gestures: 11-0, + /// + maximum_frames_per_second_err_msg: "-[UIScreen maximumFramesPerSecond]", + maximum_frames_per_second: 10-3, + /// + #[allow(unused)] // error message unused + force_touch_err_msg: "-[UITouch force]", + force_touch: 9-0, +} + +fn meets_requirements( + version: NSOperatingSystemVersion, + required_major: NSInteger, + required_minor: NSInteger, +) -> bool { + (version.majorVersion, version.minorVersion) >= (required_major, required_minor) +} + +fn get_version() -> NSOperatingSystemVersion { + let process_info = NSProcessInfo::processInfo(); + let atleast_ios_8 = process_info.respondsToSelector(sel!(operatingSystemVersion)); + // Winit 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, "`winit` requires iOS version 8 or greater"); + process_info.operatingSystemVersion() +} + +pub fn os_capabilities() -> OSCapabilities { + // Cache the version lookup for efficiency + static OS_CAPABILITIES: OnceLock = OnceLock::new(); + OS_CAPABILITIES.get_or_init(|| OSCapabilities::from_os_version(get_version())).clone() +} diff --git a/third_party/winit-0.30.13/src/platform_impl/ios/event_loop.rs b/third_party/winit-0.30.13/src/platform_impl/ios/event_loop.rs new file mode 100644 index 0000000..a093b7b --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/ios/event_loop.rs @@ -0,0 +1,494 @@ +use std::collections::VecDeque; +use std::ffi::{c_char, c_int, c_void}; +use std::marker::PhantomData; +use std::ptr::{self, NonNull}; +use std::sync::mpsc::{self, Receiver, Sender}; + +use core_foundation::base::{CFIndex, CFRelease}; +use core_foundation::runloop::{ + kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, kCFRunLoopDefaultMode, + kCFRunLoopExit, CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddSource, CFRunLoopGetMain, + CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopSourceContext, CFRunLoopSourceCreate, + CFRunLoopSourceInvalidate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, +}; +use objc2::rc::Retained; +use objc2::{msg_send_id, ClassType}; +use objc2_foundation::{MainThreadMarker, NSNotificationCenter, NSObject}; +use objc2_ui_kit::{ + UIApplication, UIApplicationDidBecomeActiveNotification, + UIApplicationDidEnterBackgroundNotification, UIApplicationDidFinishLaunchingNotification, + UIApplicationDidReceiveMemoryWarningNotification, UIApplicationMain, + UIApplicationWillEnterForegroundNotification, UIApplicationWillResignActiveNotification, + UIApplicationWillTerminateNotification, UIDevice, UIScreen, UIUserInterfaceIdiom, +}; + +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::{ + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, EventLoopClosed, +}; +use crate::platform::ios::Idiom; +use crate::platform_impl::ios::app_state::{EventLoopHandler, HandlePendingUserEvents}; +use crate::window::{CustomCursor, CustomCursorSource, Theme}; + +use super::app_state::{send_occluded_event_for_all_windows, AppState, EventWrapper}; +use super::notification_center::create_observer; +use super::{app_state, monitor, MonitorHandle}; + +#[derive(Debug)] +pub struct ActiveEventLoop { + pub(super) mtm: MainThreadMarker, +} + +impl ActiveEventLoop { + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> CustomCursor { + let _ = source.inner; + CustomCursor { inner: super::PlatformCustomCursor } + } + + pub fn available_monitors(&self) -> VecDeque { + monitor::uiscreens(self.mtm) + } + + pub fn primary_monitor(&self) -> Option { + #[allow(deprecated)] + Some(MonitorHandle::new(UIScreen::mainScreen(self.mtm))) + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::UiKit(rwh_05::UiKitDisplayHandle::empty()) + } + + #[inline] + pub fn system_theme(&self) -> Option { + None + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::UiKit(rwh_06::UiKitDisplayHandle::new())) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + AppState::get_mut(self.mtm).set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + AppState::get_mut(self.mtm).control_flow() + } + + pub(crate) fn exit(&self) { + // https://developer.apple.com/library/archive/qa/qa1561/_index.html + // it is not possible to quit an iOS app gracefully and programmatically + tracing::warn!("`ControlFlow::Exit` ignored on iOS"); + } + + pub(crate) fn exiting(&self) -> bool { + false + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::UiKitDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::UiKitDisplayHandle::new().into()) + } +} + +fn map_user_event( + mut handler: impl FnMut(Event, &RootActiveEventLoop), + receiver: mpsc::Receiver, +) -> impl FnMut(Event, &RootActiveEventLoop) { + move |event, window_target| match event.map_nonuser_event() { + Ok(event) => (handler)(event, window_target), + Err(_) => { + for event in receiver.try_iter() { + (handler)(Event::UserEvent(event), window_target); + } + }, + } +} + +pub struct EventLoop { + mtm: MainThreadMarker, + sender: Sender, + receiver: Receiver, + window_target: RootActiveEventLoop, + + // Since iOS 9.0, we no longer need to remove the observers before they are deallocated; the + // system instead cleans it up next time it would have posted a notification to it. + // + // Though we do still need to keep the observers around to prevent them from being deallocated. + _did_finish_launching_observer: Retained, + _did_become_active_observer: Retained, + _will_resign_active_observer: Retained, + _will_enter_foreground_observer: Retained, + _did_enter_background_observer: Retained, + _will_terminate_observer: Retained, + _did_receive_memory_warning_observer: Retained, +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +impl EventLoop { + pub(crate) fn new( + _: &PlatformSpecificEventLoopAttributes, + ) -> Result, EventLoopError> { + let mtm = MainThreadMarker::new() + .expect("On iOS, `EventLoop` must be created on the main thread"); + + static mut SINGLETON_INIT: bool = false; + unsafe { + assert!( + !SINGLETON_INIT, + "Only one `EventLoop` is supported on iOS. `EventLoopProxy` might be helpful" + ); + SINGLETON_INIT = true; + } + + let (sender, receiver) = mpsc::channel(); + + // this line sets up the main run loop before `UIApplicationMain` + setup_control_flow_observers(); + + let center = unsafe { NSNotificationCenter::defaultCenter() }; + + let _did_finish_launching_observer = create_observer( + ¢er, + // `application:didFinishLaunchingWithOptions:` + unsafe { UIApplicationDidFinishLaunchingNotification }, + move |_| { + app_state::did_finish_launching(mtm); + }, + ); + let _did_become_active_observer = create_observer( + ¢er, + // `applicationDidBecomeActive:` + unsafe { UIApplicationDidBecomeActiveNotification }, + move |_| { + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Resumed)); + }, + ); + let _will_resign_active_observer = create_observer( + ¢er, + // `applicationWillResignActive:` + unsafe { UIApplicationWillResignActiveNotification }, + move |_| { + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Suspended)); + }, + ); + let _will_enter_foreground_observer = create_observer( + ¢er, + // `applicationWillEnterForeground:` + unsafe { UIApplicationWillEnterForegroundNotification }, + move |notification| { + let app = unsafe { notification.object() }.expect( + "UIApplicationWillEnterForegroundNotification to have application object", + ); + // SAFETY: The `object` in `UIApplicationWillEnterForegroundNotification` is + // documented to be `UIApplication`. + let app: Retained = unsafe { Retained::cast(app) }; + send_occluded_event_for_all_windows(&app, false); + }, + ); + let _did_enter_background_observer = create_observer( + ¢er, + // `applicationDidEnterBackground:` + unsafe { UIApplicationDidEnterBackgroundNotification }, + move |notification| { + let app = unsafe { notification.object() }.expect( + "UIApplicationDidEnterBackgroundNotification to have application object", + ); + // SAFETY: The `object` in `UIApplicationDidEnterBackgroundNotification` is + // documented to be `UIApplication`. + let app: Retained = unsafe { Retained::cast(app) }; + send_occluded_event_for_all_windows(&app, true); + }, + ); + let _will_terminate_observer = create_observer( + ¢er, + // `applicationWillTerminate:` + unsafe { UIApplicationWillTerminateNotification }, + move |notification| { + let app = unsafe { notification.object() } + .expect("UIApplicationWillTerminateNotification to have application object"); + // SAFETY: The `object` in `UIApplicationWillTerminateNotification` is + // (somewhat) documented to be `UIApplication`. + let app: Retained = unsafe { Retained::cast(app) }; + app_state::terminated(&app); + }, + ); + let _did_receive_memory_warning_observer = create_observer( + ¢er, + // `applicationDidReceiveMemoryWarning:` + unsafe { UIApplicationDidReceiveMemoryWarningNotification }, + move |_| { + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::MemoryWarning), + ); + }, + ); + + Ok(EventLoop { + mtm, + sender, + receiver, + window_target: RootActiveEventLoop { p: ActiveEventLoop { mtm }, _marker: PhantomData }, + _did_finish_launching_observer, + _did_become_active_observer, + _will_resign_active_observer, + _will_enter_foreground_observer, + _did_enter_background_observer, + _will_terminate_observer, + _did_receive_memory_warning_observer, + }) + } + + pub fn run(self, handler: F) -> ! + where + F: FnMut(Event, &RootActiveEventLoop), + { + let application: Option> = + unsafe { msg_send_id![UIApplication::class(), sharedApplication] }; + assert!( + application.is_none(), + "\ + `EventLoop` cannot be `run` after a call to `UIApplicationMain` on iOS\nNote: \ + `EventLoop::run_app` calls `UIApplicationMain` on iOS", + ); + + let handler = map_user_event(handler, self.receiver); + + let handler = unsafe { + std::mem::transmute::< + Box, &RootActiveEventLoop)>, + Box, &RootActiveEventLoop)>, + >(Box::new(handler)) + }; + + let handler = EventLoopHandler { handler, event_loop: self.window_target }; + + app_state::will_launch(self.mtm, handler); + + extern "C" { + // These functions are in crt_externs.h. + fn _NSGetArgc() -> *mut c_int; + fn _NSGetArgv() -> *mut *mut *mut c_char; + } + + unsafe { + UIApplicationMain( + *_NSGetArgc(), + NonNull::new(*_NSGetArgv()).unwrap(), + // We intentionally override neither the application nor the delegate, to allow the + // user to do so themselves! + None, + None, + ) + }; + unreachable!() + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.sender.clone()) + } + + pub fn window_target(&self) -> &RootActiveEventLoop { + &self.window_target + } +} + +// EventLoopExtIOS +impl EventLoop { + pub fn idiom(&self) -> Idiom { + match UIDevice::currentDevice(self.mtm).userInterfaceIdiom() { + UIUserInterfaceIdiom::Unspecified => Idiom::Unspecified, + UIUserInterfaceIdiom::Phone => Idiom::Phone, + UIUserInterfaceIdiom::Pad => Idiom::Pad, + UIUserInterfaceIdiom::TV => Idiom::TV, + UIUserInterfaceIdiom::CarPlay => Idiom::CarPlay, + _ => Idiom::Unspecified, + } + } +} + +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(_: *const 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 { + version: 0, + info: ptr::null_mut(), + retain: None, + release: None, + copyDescription: None, + equal: None, + hash: None, + schedule: None, + cancel: None, + perform: 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(|::std::sync::mpsc::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, + ) { + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopAfterWaiting => app_state::handle_wakeup_transition(mtm), + _ => 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 AboutToWait 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 `AboutToWait`. + // + // 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, + ) { + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => app_state::handle_main_events_cleared(mtm), + kCFRunLoopExit => {}, // may happen when running on macOS + _ => 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, + ) { + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => app_state::handle_events_cleared(mtm), + kCFRunLoopExit => {}, // may happen when running on macOS + _ => unreachable!(), + } + } + + let main_loop = CFRunLoopGetMain(); + + let begin_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + 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); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/ios/mod.rs b/third_party/winit-0.30.13/src/platform_impl/ios/mod.rs new file mode 100644 index 0000000..69e79c9 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/ios/mod.rs @@ -0,0 +1,52 @@ +#![allow(clippy::let_unit_value)] + +mod app_state; +mod event_loop; +mod monitor; +mod notification_center; +mod view; +mod view_controller; +mod window; + +use std::fmt; + +use crate::event::DeviceId as RootDeviceId; + +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub(crate) use self::monitor::{MonitorHandle, VideoModeHandle}; +pub(crate) use self::window::{PlatformSpecificWindowAttributes, Window, WindowId}; +pub(crate) use crate::cursor::{ + NoCustomCursor as PlatformCustomCursor, NoCustomCursor as PlatformCustomCursorSource, +}; +pub(crate) use crate::icon::NoIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; + +/// There is no way to detect which device that performed a certain event in +/// UIKit (i.e. you can't differentiate between different external keyboards, +/// or whether it was the main touchscreen, assistive technologies, or some +/// other pointer device that caused a touch event). +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId; + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId + } +} + +pub(crate) const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyEventExtra {} + +#[derive(Debug)] +pub enum OsError {} + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "os error") + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/ios/monitor.rs b/third_party/winit-0.30.13/src/platform_impl/ios/monitor.rs new file mode 100644 index 0000000..9f017a2 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/ios/monitor.rs @@ -0,0 +1,277 @@ +#![allow(clippy::unnecessary_cast)] + +use std::collections::{BTreeSet, VecDeque}; +use std::{fmt, hash, ptr}; + +use objc2::mutability::IsRetainable; +use objc2::rc::Retained; +use objc2::Message; +use objc2_foundation::{run_on_main, MainThreadBound, MainThreadMarker, NSInteger}; +use objc2_ui_kit::{UIScreen, UIScreenMode}; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::monitor::VideoModeHandle as RootVideoModeHandle; +use crate::platform_impl::platform::app_state; + +// Workaround for `MainThreadBound` implementing almost no traits +#[derive(Debug)] +struct MainThreadBoundDelegateImpls(MainThreadBound>); + +impl Clone for MainThreadBoundDelegateImpls { + fn clone(&self) -> Self { + Self(run_on_main(|mtm| MainThreadBound::new(Retained::clone(self.0.get(mtm)), mtm))) + } +} + +impl hash::Hash for MainThreadBoundDelegateImpls { + fn hash(&self, state: &mut H) { + // SAFETY: Marker only used to get the pointer + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.0.get(mtm)).hash(state); + } +} + +impl PartialEq for MainThreadBoundDelegateImpls { + fn eq(&self, other: &Self) -> bool { + // SAFETY: Marker only used to get the pointer + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.0.get(mtm)) == Retained::as_ptr(other.0.get(mtm)) + } +} + +impl Eq for MainThreadBoundDelegateImpls {} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct VideoModeHandle { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + screen_mode: MainThreadBoundDelegateImpls, + pub(crate) monitor: MonitorHandle, +} + +impl VideoModeHandle { + fn new( + uiscreen: Retained, + screen_mode: Retained, + mtm: MainThreadMarker, + ) -> VideoModeHandle { + let refresh_rate_millihertz = refresh_rate_millihertz(&uiscreen); + let size = screen_mode.size(); + VideoModeHandle { + size: (size.width as u32, size.height as u32), + bit_depth: 32, + refresh_rate_millihertz, + screen_mode: MainThreadBoundDelegateImpls(MainThreadBound::new(screen_mode, mtm)), + monitor: MonitorHandle::new(uiscreen), + } + } + + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } + + pub(super) fn screen_mode(&self, mtm: MainThreadMarker) -> &Retained { + self.screen_mode.0.get(mtm) + } +} + +pub struct MonitorHandle { + ui_screen: MainThreadBound>, +} + +impl Clone for MonitorHandle { + fn clone(&self) -> Self { + run_on_main(|mtm| Self { + ui_screen: MainThreadBound::new(self.ui_screen.get(mtm).clone(), mtm), + }) + } +} + +impl hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + // SAFETY: Only getting the pointer. + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.ui_screen.get(mtm)).hash(state); + } +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + // SAFETY: Only getting the pointer. + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + ptr::eq( + Retained::as_ptr(self.ui_screen.get(mtm)), + Retained::as_ptr(other.ui_screen.get(mtm)), + ) + } +} + +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 { + // SAFETY: Only getting the pointer. + // TODO: Make a better ordering + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.ui_screen.get(mtm)).cmp(&Retained::as_ptr(other.ui_screen.get(mtm))) + } +} + +impl fmt::Debug for MonitorHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MonitorHandle") + .field("name", &self.name()) + .field("size", &self.size()) + .field("position", &self.position()) + .field("scale_factor", &self.scale_factor()) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz()) + .finish_non_exhaustive() + } +} + +impl MonitorHandle { + pub(crate) fn new(ui_screen: Retained) -> Self { + // Holding `Retained` implies we're on the main thread. + let mtm = MainThreadMarker::new().unwrap(); + Self { ui_screen: MainThreadBound::new(ui_screen, mtm) } + } + + pub fn name(&self) -> Option { + run_on_main(|mtm| { + #[allow(deprecated)] + let main = UIScreen::mainScreen(mtm); + if *self.ui_screen(mtm) == main { + Some("Primary".to_string()) + } else if Some(self.ui_screen(mtm)) == main.mirroredScreen().as_ref() { + Some("Mirrored".to_string()) + } else { + #[allow(deprecated)] + UIScreen::screens(mtm) + .iter() + .position(|rhs| rhs == &**self.ui_screen(mtm)) + .map(|idx| idx.to_string()) + } + }) + } + + pub fn size(&self) -> PhysicalSize { + let bounds = self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeBounds()); + PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32) + } + + pub fn position(&self) -> PhysicalPosition { + let bounds = self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeBounds()); + (bounds.origin.x as f64, bounds.origin.y as f64).into() + } + + pub fn scale_factor(&self) -> f64 { + self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeScale()) as f64 + } + + pub fn refresh_rate_millihertz(&self) -> Option { + Some(self.ui_screen.get_on_main(|ui_screen| refresh_rate_millihertz(ui_screen))) + } + + pub fn video_modes(&self) -> impl Iterator { + run_on_main(|mtm| { + let ui_screen = self.ui_screen(mtm); + // Use Ord impl of RootVideoModeHandle + + let modes: BTreeSet<_> = ui_screen + .availableModes() + .into_iter() + .map(|mode| RootVideoModeHandle { + video_mode: VideoModeHandle::new(ui_screen.clone(), mode, mtm), + }) + .collect(); + + modes.into_iter().map(|mode| mode.video_mode) + }) + } + + pub(crate) fn ui_screen(&self, mtm: MainThreadMarker) -> &Retained { + self.ui_screen.get(mtm) + } + + pub fn preferred_video_mode(&self) -> VideoModeHandle { + run_on_main(|mtm| { + VideoModeHandle::new( + self.ui_screen(mtm).clone(), + self.ui_screen(mtm).preferredMode().unwrap(), + mtm, + ) + }) + } +} + +fn refresh_rate_millihertz(uiscreen: &UIScreen) -> u32 { + let refresh_rate_millihertz: NSInteger = { + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.maximum_frames_per_second { + 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 + } + }; + + refresh_rate_millihertz as u32 * 1000 +} + +pub fn uiscreens(mtm: MainThreadMarker) -> VecDeque { + #[allow(deprecated)] + UIScreen::screens(mtm).into_iter().map(MonitorHandle::new).collect() +} + +#[cfg(test)] +mod tests { + use objc2_foundation::NSSet; + + use super::*; + + // Test that UIScreen pointer comparisons are correct. + #[test] + #[allow(deprecated)] + fn screen_comparisons() { + // Test code, doesn't matter that it's not thread safe + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + + assert!(ptr::eq(&*UIScreen::mainScreen(mtm), &*UIScreen::mainScreen(mtm))); + + let main = UIScreen::mainScreen(mtm); + assert!(UIScreen::screens(mtm).iter().any(|screen| ptr::eq(screen, &*main))); + + assert!(unsafe { + NSSet::setWithArray(&UIScreen::screens(mtm)).containsObject(&UIScreen::mainScreen(mtm)) + }); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/ios/notification_center.rs b/third_party/winit-0.30.13/src/platform_impl/ios/notification_center.rs new file mode 100644 index 0000000..652bf1d --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/ios/notification_center.rs @@ -0,0 +1,27 @@ +use std::ptr::NonNull; + +use block2::RcBlock; +use objc2::rc::Retained; +use objc2_foundation::{NSNotification, NSNotificationCenter, NSNotificationName, NSObject}; + +/// Observe the given notification. +/// +/// This is used in Winit as an alternative to declaring an application delegate, as we want to +/// give the user full control over those. +pub fn create_observer( + center: &NSNotificationCenter, + name: &NSNotificationName, + handler: impl Fn(&NSNotification) + 'static, +) -> Retained { + let block = RcBlock::new(move |notification: NonNull| { + handler(unsafe { notification.as_ref() }); + }); + unsafe { + center.addObserverForName_object_queue_usingBlock( + Some(name), + None, // No sender filter + None, // No queue, run on posting thread (i.e. main thread) + &block, + ) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/ios/view.rs b/third_party/winit-0.30.13/src/platform_impl/ios/view.rs new file mode 100644 index 0000000..418968c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/ios/view.rs @@ -0,0 +1,607 @@ +#![allow(clippy::unnecessary_cast)] +use std::cell::{Cell, RefCell}; + +use objc2::rc::Retained; +use objc2::runtime::{NSObjectProtocol, ProtocolObject}; +use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass}; +use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet, NSString}; +use objc2_ui_kit::{ + UICoordinateSpace, UIEvent, UIForceTouchCapability, UIGestureRecognizer, + UIGestureRecognizerDelegate, UIGestureRecognizerState, UIKeyInput, UIPanGestureRecognizer, + UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, + UITextInputTraits, UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView, +}; + +use super::app_state::{self, EventWrapper}; +use super::window::WinitUIWindow; +use crate::dpi::PhysicalPosition; +use crate::event::{ElementState, Event, Force, KeyEvent, Touch, TouchPhase, WindowEvent}; +use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKeyCode, PhysicalKey}; +use crate::platform_impl::platform::DEVICE_ID; +use crate::platform_impl::KeyEventExtra; +use crate::window::{WindowAttributes, WindowId as RootWindowId}; + +pub struct WinitViewState { + pinch_gesture_recognizer: RefCell>>, + doubletap_gesture_recognizer: RefCell>>, + rotation_gesture_recognizer: RefCell>>, + pan_gesture_recognizer: RefCell>>, + + // for iOS delta references the start of the Gesture + rotation_last_delta: Cell, + pinch_last_delta: Cell, + pan_last_delta: Cell, +} + +declare_class!( + pub(crate) struct WinitView; + + unsafe impl ClassType for WinitView { + #[inherits(UIResponder, NSObject)] + type Super = UIView; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitUIView"; + } + + impl DeclaredClass for WinitView { + type Ivars = WinitViewState; + } + + unsafe impl WinitView { + #[method(drawRect:)] + fn draw_rect(&self, rect: CGRect) { + let mtm = MainThreadMarker::new().unwrap(); + let window = self.window().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::RedrawRequested, + }), + ); + let _: () = unsafe { msg_send![super(self), drawRect: rect] }; + } + + #[method(layoutSubviews)] + fn layout_subviews(&self) { + let mtm = MainThreadMarker::new().unwrap(); + let _: () = unsafe { msg_send![super(self), layoutSubviews] }; + + let window = self.window().unwrap(); + let window_bounds = window.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = self.convertRect_toCoordinateSpace(window_bounds, &screen_space); + let scale_factor = 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 as f64); + + // 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 = self.frame(); + if view_frame != window_bounds { + self.setFrame(window_bounds); + } + + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Resized(size), + }), + ); + } + + #[method(setContentScaleFactor:)] + fn set_content_scale_factor(&self, untrusted_scale_factor: CGFloat) { + let mtm = MainThreadMarker::new().unwrap(); + let _: () = + unsafe { msg_send![super(self), setContentScaleFactor: untrusted_scale_factor] }; + + // `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 + let window = match self.window() { + Some(window) => window, + None => 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 = self.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 = scale_factor as f64; + let bounds = self.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = self.convertRect_toCoordinateSpace(bounds, &screen_space); + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + }; + let window_id = RootWindowId(window.id()); + app_state::handle_nonuser_events( + mtm, + std::iter::once(EventWrapper::ScaleFactorChanged( + app_state::ScaleFactorChanged { + window, + scale_factor, + suggested_size: size.to_physical(scale_factor), + }, + )) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id, + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), + ); + } + + #[method(touchesBegan:withEvent:)] + fn touches_began(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(touchesMoved:withEvent:)] + fn touches_moved(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(touchesEnded:withEvent:)] + fn touches_ended(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(touchesCancelled:withEvent:)] + fn touches_cancelled(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(pinchGesture:)] + fn pinch_gesture(&self, recognizer: &UIPinchGestureRecognizer) { + let window = self.window().unwrap(); + + let (phase, delta) = match recognizer.state() { + UIGestureRecognizerState::Began => { + self.ivars().pinch_last_delta.set(recognizer.scale()); + (TouchPhase::Started, 0.0) + } + UIGestureRecognizerState::Changed => { + let last_scale: f64 = self.ivars().pinch_last_delta.replace(recognizer.scale()); + (TouchPhase::Moved, recognizer.scale() - last_scale) + } + UIGestureRecognizerState::Ended => { + let last_scale: f64 = self.ivars().pinch_last_delta.replace(0.0); + (TouchPhase::Moved, recognizer.scale() - last_scale) + } + UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => { + self.ivars().rotation_last_delta.set(0.0); + // Pass -delta so that action is reversed + (TouchPhase::Cancelled, -recognizer.scale()) + } + state => panic!("unexpected recognizer state: {state:?}"), + }; + + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::PinchGesture { + device_id: DEVICE_ID, + delta: delta as f64, + phase, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + + #[method(doubleTapGesture:)] + fn double_tap_gesture(&self, recognizer: &UITapGestureRecognizer) { + let window = self.window().unwrap(); + + if recognizer.state() == UIGestureRecognizerState::Ended { + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::DoubleTapGesture { + device_id: DEVICE_ID, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + } + + #[method(rotationGesture:)] + fn rotation_gesture(&self, recognizer: &UIRotationGestureRecognizer) { + let window = self.window().unwrap(); + + let (phase, delta) = match recognizer.state() { + UIGestureRecognizerState::Began => { + self.ivars().rotation_last_delta.set(0.0); + + (TouchPhase::Started, 0.0) + } + UIGestureRecognizerState::Changed => { + let last_rotation = self.ivars().rotation_last_delta.replace(recognizer.rotation()); + + (TouchPhase::Moved, recognizer.rotation() - last_rotation) + } + UIGestureRecognizerState::Ended => { + let last_rotation = self.ivars().rotation_last_delta.replace(0.0); + + (TouchPhase::Ended, recognizer.rotation() - last_rotation) + } + UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => { + self.ivars().rotation_last_delta.set(0.0); + + // Pass -delta so that action is reversed + (TouchPhase::Cancelled, -recognizer.rotation()) + } + state => panic!("unexpected recognizer state: {state:?}"), + }; + + // Make delta negative to match macos, convert to degrees + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::RotationGesture { + device_id: DEVICE_ID, + delta: -delta.to_degrees() as _, + phase, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + + #[method(panGesture:)] + fn pan_gesture(&self, recognizer: &UIPanGestureRecognizer) { + let window = self.window().unwrap(); + + let translation = recognizer.translationInView(Some(self)); + + let (phase, dx, dy) = match recognizer.state() { + UIGestureRecognizerState::Began => { + self.ivars().pan_last_delta.set(translation); + + (TouchPhase::Started, 0.0, 0.0) + } + UIGestureRecognizerState::Changed => { + let last_pan: CGPoint = self.ivars().pan_last_delta.replace(translation); + + let dx = translation.x - last_pan.x; + let dy = translation.y - last_pan.y; + + (TouchPhase::Moved, dx, dy) + } + UIGestureRecognizerState::Ended => { + let last_pan: CGPoint = self.ivars().pan_last_delta.replace(CGPoint{x:0.0, y:0.0}); + + let dx = translation.x - last_pan.x; + let dy = translation.y - last_pan.y; + + (TouchPhase::Ended, dx, dy) + } + UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => { + let last_pan: CGPoint = self.ivars().pan_last_delta.replace(CGPoint{x:0.0, y:0.0}); + + // Pass -delta so that action is reversed + (TouchPhase::Cancelled, -last_pan.x, -last_pan.y) + } + state => panic!("unexpected recognizer state: {state:?}"), + }; + + + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::PanGesture { + device_id: DEVICE_ID, + delta: PhysicalPosition::new(dx as _, dy as _), + phase, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + + #[method(canBecomeFirstResponder)] + fn can_become_first_responder(&self) -> bool { + true + } + } + + unsafe impl NSObjectProtocol for WinitView {} + + unsafe impl UIGestureRecognizerDelegate for WinitView { + #[method(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)] + fn should_recognize_simultaneously(&self, _gesture_recognizer: &UIGestureRecognizer, _other_gesture_recognizer: &UIGestureRecognizer) -> bool { + true + } + } + + unsafe impl UITextInputTraits for WinitView { + } + + unsafe impl UIKeyInput for WinitView { + #[method(hasText)] + fn has_text(&self) -> bool { + true + } + + #[method(insertText:)] + fn insert_text(&self, text: &NSString) { + self.handle_insert_text(text) + } + + #[method(deleteBackward)] + fn delete_backward(&self) { + self.handle_delete_backward() + } + } +); + +impl WinitView { + pub(crate) fn new( + mtm: MainThreadMarker, + window_attributes: &WindowAttributes, + frame: CGRect, + ) -> Retained { + let this = mtm.alloc().set_ivars(WinitViewState { + pinch_gesture_recognizer: RefCell::new(None), + doubletap_gesture_recognizer: RefCell::new(None), + rotation_gesture_recognizer: RefCell::new(None), + pan_gesture_recognizer: RefCell::new(None), + + rotation_last_delta: Cell::new(0.0), + pinch_last_delta: Cell::new(0.0), + pan_last_delta: Cell::new(CGPoint { x: 0.0, y: 0.0 }), + }); + let this: Retained = unsafe { msg_send_id![super(this), initWithFrame: frame] }; + + this.setMultipleTouchEnabled(true); + + if let Some(scale_factor) = window_attributes.platform_specific.scale_factor { + this.setContentScaleFactor(scale_factor as _); + } + + this + } + + fn window(&self) -> Option> { + // SAFETY: `WinitView`s are always installed in a `WinitUIWindow` + (**self).window().map(|window| unsafe { Retained::cast(window) }) + } + + pub(crate) fn recognize_pinch_gesture(&self, should_recognize: bool) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().pinch_gesture_recognizer.borrow().is_none() { + let pinch = unsafe { + UIPinchGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(pinchGesture:)), + ) + }; + pinch.setDelegate(Some(ProtocolObject::from_ref(self))); + self.addGestureRecognizer(&pinch); + self.ivars().pinch_gesture_recognizer.replace(Some(pinch)); + } + } else if let Some(recognizer) = self.ivars().pinch_gesture_recognizer.take() { + self.removeGestureRecognizer(&recognizer); + } + } + + pub(crate) fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().pan_gesture_recognizer.borrow().is_none() { + let pan = unsafe { + UIPanGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(panGesture:)), + ) + }; + pan.setDelegate(Some(ProtocolObject::from_ref(self))); + pan.setMinimumNumberOfTouches(minimum_number_of_touches as _); + pan.setMaximumNumberOfTouches(maximum_number_of_touches as _); + self.addGestureRecognizer(&pan); + self.ivars().pan_gesture_recognizer.replace(Some(pan)); + } + } else if let Some(recognizer) = self.ivars().pan_gesture_recognizer.take() { + self.removeGestureRecognizer(&recognizer); + } + } + + pub(crate) fn recognize_doubletap_gesture(&self, should_recognize: bool) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().doubletap_gesture_recognizer.borrow().is_none() { + let tap = unsafe { + UITapGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(doubleTapGesture:)), + ) + }; + tap.setDelegate(Some(ProtocolObject::from_ref(self))); + tap.setNumberOfTapsRequired(2); + tap.setNumberOfTouchesRequired(1); + self.addGestureRecognizer(&tap); + self.ivars().doubletap_gesture_recognizer.replace(Some(tap)); + } + } else if let Some(recognizer) = self.ivars().doubletap_gesture_recognizer.take() { + self.removeGestureRecognizer(&recognizer); + } + } + + pub(crate) fn recognize_rotation_gesture(&self, should_recognize: bool) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().rotation_gesture_recognizer.borrow().is_none() { + let rotation = unsafe { + UIRotationGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(rotationGesture:)), + ) + }; + rotation.setDelegate(Some(ProtocolObject::from_ref(self))); + self.addGestureRecognizer(&rotation); + self.ivars().rotation_gesture_recognizer.replace(Some(rotation)); + } + } else if let Some(recognizer) = self.ivars().rotation_gesture_recognizer.take() { + self.removeGestureRecognizer(&recognizer); + } + } + + fn handle_touches(&self, touches: &NSSet) { + let window = self.window().unwrap(); + let mut touch_events = Vec::new(); + let os_supports_force = app_state::os_capabilities().force_touch; + for touch in touches { + let logical_location = touch.locationInView(None); + let touch_type = touch.r#type(); + let force = if os_supports_force { + let trait_collection = self.traitCollection(); + let touch_capability = trait_collection.forceTouchCapability(); + // Both the OS _and_ the device need to be checked for force touch support. + if touch_capability == UIForceTouchCapability::Available + || touch_type == UITouchType::Pencil + { + let force = touch.force(); + let max_possible_force = touch.maximumPossibleForce(); + let altitude_angle: Option = if touch_type == UITouchType::Pencil { + let angle = 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 *const UITouch as u64; + let phase = 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:?}"), + }; + + let physical_location = { + let scale_factor = self.contentScaleFactor(); + PhysicalPosition::from_logical::<(f64, f64), f64>( + (logical_location.x as _, logical_location.y as _), + scale_factor as f64, + ) + }; + touch_events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Touch(Touch { + device_id: DEVICE_ID, + id: touch_id, + location: physical_location, + force, + phase, + }), + })); + } + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_events(mtm, touch_events); + } + + fn handle_insert_text(&self, text: &NSString) { + let window = self.window().unwrap(); + let window_id = RootWindowId(window.id()); + let mtm = MainThreadMarker::new().unwrap(); + // send individual events for each character + app_state::handle_nonuser_events( + mtm, + text.to_string().chars().flat_map(|c| { + let text = smol_str::SmolStr::from_iter([c]); + // Emit both press and release events + [ElementState::Pressed, ElementState::Released].map(|state| { + EventWrapper::StaticEvent(Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + event: KeyEvent { + text: if state == ElementState::Pressed { + Some(text.clone()) + } else { + None + }, + state, + location: KeyLocation::Standard, + repeat: false, + logical_key: Key::Character(text.clone()), + physical_key: PhysicalKey::Unidentified( + NativeKeyCode::Unidentified, + ), + platform_specific: KeyEventExtra {}, + }, + is_synthetic: false, + device_id: DEVICE_ID, + }, + }) + }) + }), + ); + } + + fn handle_delete_backward(&self) { + let window = self.window().unwrap(); + let window_id = RootWindowId(window.id()); + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_events( + mtm, + [ElementState::Pressed, ElementState::Released].map(|state| { + EventWrapper::StaticEvent(Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: KeyEvent { + state, + logical_key: Key::Named(NamedKey::Backspace), + physical_key: PhysicalKey::Code(KeyCode::Backspace), + platform_specific: KeyEventExtra {}, + repeat: false, + location: KeyLocation::Standard, + text: None, + }, + is_synthetic: false, + }, + }) + }), + ); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/ios/view_controller.rs b/third_party/winit-0.30.13/src/platform_impl/ios/view_controller.rs new file mode 100644 index 0000000..dd65868 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/ios/view_controller.rs @@ -0,0 +1,176 @@ +use std::cell::Cell; + +use objc2::rc::Retained; +use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; +use objc2_foundation::{MainThreadMarker, NSObject}; +use objc2_ui_kit::{ + UIDevice, UIInterfaceOrientationMask, UIRectEdge, UIResponder, UIStatusBarStyle, + UIUserInterfaceIdiom, UIView, UIViewController, +}; + +use super::app_state::{self}; +use crate::platform::ios::{ScreenEdge, StatusBarStyle, ValidOrientations}; +use crate::window::WindowAttributes; + +pub struct ViewControllerState { + prefers_status_bar_hidden: Cell, + preferred_status_bar_style: Cell, + prefers_home_indicator_auto_hidden: Cell, + supported_orientations: Cell, + preferred_screen_edges_deferring_system_gestures: Cell, +} + +declare_class!( + pub(crate) struct WinitViewController; + + unsafe impl ClassType for WinitViewController { + #[inherits(UIResponder, NSObject)] + type Super = UIViewController; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitUIViewController"; + } + + impl DeclaredClass for WinitViewController { + type Ivars = ViewControllerState; + } + + unsafe impl WinitViewController { + #[method(shouldAutorotate)] + fn should_autorotate(&self) -> bool { + true + } + + #[method(prefersStatusBarHidden)] + fn prefers_status_bar_hidden(&self) -> bool { + self.ivars().prefers_status_bar_hidden.get() + } + + #[method(preferredStatusBarStyle)] + fn preferred_status_bar_style(&self) -> UIStatusBarStyle { + self.ivars().preferred_status_bar_style.get() + } + + #[method(prefersHomeIndicatorAutoHidden)] + fn prefers_home_indicator_auto_hidden(&self) -> bool { + self.ivars().prefers_home_indicator_auto_hidden.get() + } + + #[method(supportedInterfaceOrientations)] + fn supported_orientations(&self) -> UIInterfaceOrientationMask { + self.ivars().supported_orientations.get() + } + + #[method(preferredScreenEdgesDeferringSystemGestures)] + fn preferred_screen_edges_deferring_system_gestures(&self) -> UIRectEdge { + self.ivars() + .preferred_screen_edges_deferring_system_gestures + .get() + } + } +); + +impl WinitViewController { + pub(crate) fn set_prefers_status_bar_hidden(&self, val: bool) { + self.ivars().prefers_status_bar_hidden.set(val); + self.setNeedsStatusBarAppearanceUpdate(); + } + + pub(crate) fn set_preferred_status_bar_style(&self, val: StatusBarStyle) { + let val = match val { + StatusBarStyle::Default => UIStatusBarStyle::Default, + StatusBarStyle::LightContent => UIStatusBarStyle::LightContent, + StatusBarStyle::DarkContent => UIStatusBarStyle::DarkContent, + }; + self.ivars().preferred_status_bar_style.set(val); + self.setNeedsStatusBarAppearanceUpdate(); + } + + pub(crate) fn set_prefers_home_indicator_auto_hidden(&self, val: bool) { + self.ivars().prefers_home_indicator_auto_hidden.set(val); + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.home_indicator_hidden { + self.setNeedsUpdateOfHomeIndicatorAutoHidden(); + } else { + os_capabilities.home_indicator_hidden_err_msg("ignoring") + } + } + + pub(crate) fn set_preferred_screen_edges_deferring_system_gestures(&self, val: ScreenEdge) { + let val = { + assert_eq!(val.bits() & !ScreenEdge::ALL.bits(), 0, "invalid `ScreenEdge`"); + UIRectEdge(val.bits().into()) + }; + self.ivars().preferred_screen_edges_deferring_system_gestures.set(val); + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.defer_system_gestures { + self.setNeedsUpdateOfScreenEdgesDeferringSystemGestures(); + } else { + os_capabilities.defer_system_gestures_err_msg("ignoring") + } + } + + pub(crate) fn set_supported_interface_orientations( + &self, + mtm: MainThreadMarker, + valid_orientations: ValidOrientations, + ) { + let mask = match (valid_orientations, UIDevice::currentDevice(mtm).userInterfaceIdiom()) { + (ValidOrientations::LandscapeAndPortrait, UIUserInterfaceIdiom::Phone) => { + UIInterfaceOrientationMask::AllButUpsideDown + }, + (ValidOrientations::LandscapeAndPortrait, _) => UIInterfaceOrientationMask::All, + (ValidOrientations::Landscape, _) => UIInterfaceOrientationMask::Landscape, + (ValidOrientations::Portrait, UIUserInterfaceIdiom::Phone) => { + UIInterfaceOrientationMask::Portrait + }, + (ValidOrientations::Portrait, _) => { + UIInterfaceOrientationMask::Portrait + | UIInterfaceOrientationMask::PortraitUpsideDown + }, + }; + self.ivars().supported_orientations.set(mask); + #[allow(deprecated)] + UIViewController::attemptRotationToDeviceOrientation(mtm); + } + + pub(crate) fn new( + mtm: MainThreadMarker, + window_attributes: &WindowAttributes, + view: &UIView, + ) -> Retained { + // These are set properly below, we just to set them to something in the meantime. + let this = mtm.alloc().set_ivars(ViewControllerState { + prefers_status_bar_hidden: Cell::new(false), + preferred_status_bar_style: Cell::new(UIStatusBarStyle::Default), + prefers_home_indicator_auto_hidden: Cell::new(false), + supported_orientations: Cell::new(UIInterfaceOrientationMask::All), + preferred_screen_edges_deferring_system_gestures: Cell::new(UIRectEdge::empty()), + }); + let this: Retained = unsafe { msg_send_id![super(this), init] }; + + this.set_prefers_status_bar_hidden( + window_attributes.platform_specific.prefers_status_bar_hidden, + ); + + this.set_preferred_status_bar_style( + window_attributes.platform_specific.preferred_status_bar_style, + ); + + this.set_supported_interface_orientations( + mtm, + window_attributes.platform_specific.valid_orientations, + ); + + this.set_prefers_home_indicator_auto_hidden( + window_attributes.platform_specific.prefers_home_indicator_hidden, + ); + + this.set_preferred_screen_edges_deferring_system_gestures( + window_attributes.platform_specific.preferred_screen_edges_deferring_system_gestures, + ); + + this.setView(Some(view)); + + this + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/ios/window.rs b/third_party/winit-0.30.13/src/platform_impl/ios/window.rs new file mode 100644 index 0000000..be02759 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/ios/window.rs @@ -0,0 +1,750 @@ +#![allow(clippy::unnecessary_cast)] + +use std::collections::VecDeque; + +use objc2::rc::Retained; +use objc2::runtime::{AnyObject, NSObject}; +use objc2::{class, declare_class, msg_send, msg_send_id, mutability, ClassType, DeclaredClass}; +use objc2_foundation::{ + CGFloat, CGPoint, CGRect, CGSize, MainThreadBound, MainThreadMarker, NSObjectProtocol, +}; +use objc2_ui_kit::{ + UIApplication, UICoordinateSpace, UIResponder, UIScreen, UIScreenOverscanCompensation, + UIViewController, UIWindow, +}; +use tracing::{debug, warn}; + +use super::app_state::EventWrapper; +use super::view::WinitView; +use super::view_controller::WinitViewController; +use crate::cursor::Cursor; +use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::{Event, WindowEvent}; +use crate::icon::Icon; +use crate::platform::ios::{ScreenEdge, StatusBarStyle, ValidOrientations}; +use crate::platform_impl::platform::{ + app_state, monitor, ActiveEventLoop, Fullscreen, MonitorHandle, +}; +use crate::window::{ + CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowId as RootWindowId, WindowLevel, +}; + +declare_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct WinitUIWindow; + + unsafe impl ClassType for WinitUIWindow { + #[inherits(UIResponder, NSObject)] + type Super = UIWindow; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitUIWindow"; + } + + impl DeclaredClass for WinitUIWindow {} + + unsafe impl WinitUIWindow { + #[method(becomeKeyWindow)] + fn become_key_window(&self) { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(self.id()), + event: WindowEvent::Focused(true), + }), + ); + let _: () = unsafe { msg_send![super(self), becomeKeyWindow] }; + } + + #[method(resignKeyWindow)] + fn resign_key_window(&self) { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(self.id()), + event: WindowEvent::Focused(false), + }), + ); + let _: () = unsafe { msg_send![super(self), resignKeyWindow] }; + } + } +); + +impl WinitUIWindow { + pub(crate) fn new( + mtm: MainThreadMarker, + window_attributes: &WindowAttributes, + frame: CGRect, + view_controller: &UIViewController, + ) -> Retained { + let this: Retained = unsafe { msg_send_id![mtm.alloc(), initWithFrame: frame] }; + + this.setRootViewController(Some(view_controller)); + + match window_attributes.fullscreen.clone().map(Into::into) { + Some(Fullscreen::Exclusive(ref video_mode)) => { + let monitor = video_mode.monitor(); + let screen = monitor.ui_screen(mtm); + screen.setCurrentMode(Some(video_mode.screen_mode(mtm))); + this.setScreen(screen); + }, + Some(Fullscreen::Borderless(Some(ref monitor))) => { + let screen = monitor.ui_screen(mtm); + this.setScreen(screen); + }, + _ => (), + } + + this + } + + pub(crate) fn id(&self) -> WindowId { + (self as *const Self as usize as u64).into() + } +} + +pub struct Inner { + window: Retained, + view_controller: Retained, + view: Retained, + gl_or_metal_backed: bool, +} + +impl Inner { + pub fn set_title(&self, _title: &str) { + debug!("`Window::set_title` is ignored on iOS") + } + + pub fn set_transparent(&self, _transparent: bool) { + debug!("`Window::set_transparent` is ignored on iOS") + } + + pub fn set_blur(&self, _blur: bool) { + debug!("`Window::set_blur` is ignored on iOS") + } + + pub fn set_visible(&self, visible: bool) { + self.window.setHidden(!visible) + } + + pub fn is_visible(&self) -> Option { + warn!("`Window::is_visible` is ignored on iOS"); + None + } + + pub fn request_redraw(&self) { + if self.gl_or_metal_backed { + let mtm = MainThreadMarker::new().unwrap(); + // `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(mtm, self.window.clone()); + } else { + self.view.setNeedsDisplay(); + } + } + + pub fn pre_present_notify(&self) {} + + pub fn inner_position(&self) -> Result, NotSupportedError> { + 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> { + 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) { + 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.rect_from_screen_space(new_screen_frame); + self.window.setBounds(bounds); + } + + pub fn inner_size(&self) -> PhysicalSize { + 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 { + 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 request_inner_size(&self, _size: Size) -> Option> { + Some(self.inner_size()) + } + + pub fn set_min_inner_size(&self, _dimensions: Option) { + warn!("`Window::set_min_inner_size` is ignored on iOS") + } + + pub fn set_max_inner_size(&self, _dimensions: Option) { + warn!("`Window::set_max_inner_size` is ignored on iOS") + } + + pub fn resize_increments(&self) -> Option> { + None + } + + #[inline] + pub fn set_resize_increments(&self, _increments: Option) { + warn!("`Window::set_resize_increments` is ignored on iOS") + } + + pub fn set_resizable(&self, _resizable: bool) { + warn!("`Window::set_resizable` is ignored on iOS") + } + + pub fn is_resizable(&self) -> bool { + warn!("`Window::is_resizable` is ignored on iOS"); + false + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) { + warn!("`Window::set_enabled_buttons` is ignored on iOS"); + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + warn!("`Window::enabled_buttons` is ignored on iOS"); + WindowButtons::all() + } + + pub fn scale_factor(&self) -> f64 { + self.view.contentScaleFactor() as _ + } + + pub fn set_cursor(&self, _cursor: Cursor) { + debug!("`Window::set_cursor` ignored on iOS") + } + + pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn set_cursor_grab(&self, _: CursorGrabMode) -> 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 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())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + pub fn set_cursor_hittest(&self, _hittest: 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 is_minimized(&self) -> Option { + warn!("`Window::is_minimized` is ignored on iOS"); + None + } + + 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(crate) fn set_fullscreen(&self, monitor: Option) { + let mtm = MainThreadMarker::new().unwrap(); + let uiscreen = match &monitor { + Some(Fullscreen::Exclusive(video_mode)) => { + let uiscreen = video_mode.monitor.ui_screen(mtm); + uiscreen.setCurrentMode(Some(video_mode.screen_mode(mtm))); + uiscreen.clone() + }, + Some(Fullscreen::Borderless(Some(monitor))) => monitor.ui_screen(mtm).clone(), + Some(Fullscreen::Borderless(None)) => { + self.current_monitor_inner().ui_screen(mtm).clone() + }, + 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 = self.window.screen(); + if uiscreen != current { + self.window.setScreen(&uiscreen); + } + + let bounds = uiscreen.bounds(); + 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 + uiscreen.setOverscanCompensation(UIScreenOverscanCompensation::None); + } + + pub(crate) fn fullscreen(&self) -> Option { + let mtm = MainThreadMarker::new().unwrap(); + let monitor = self.current_monitor_inner(); + let uiscreen = monitor.ui_screen(mtm); + let screen_space_bounds = self.screen_frame(); + let screen_bounds = 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) {} + + pub fn is_decorated(&self) -> bool { + true + } + + pub fn set_window_level(&self, _level: WindowLevel) { + warn!("`Window::set_window_level` 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_cursor_area(&self, _position: Position, _size: Size) { + warn!("`Window::set_ime_cursor_area` is ignored on iOS") + } + + /// Show / hide the keyboard. To show the keyboard, we call `becomeFirstResponder`, + /// requesting focus for the [WinitView]. Since [WinitView] implements + /// [objc2_ui_kit::UIKeyInput], the keyboard will be shown. + /// + pub fn set_ime_allowed(&self, allowed: bool) { + if allowed { + unsafe { + self.view.becomeFirstResponder(); + } + } else { + unsafe { + self.view.resignFirstResponder(); + } + } + } + + pub fn set_ime_purpose(&self, _purpose: ImePurpose) { + warn!("`Window::set_ime_purpose` is ignored on iOS") + } + + pub fn focus_window(&self) { + warn!("`Window::set_focus` is ignored on iOS") + } + + pub fn request_user_attention(&self, _request_type: Option) { + warn!("`Window::request_user_attention` is ignored on iOS") + } + + // Allow directly accessing the current monitor internally without unwrapping. + fn current_monitor_inner(&self) -> MonitorHandle { + MonitorHandle::new(self.window.screen()) + } + + pub fn current_monitor(&self) -> Option { + Some(self.current_monitor_inner()) + } + + pub fn available_monitors(&self) -> VecDeque { + monitor::uiscreens(MainThreadMarker::new().unwrap()) + } + + pub fn primary_monitor(&self) -> Option { + #[allow(deprecated)] + Some(MonitorHandle::new(UIScreen::mainScreen(MainThreadMarker::new().unwrap()))) + } + + pub fn id(&self) -> WindowId { + self.window.id() + } + + #[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 = Retained::as_ptr(&self.window) as _; + window_handle.ui_view = Retained::as_ptr(&self.view) as _; + window_handle.ui_view_controller = Retained::as_ptr(&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 = Retained::as_ptr(&self.window) as _; + window_handle.ui_view = Retained::as_ptr(&self.view) as _; + window_handle.ui_view_controller = Retained::as_ptr(&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) -> rwh_06::RawWindowHandle { + let mut window_handle = rwh_06::UiKitWindowHandle::new({ + let ui_view = Retained::as_ptr(&self.view) as _; + std::ptr::NonNull::new(ui_view).expect("Retained should never be null") + }); + window_handle.ui_view_controller = + std::ptr::NonNull::new(Retained::as_ptr(&self.view_controller) as _); + rwh_06::RawWindowHandle::UiKit(window_handle) + } + + pub fn theme(&self) -> Option { + warn!("`Window::theme` is ignored on iOS"); + None + } + + pub fn set_content_protected(&self, _protected: bool) {} + + pub fn has_focus(&self) -> bool { + self.window.isKeyWindow() + } + + #[inline] + pub fn set_theme(&self, _theme: Option) { + warn!("`Window::set_theme` is ignored on iOS"); + } + + pub fn title(&self) -> String { + warn!("`Window::title` is ignored on iOS"); + String::new() + } + + pub fn reset_dead_keys(&self) { + // Noop + } +} + +pub struct Window { + inner: MainThreadBound, +} + +impl Window { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + window_attributes: WindowAttributes, + ) -> Result { + let mtm = event_loop.mtm; + + if window_attributes.min_inner_size.is_some() { + warn!("`WindowAttributes::min_inner_size` is ignored on iOS"); + } + if window_attributes.max_inner_size.is_some() { + warn!("`WindowAttributes::max_inner_size` is ignored on iOS"); + } + + // TODO: transparency, visible + + #[allow(deprecated)] + let main_screen = UIScreen::mainScreen(mtm); + let fullscreen = window_attributes.fullscreen.clone().map(Into::into); + let screen = match fullscreen { + Some(Fullscreen::Exclusive(ref video_mode)) => video_mode.monitor.ui_screen(mtm), + Some(Fullscreen::Borderless(Some(ref monitor))) => monitor.ui_screen(mtm), + Some(Fullscreen::Borderless(None)) | None => &main_screen, + }; + + let screen_bounds = screen.bounds(); + + let frame = match window_attributes.inner_size { + Some(dim) => { + let scale_factor = screen.scale(); + let size = dim.to_logical::(scale_factor as f64); + CGRect { + origin: screen_bounds.origin, + size: CGSize { width: size.width as _, height: size.height as _ }, + } + }, + None => screen_bounds, + }; + + let view = WinitView::new(mtm, &window_attributes, frame); + + let gl_or_metal_backed = + view.isKindOfClass(class!(CAMetalLayer)) || view.isKindOfClass(class!(CAEAGLLayer)); + + let view_controller = WinitViewController::new(mtm, &window_attributes, &view); + let window = WinitUIWindow::new(mtm, &window_attributes, frame, &view_controller); + + app_state::set_key_window(mtm, &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 = view.contentScaleFactor(); + let scale_factor = scale_factor as f64; + if scale_factor != 1.0 { + let bounds = view.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = view.convertRect_toCoordinateSpace(bounds, &screen_space); + let size = LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + }; + let window_id = RootWindowId(window.id()); + app_state::handle_nonuser_events( + mtm, + std::iter::once(EventWrapper::ScaleFactorChanged(app_state::ScaleFactorChanged { + window: window.clone(), + scale_factor, + suggested_size: size.to_physical(scale_factor), + })) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id, + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), + ); + } + + let inner = Inner { window, view_controller, view, gl_or_metal_backed }; + Ok(Window { inner: MainThreadBound::new(inner, mtm) }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Inner) + Send + 'static) { + // For now, don't actually do queuing, since it may be less predictable + self.maybe_wait_on_main(f) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Inner) -> R + Send) -> R { + self.inner.get_on_main(|inner| f(inner)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_window_handle_rwh_06( + &self, + ) -> Result { + if let Some(mtm) = MainThreadMarker::new() { + Ok(self.inner.get(mtm).raw_window_handle_rwh_06()) + } else { + Err(rwh_06::HandleError::Unavailable) + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::UiKit(rwh_06::UiKitDisplayHandle::new())) + } +} + +// WindowExtIOS +impl Inner { + pub fn set_scale_factor(&self, scale_factor: f64) { + assert!( + dpi::validate_scale_factor(scale_factor), + "`WindowExtIOS::set_scale_factor` received an invalid hidpi factor" + ); + let scale_factor = scale_factor as CGFloat; + self.view.setContentScaleFactor(scale_factor); + } + + pub fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { + self.view_controller.set_supported_interface_orientations( + MainThreadMarker::new().unwrap(), + valid_orientations, + ); + } + + pub fn set_prefers_home_indicator_hidden(&self, hidden: bool) { + self.view_controller.set_prefers_home_indicator_auto_hidden(hidden); + } + + pub fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { + self.view_controller.set_preferred_screen_edges_deferring_system_gestures(edges); + } + + pub fn set_prefers_status_bar_hidden(&self, hidden: bool) { + self.view_controller.set_prefers_status_bar_hidden(hidden); + } + + pub fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle) { + self.view_controller.set_preferred_status_bar_style(status_bar_style); + } + + pub fn recognize_pinch_gesture(&self, should_recognize: bool) { + self.view.recognize_pinch_gesture(should_recognize); + } + + pub fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ) { + self.view.recognize_pan_gesture( + should_recognize, + minimum_number_of_touches, + maximum_number_of_touches, + ); + } + + pub fn recognize_doubletap_gesture(&self, should_recognize: bool) { + self.view.recognize_doubletap_gesture(should_recognize); + } + + pub fn recognize_rotation_gesture(&self, should_recognize: bool) { + self.view.recognize_rotation_gesture(should_recognize); + } +} + +impl Inner { + fn screen_frame(&self) -> CGRect { + self.rect_to_screen_space(self.window.bounds()) + } + + fn rect_to_screen_space(&self, rect: CGRect) -> CGRect { + let screen_space = self.window.screen().coordinateSpace(); + self.window.convertRect_toCoordinateSpace(rect, &screen_space) + } + + fn rect_from_screen_space(&self, rect: CGRect) -> CGRect { + let screen_space = self.window.screen().coordinateSpace(); + self.window.convertRect_fromCoordinateSpace(rect, &screen_space) + } + + fn safe_area_screen_space(&self) -> CGRect { + let bounds = self.window.bounds(); + if app_state::os_capabilities().safe_area { + let safe_area = 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.rect_to_screen_space(safe_bounds) + } else { + let screen_frame = self.rect_to_screen_space(bounds); + let status_bar_frame = { + let app = UIApplication::sharedApplication(MainThreadMarker::new().unwrap()); + #[allow(deprecated)] + 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: *mut WinitUIWindow, +} + +impl WindowId { + pub const fn dummy() -> Self { + WindowId { window: std::ptr::null_mut() } + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.window as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self { window: raw_id as _ } + } +} + +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 _ } + } +} + +#[derive(Clone, Debug, Default)] +pub struct PlatformSpecificWindowAttributes { + pub scale_factor: Option, + pub valid_orientations: ValidOrientations, + pub prefers_home_indicator_hidden: bool, + pub prefers_status_bar_hidden: bool, + pub preferred_status_bar_style: StatusBarStyle, + pub preferred_screen_edges_deferring_system_gestures: ScreenEdge, +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/common/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/common/mod.rs new file mode 100644 index 0000000..ed7f82c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/common/mod.rs @@ -0,0 +1 @@ +pub mod xkb; diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/compose.rs b/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/compose.rs new file mode 100644 index 0000000..9d4f590 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/compose.rs @@ -0,0 +1,123 @@ +//! XKB compose handling. + +use std::env; +use std::ffi::CString; +use std::ops::Deref; +use std::os::unix::ffi::OsStringExt; +use std::ptr::NonNull; + +use super::{XkbContext, XKBCH}; +use smol_str::SmolStr; +use xkbcommon_dl::{ + xkb_compose_compile_flags, xkb_compose_feed_result, xkb_compose_state, xkb_compose_state_flags, + xkb_compose_status, xkb_compose_table, xkb_keysym_t, +}; + +#[derive(Debug)] +pub struct XkbComposeTable { + table: NonNull, +} + +impl XkbComposeTable { + pub fn new(context: &XkbContext) -> Option { + let locale = env::var_os("LC_ALL") + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LC_CTYPE")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LANG")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .unwrap_or_else(|| "C".into()); + let locale = CString::new(locale.into_vec()).unwrap(); + + let table = unsafe { + (XKBCH.xkb_compose_table_new_from_locale)( + context.as_ptr(), + locale.as_ptr(), + xkb_compose_compile_flags::XKB_COMPOSE_COMPILE_NO_FLAGS, + ) + }; + + let table = NonNull::new(table)?; + Some(Self { table }) + } + + /// Create new state with the given compose table. + pub fn new_state(&self) -> Option { + let state = unsafe { + (XKBCH.xkb_compose_state_new)( + self.table.as_ptr(), + xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS, + ) + }; + + let state = NonNull::new(state)?; + Some(XkbComposeState { state }) + } +} + +impl Deref for XkbComposeTable { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.table + } +} + +impl Drop for XkbComposeTable { + fn drop(&mut self) { + unsafe { + (XKBCH.xkb_compose_table_unref)(self.table.as_ptr()); + } + } +} + +#[derive(Debug)] +pub struct XkbComposeState { + state: NonNull, +} + +impl XkbComposeState { + pub fn get_string(&mut self, scratch_buffer: &mut Vec) -> Option { + super::make_string_with(scratch_buffer, |ptr, len| unsafe { + (XKBCH.xkb_compose_state_get_utf8)(self.state.as_ptr(), ptr, len) + }) + } + + #[inline] + pub fn feed(&mut self, keysym: xkb_keysym_t) -> ComposeStatus { + let feed_result = unsafe { (XKBCH.xkb_compose_state_feed)(self.state.as_ptr(), keysym) }; + match feed_result { + xkb_compose_feed_result::XKB_COMPOSE_FEED_IGNORED => ComposeStatus::Ignored, + xkb_compose_feed_result::XKB_COMPOSE_FEED_ACCEPTED => { + ComposeStatus::Accepted(self.status()) + }, + } + } + + #[inline] + pub fn reset(&mut self) { + unsafe { + (XKBCH.xkb_compose_state_reset)(self.state.as_ptr()); + } + } + + #[inline] + pub fn status(&mut self) -> xkb_compose_status { + unsafe { (XKBCH.xkb_compose_state_get_status)(self.state.as_ptr()) } + } +} + +impl Drop for XkbComposeState { + fn drop(&mut self) { + unsafe { + (XKBCH.xkb_compose_state_unref)(self.state.as_ptr()); + }; + } +} + +#[derive(Copy, Clone, Debug)] +pub enum ComposeStatus { + Accepted(xkb_compose_status), + Ignored, + None, +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/keymap.rs b/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/keymap.rs new file mode 100644 index 0000000..67aac5c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/keymap.rs @@ -0,0 +1,1044 @@ +//! XKB keymap. + +use std::ffi::c_char; +use std::ops::Deref; +use std::ptr::{self, NonNull}; + +#[cfg(x11_platform)] +use x11_dl::xlib_xcb::xcb_connection_t; +#[cfg(wayland_platform)] +use {memmap2::MmapOptions, std::os::unix::io::OwnedFd}; + +use xkb::XKB_MOD_INVALID; +use xkbcommon_dl::{ + self as xkb, xkb_keycode_t, xkb_keymap, xkb_keymap_compile_flags, xkb_keysym_t, + xkb_layout_index_t, xkb_mod_index_t, +}; + +use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; +#[cfg(x11_platform)] +use crate::platform_impl::common::xkb::XKBXH; +use crate::platform_impl::common::xkb::{XkbContext, XKBH}; + +/// Map the raw X11-style keycode to the `KeyCode` enum. +/// +/// X11-style keycodes are offset by 8 from the keycodes the Linux kernel uses. +pub fn raw_keycode_to_physicalkey(keycode: u32) -> PhysicalKey { + scancode_to_physicalkey(keycode.saturating_sub(8)) +} + +/// Map the linux scancode to Keycode. +/// +/// Both X11 and Wayland use keys with `+ 8` offset to linux scancode. +pub fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey { + // The keycode values are taken from linux/include/uapi/linux/input-event-codes.h, as + // libxkbcommon's documentation seems to suggest that the keycode values we're interested in + // are defined by the Linux kernel. If Winit programs end up being run on other Unix-likes, + // I can only hope they agree on what the keycodes mean. + // + // Some of the keycodes are likely superfluous for our purposes, and some are ones which are + // difficult to test the correctness of, or discover the purpose of. Because of this, they've + // either been commented out here, or not included at all. + PhysicalKey::Code(match scancode { + 0 => return PhysicalKey::Unidentified(NativeKeyCode::Xkb(0)), + 1 => KeyCode::Escape, + 2 => KeyCode::Digit1, + 3 => KeyCode::Digit2, + 4 => KeyCode::Digit3, + 5 => KeyCode::Digit4, + 6 => KeyCode::Digit5, + 7 => KeyCode::Digit6, + 8 => KeyCode::Digit7, + 9 => KeyCode::Digit8, + 10 => KeyCode::Digit9, + 11 => KeyCode::Digit0, + 12 => KeyCode::Minus, + 13 => KeyCode::Equal, + 14 => KeyCode::Backspace, + 15 => KeyCode::Tab, + 16 => KeyCode::KeyQ, + 17 => KeyCode::KeyW, + 18 => KeyCode::KeyE, + 19 => KeyCode::KeyR, + 20 => KeyCode::KeyT, + 21 => KeyCode::KeyY, + 22 => KeyCode::KeyU, + 23 => KeyCode::KeyI, + 24 => KeyCode::KeyO, + 25 => KeyCode::KeyP, + 26 => KeyCode::BracketLeft, + 27 => KeyCode::BracketRight, + 28 => KeyCode::Enter, + 29 => KeyCode::ControlLeft, + 30 => KeyCode::KeyA, + 31 => KeyCode::KeyS, + 32 => KeyCode::KeyD, + 33 => KeyCode::KeyF, + 34 => KeyCode::KeyG, + 35 => KeyCode::KeyH, + 36 => KeyCode::KeyJ, + 37 => KeyCode::KeyK, + 38 => KeyCode::KeyL, + 39 => KeyCode::Semicolon, + 40 => KeyCode::Quote, + 41 => KeyCode::Backquote, + 42 => KeyCode::ShiftLeft, + 43 => KeyCode::Backslash, + 44 => KeyCode::KeyZ, + 45 => KeyCode::KeyX, + 46 => KeyCode::KeyC, + 47 => KeyCode::KeyV, + 48 => KeyCode::KeyB, + 49 => KeyCode::KeyN, + 50 => KeyCode::KeyM, + 51 => KeyCode::Comma, + 52 => KeyCode::Period, + 53 => KeyCode::Slash, + 54 => KeyCode::ShiftRight, + 55 => KeyCode::NumpadMultiply, + 56 => KeyCode::AltLeft, + 57 => KeyCode::Space, + 58 => KeyCode::CapsLock, + 59 => KeyCode::F1, + 60 => KeyCode::F2, + 61 => KeyCode::F3, + 62 => KeyCode::F4, + 63 => KeyCode::F5, + 64 => KeyCode::F6, + 65 => KeyCode::F7, + 66 => KeyCode::F8, + 67 => KeyCode::F9, + 68 => KeyCode::F10, + 69 => KeyCode::NumLock, + 70 => KeyCode::ScrollLock, + 71 => KeyCode::Numpad7, + 72 => KeyCode::Numpad8, + 73 => KeyCode::Numpad9, + 74 => KeyCode::NumpadSubtract, + 75 => KeyCode::Numpad4, + 76 => KeyCode::Numpad5, + 77 => KeyCode::Numpad6, + 78 => KeyCode::NumpadAdd, + 79 => KeyCode::Numpad1, + 80 => KeyCode::Numpad2, + 81 => KeyCode::Numpad3, + 82 => KeyCode::Numpad0, + 83 => KeyCode::NumpadDecimal, + 85 => KeyCode::Lang5, + 86 => KeyCode::IntlBackslash, + 87 => KeyCode::F11, + 88 => KeyCode::F12, + 89 => KeyCode::IntlRo, + 90 => KeyCode::Lang3, + 91 => KeyCode::Lang4, + 92 => KeyCode::Convert, + 93 => KeyCode::KanaMode, + 94 => KeyCode::NonConvert, + // 95 => KeyCode::KPJPCOMMA, + 96 => KeyCode::NumpadEnter, + 97 => KeyCode::ControlRight, + 98 => KeyCode::NumpadDivide, + 99 => KeyCode::PrintScreen, + 100 => KeyCode::AltRight, + // 101 => KeyCode::LINEFEED, + 102 => KeyCode::Home, + 103 => KeyCode::ArrowUp, + 104 => KeyCode::PageUp, + 105 => KeyCode::ArrowLeft, + 106 => KeyCode::ArrowRight, + 107 => KeyCode::End, + 108 => KeyCode::ArrowDown, + 109 => KeyCode::PageDown, + 110 => KeyCode::Insert, + 111 => KeyCode::Delete, + // 112 => KeyCode::MACRO, + 113 => KeyCode::AudioVolumeMute, + 114 => KeyCode::AudioVolumeDown, + 115 => KeyCode::AudioVolumeUp, + // 116 => KeyCode::POWER, + 117 => KeyCode::NumpadEqual, + // 118 => KeyCode::KPPLUSMINUS, + 119 => KeyCode::Pause, + // 120 => KeyCode::SCALE, + 121 => KeyCode::NumpadComma, + 122 => KeyCode::Lang1, + 123 => KeyCode::Lang2, + 124 => KeyCode::IntlYen, + 125 => KeyCode::SuperLeft, + 126 => KeyCode::SuperRight, + 127 => KeyCode::ContextMenu, + // 128 => KeyCode::STOP, + // 129 => KeyCode::AGAIN, + // 130 => KeyCode::PROPS, + // 131 => KeyCode::UNDO, + // 132 => KeyCode::FRONT, + // 133 => KeyCode::COPY, + // 134 => KeyCode::OPEN, + // 135 => KeyCode::PASTE, + // 136 => KeyCode::FIND, + // 137 => KeyCode::CUT, + // 138 => KeyCode::HELP, + // 139 => KeyCode::MENU, + // 140 => KeyCode::CALC, + // 141 => KeyCode::SETUP, + // 142 => KeyCode::SLEEP, + // 143 => KeyCode::WAKEUP, + // 144 => KeyCode::FILE, + // 145 => KeyCode::SENDFILE, + // 146 => KeyCode::DELETEFILE, + // 147 => KeyCode::XFER, + // 148 => KeyCode::PROG1, + // 149 => KeyCode::PROG2, + // 150 => KeyCode::WWW, + // 151 => KeyCode::MSDOS, + // 152 => KeyCode::COFFEE, + // 153 => KeyCode::ROTATE_DISPLAY, + // 154 => KeyCode::CYCLEWINDOWS, + // 155 => KeyCode::MAIL, + // 156 => KeyCode::BOOKMARKS, + // 157 => KeyCode::COMPUTER, + // 158 => KeyCode::BACK, + // 159 => KeyCode::FORWARD, + // 160 => KeyCode::CLOSECD, + // 161 => KeyCode::EJECTCD, + // 162 => KeyCode::EJECTCLOSECD, + 163 => KeyCode::MediaTrackNext, + 164 => KeyCode::MediaPlayPause, + 165 => KeyCode::MediaTrackPrevious, + 166 => KeyCode::MediaStop, + // 167 => KeyCode::RECORD, + // 168 => KeyCode::REWIND, + // 169 => KeyCode::PHONE, + // 170 => KeyCode::ISO, + // 171 => KeyCode::CONFIG, + // 172 => KeyCode::HOMEPAGE, + // 173 => KeyCode::REFRESH, + // 174 => KeyCode::EXIT, + // 175 => KeyCode::MOVE, + // 176 => KeyCode::EDIT, + // 177 => KeyCode::SCROLLUP, + // 178 => KeyCode::SCROLLDOWN, + // 179 => KeyCode::KPLEFTPAREN, + // 180 => KeyCode::KPRIGHTPAREN, + // 181 => KeyCode::NEW, + // 182 => KeyCode::REDO, + 183 => KeyCode::F13, + 184 => KeyCode::F14, + 185 => KeyCode::F15, + 186 => KeyCode::F16, + 187 => KeyCode::F17, + 188 => KeyCode::F18, + 189 => KeyCode::F19, + 190 => KeyCode::F20, + 191 => KeyCode::F21, + 192 => KeyCode::F22, + 193 => KeyCode::F23, + 194 => KeyCode::F24, + // 200 => KeyCode::PLAYCD, + // 201 => KeyCode::PAUSECD, + // 202 => KeyCode::PROG3, + // 203 => KeyCode::PROG4, + // 204 => KeyCode::DASHBOARD, + // 205 => KeyCode::SUSPEND, + // 206 => KeyCode::CLOSE, + // 207 => KeyCode::PLAY, + // 208 => KeyCode::FASTFORWARD, + // 209 => KeyCode::BASSBOOST, + // 210 => KeyCode::PRINT, + // 211 => KeyCode::HP, + // 212 => KeyCode::CAMERA, + // 213 => KeyCode::SOUND, + // 214 => KeyCode::QUESTION, + // 215 => KeyCode::EMAIL, + // 216 => KeyCode::CHAT, + // 217 => KeyCode::SEARCH, + // 218 => KeyCode::CONNECT, + // 219 => KeyCode::FINANCE, + // 220 => KeyCode::SPORT, + // 221 => KeyCode::SHOP, + // 222 => KeyCode::ALTERASE, + // 223 => KeyCode::CANCEL, + // 224 => KeyCode::BRIGHTNESSDOW, + // 225 => KeyCode::BRIGHTNESSU, + // 226 => KeyCode::MEDIA, + // 227 => KeyCode::SWITCHVIDEOMODE, + // 228 => KeyCode::KBDILLUMTOGGLE, + // 229 => KeyCode::KBDILLUMDOWN, + // 230 => KeyCode::KBDILLUMUP, + // 231 => KeyCode::SEND, + // 232 => KeyCode::REPLY, + // 233 => KeyCode::FORWARDMAIL, + // 234 => KeyCode::SAVE, + // 235 => KeyCode::DOCUMENTS, + // 236 => KeyCode::BATTERY, + // 237 => KeyCode::BLUETOOTH, + // 238 => KeyCode::WLAN, + // 239 => KeyCode::UWB, + 240 => return PhysicalKey::Unidentified(NativeKeyCode::Unidentified), + // 241 => KeyCode::VIDEO_NEXT, + // 242 => KeyCode::VIDEO_PREV, + // 243 => KeyCode::BRIGHTNESS_CYCLE, + // 244 => KeyCode::BRIGHTNESS_AUTO, + // 245 => KeyCode::DISPLAY_OFF, + // 246 => KeyCode::WWAN, + // 247 => KeyCode::RFKILL, + // 248 => KeyCode::KEY_MICMUTE, + _ => return PhysicalKey::Unidentified(NativeKeyCode::Xkb(scancode)), + }) +} + +pub fn physicalkey_to_scancode(key: PhysicalKey) -> Option { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => { + return match code { + NativeKeyCode::Unidentified => Some(240), + NativeKeyCode::Xkb(raw) => Some(raw), + _ => None, + }; + }, + }; + + match code { + KeyCode::Escape => Some(1), + KeyCode::Digit1 => Some(2), + KeyCode::Digit2 => Some(3), + KeyCode::Digit3 => Some(4), + KeyCode::Digit4 => Some(5), + KeyCode::Digit5 => Some(6), + KeyCode::Digit6 => Some(7), + KeyCode::Digit7 => Some(8), + KeyCode::Digit8 => Some(9), + KeyCode::Digit9 => Some(10), + KeyCode::Digit0 => Some(11), + KeyCode::Minus => Some(12), + KeyCode::Equal => Some(13), + KeyCode::Backspace => Some(14), + KeyCode::Tab => Some(15), + KeyCode::KeyQ => Some(16), + KeyCode::KeyW => Some(17), + KeyCode::KeyE => Some(18), + KeyCode::KeyR => Some(19), + KeyCode::KeyT => Some(20), + KeyCode::KeyY => Some(21), + KeyCode::KeyU => Some(22), + KeyCode::KeyI => Some(23), + KeyCode::KeyO => Some(24), + KeyCode::KeyP => Some(25), + KeyCode::BracketLeft => Some(26), + KeyCode::BracketRight => Some(27), + KeyCode::Enter => Some(28), + KeyCode::ControlLeft => Some(29), + KeyCode::KeyA => Some(30), + KeyCode::KeyS => Some(31), + KeyCode::KeyD => Some(32), + KeyCode::KeyF => Some(33), + KeyCode::KeyG => Some(34), + KeyCode::KeyH => Some(35), + KeyCode::KeyJ => Some(36), + KeyCode::KeyK => Some(37), + KeyCode::KeyL => Some(38), + KeyCode::Semicolon => Some(39), + KeyCode::Quote => Some(40), + KeyCode::Backquote => Some(41), + KeyCode::ShiftLeft => Some(42), + KeyCode::Backslash => Some(43), + KeyCode::KeyZ => Some(44), + KeyCode::KeyX => Some(45), + KeyCode::KeyC => Some(46), + KeyCode::KeyV => Some(47), + KeyCode::KeyB => Some(48), + KeyCode::KeyN => Some(49), + KeyCode::KeyM => Some(50), + KeyCode::Comma => Some(51), + KeyCode::Period => Some(52), + KeyCode::Slash => Some(53), + KeyCode::ShiftRight => Some(54), + KeyCode::NumpadMultiply => Some(55), + KeyCode::AltLeft => Some(56), + KeyCode::Space => Some(57), + KeyCode::CapsLock => Some(58), + KeyCode::F1 => Some(59), + KeyCode::F2 => Some(60), + KeyCode::F3 => Some(61), + KeyCode::F4 => Some(62), + KeyCode::F5 => Some(63), + KeyCode::F6 => Some(64), + KeyCode::F7 => Some(65), + KeyCode::F8 => Some(66), + KeyCode::F9 => Some(67), + KeyCode::F10 => Some(68), + KeyCode::NumLock => Some(69), + KeyCode::ScrollLock => Some(70), + KeyCode::Numpad7 => Some(71), + KeyCode::Numpad8 => Some(72), + KeyCode::Numpad9 => Some(73), + KeyCode::NumpadSubtract => Some(74), + KeyCode::Numpad4 => Some(75), + KeyCode::Numpad5 => Some(76), + KeyCode::Numpad6 => Some(77), + KeyCode::NumpadAdd => Some(78), + KeyCode::Numpad1 => Some(79), + KeyCode::Numpad2 => Some(80), + KeyCode::Numpad3 => Some(81), + KeyCode::Numpad0 => Some(82), + KeyCode::NumpadDecimal => Some(83), + KeyCode::Lang5 => Some(85), + KeyCode::IntlBackslash => Some(86), + KeyCode::F11 => Some(87), + KeyCode::F12 => Some(88), + KeyCode::IntlRo => Some(89), + KeyCode::Lang3 => Some(90), + KeyCode::Lang4 => Some(91), + KeyCode::Convert => Some(92), + KeyCode::KanaMode => Some(93), + KeyCode::NonConvert => Some(94), + KeyCode::NumpadEnter => Some(96), + KeyCode::ControlRight => Some(97), + KeyCode::NumpadDivide => Some(98), + KeyCode::PrintScreen => Some(99), + KeyCode::AltRight => Some(100), + KeyCode::Home => Some(102), + KeyCode::ArrowUp => Some(103), + KeyCode::PageUp => Some(104), + KeyCode::ArrowLeft => Some(105), + KeyCode::ArrowRight => Some(106), + KeyCode::End => Some(107), + KeyCode::ArrowDown => Some(108), + KeyCode::PageDown => Some(109), + KeyCode::Insert => Some(110), + KeyCode::Delete => Some(111), + KeyCode::AudioVolumeMute => Some(113), + KeyCode::AudioVolumeDown => Some(114), + KeyCode::AudioVolumeUp => Some(115), + KeyCode::NumpadEqual => Some(117), + KeyCode::Pause => Some(119), + KeyCode::NumpadComma => Some(121), + KeyCode::Lang1 => Some(122), + KeyCode::Lang2 => Some(123), + KeyCode::IntlYen => Some(124), + KeyCode::SuperLeft => Some(125), + KeyCode::SuperRight => Some(126), + KeyCode::ContextMenu => Some(127), + KeyCode::MediaTrackNext => Some(163), + KeyCode::MediaPlayPause => Some(164), + KeyCode::MediaTrackPrevious => Some(165), + KeyCode::MediaStop => Some(166), + KeyCode::F13 => Some(183), + KeyCode::F14 => Some(184), + KeyCode::F15 => Some(185), + KeyCode::F16 => Some(186), + KeyCode::F17 => Some(187), + KeyCode::F18 => Some(188), + KeyCode::F19 => Some(189), + KeyCode::F20 => Some(190), + KeyCode::F21 => Some(191), + KeyCode::F22 => Some(192), + KeyCode::F23 => Some(193), + KeyCode::F24 => Some(194), + _ => None, + } +} + +pub fn keysym_to_key(keysym: u32) -> Key { + use xkbcommon_dl::keysyms; + Key::Named(match keysym { + // TTY function keys + keysyms::BackSpace => NamedKey::Backspace, + keysyms::Tab => NamedKey::Tab, + // keysyms::Linefeed => NamedKey::Linefeed, + keysyms::Clear => NamedKey::Clear, + keysyms::Return => NamedKey::Enter, + keysyms::Pause => NamedKey::Pause, + keysyms::Scroll_Lock => NamedKey::ScrollLock, + keysyms::Sys_Req => NamedKey::PrintScreen, + keysyms::Escape => NamedKey::Escape, + keysyms::Delete => NamedKey::Delete, + + // IME keys + keysyms::Multi_key => NamedKey::Compose, + keysyms::Codeinput => NamedKey::CodeInput, + keysyms::SingleCandidate => NamedKey::SingleCandidate, + keysyms::MultipleCandidate => NamedKey::AllCandidates, + keysyms::PreviousCandidate => NamedKey::PreviousCandidate, + + // Japanese keys + keysyms::Kanji => NamedKey::KanjiMode, + keysyms::Muhenkan => NamedKey::NonConvert, + keysyms::Henkan_Mode => NamedKey::Convert, + keysyms::Romaji => NamedKey::Romaji, + keysyms::Hiragana => NamedKey::Hiragana, + keysyms::Hiragana_Katakana => NamedKey::HiraganaKatakana, + keysyms::Zenkaku => NamedKey::Zenkaku, + keysyms::Hankaku => NamedKey::Hankaku, + keysyms::Zenkaku_Hankaku => NamedKey::ZenkakuHankaku, + // keysyms::Touroku => NamedKey::Touroku, + // keysyms::Massyo => NamedKey::Massyo, + keysyms::Kana_Lock => NamedKey::KanaMode, + keysyms::Kana_Shift => NamedKey::KanaMode, + keysyms::Eisu_Shift => NamedKey::Alphanumeric, + keysyms::Eisu_toggle => NamedKey::Alphanumeric, + // NOTE: The next three items are aliases for values we've already mapped. + // keysyms::Kanji_Bangou => NamedKey::CodeInput, + // keysyms::Zen_Koho => NamedKey::AllCandidates, + // keysyms::Mae_Koho => NamedKey::PreviousCandidate, + + // Cursor control & motion + keysyms::Home => NamedKey::Home, + keysyms::Left => NamedKey::ArrowLeft, + keysyms::Up => NamedKey::ArrowUp, + keysyms::Right => NamedKey::ArrowRight, + keysyms::Down => NamedKey::ArrowDown, + // keysyms::Prior => NamedKey::PageUp, + keysyms::Page_Up => NamedKey::PageUp, + // keysyms::Next => NamedKey::PageDown, + keysyms::Page_Down => NamedKey::PageDown, + keysyms::End => NamedKey::End, + // keysyms::Begin => NamedKey::Begin, + + // Misc. functions + keysyms::Select => NamedKey::Select, + keysyms::Print => NamedKey::PrintScreen, + keysyms::Execute => NamedKey::Execute, + keysyms::Insert => NamedKey::Insert, + keysyms::Undo => NamedKey::Undo, + keysyms::Redo => NamedKey::Redo, + keysyms::Menu => NamedKey::ContextMenu, + keysyms::Find => NamedKey::Find, + keysyms::Cancel => NamedKey::Cancel, + keysyms::Help => NamedKey::Help, + keysyms::Break => NamedKey::Pause, + keysyms::Mode_switch => NamedKey::ModeChange, + // keysyms::script_switch => NamedKey::ModeChange, + keysyms::Num_Lock => NamedKey::NumLock, + + // Keypad keys + // keysyms::KP_Space => return Key::Character(" "), + keysyms::KP_Tab => NamedKey::Tab, + keysyms::KP_Enter => NamedKey::Enter, + keysyms::KP_F1 => NamedKey::F1, + keysyms::KP_F2 => NamedKey::F2, + keysyms::KP_F3 => NamedKey::F3, + keysyms::KP_F4 => NamedKey::F4, + keysyms::KP_Home => NamedKey::Home, + keysyms::KP_Left => NamedKey::ArrowLeft, + keysyms::KP_Up => NamedKey::ArrowUp, + keysyms::KP_Right => NamedKey::ArrowRight, + keysyms::KP_Down => NamedKey::ArrowDown, + // keysyms::KP_Prior => NamedKey::PageUp, + keysyms::KP_Page_Up => NamedKey::PageUp, + // keysyms::KP_Next => NamedKey::PageDown, + keysyms::KP_Page_Down => NamedKey::PageDown, + keysyms::KP_End => NamedKey::End, + // This is the key labeled "5" on the numpad when NumLock is off. + // keysyms::KP_Begin => NamedKey::Begin, + keysyms::KP_Insert => NamedKey::Insert, + keysyms::KP_Delete => NamedKey::Delete, + // keysyms::KP_Equal => NamedKey::Equal, + // keysyms::KP_Multiply => NamedKey::Multiply, + // keysyms::KP_Add => NamedKey::Add, + // keysyms::KP_Separator => NamedKey::Separator, + // keysyms::KP_Subtract => NamedKey::Subtract, + // keysyms::KP_Decimal => NamedKey::Decimal, + // keysyms::KP_Divide => NamedKey::Divide, + + // keysyms::KP_0 => return Key::Character("0"), + // keysyms::KP_1 => return Key::Character("1"), + // keysyms::KP_2 => return Key::Character("2"), + // keysyms::KP_3 => return Key::Character("3"), + // keysyms::KP_4 => return Key::Character("4"), + // keysyms::KP_5 => return Key::Character("5"), + // keysyms::KP_6 => return Key::Character("6"), + // keysyms::KP_7 => return Key::Character("7"), + // keysyms::KP_8 => return Key::Character("8"), + // keysyms::KP_9 => return Key::Character("9"), + + // Function keys + keysyms::F1 => NamedKey::F1, + keysyms::F2 => NamedKey::F2, + keysyms::F3 => NamedKey::F3, + keysyms::F4 => NamedKey::F4, + keysyms::F5 => NamedKey::F5, + keysyms::F6 => NamedKey::F6, + keysyms::F7 => NamedKey::F7, + keysyms::F8 => NamedKey::F8, + keysyms::F9 => NamedKey::F9, + keysyms::F10 => NamedKey::F10, + keysyms::F11 => NamedKey::F11, + keysyms::F12 => NamedKey::F12, + keysyms::F13 => NamedKey::F13, + keysyms::F14 => NamedKey::F14, + keysyms::F15 => NamedKey::F15, + keysyms::F16 => NamedKey::F16, + keysyms::F17 => NamedKey::F17, + keysyms::F18 => NamedKey::F18, + keysyms::F19 => NamedKey::F19, + keysyms::F20 => NamedKey::F20, + keysyms::F21 => NamedKey::F21, + keysyms::F22 => NamedKey::F22, + keysyms::F23 => NamedKey::F23, + keysyms::F24 => NamedKey::F24, + keysyms::F25 => NamedKey::F25, + keysyms::F26 => NamedKey::F26, + keysyms::F27 => NamedKey::F27, + keysyms::F28 => NamedKey::F28, + keysyms::F29 => NamedKey::F29, + keysyms::F30 => NamedKey::F30, + keysyms::F31 => NamedKey::F31, + keysyms::F32 => NamedKey::F32, + keysyms::F33 => NamedKey::F33, + keysyms::F34 => NamedKey::F34, + keysyms::F35 => NamedKey::F35, + + // Modifiers + keysyms::Shift_L => NamedKey::Shift, + keysyms::Shift_R => NamedKey::Shift, + keysyms::Control_L => NamedKey::Control, + keysyms::Control_R => NamedKey::Control, + keysyms::Caps_Lock => NamedKey::CapsLock, + // keysyms::Shift_Lock => NamedKey::ShiftLock, + + // keysyms::Meta_L => NamedKey::Meta, + // keysyms::Meta_R => NamedKey::Meta, + keysyms::Alt_L => NamedKey::Alt, + keysyms::Alt_R => NamedKey::Alt, + keysyms::Super_L => NamedKey::Super, + keysyms::Super_R => NamedKey::Super, + keysyms::Hyper_L => NamedKey::Hyper, + keysyms::Hyper_R => NamedKey::Hyper, + + // XKB function and modifier keys + // keysyms::ISO_Lock => NamedKey::IsoLock, + // keysyms::ISO_Level2_Latch => NamedKey::IsoLevel2Latch, + keysyms::ISO_Level3_Shift => NamedKey::AltGraph, + keysyms::ISO_Level3_Latch => NamedKey::AltGraph, + keysyms::ISO_Level3_Lock => NamedKey::AltGraph, + // keysyms::ISO_Level5_Shift => NamedKey::IsoLevel5Shift, + // keysyms::ISO_Level5_Latch => NamedKey::IsoLevel5Latch, + // keysyms::ISO_Level5_Lock => NamedKey::IsoLevel5Lock, + // keysyms::ISO_Group_Shift => NamedKey::IsoGroupShift, + // keysyms::ISO_Group_Latch => NamedKey::IsoGroupLatch, + // keysyms::ISO_Group_Lock => NamedKey::IsoGroupLock, + keysyms::ISO_Next_Group => NamedKey::GroupNext, + // keysyms::ISO_Next_Group_Lock => NamedKey::GroupNextLock, + keysyms::ISO_Prev_Group => NamedKey::GroupPrevious, + // keysyms::ISO_Prev_Group_Lock => NamedKey::GroupPreviousLock, + keysyms::ISO_First_Group => NamedKey::GroupFirst, + // keysyms::ISO_First_Group_Lock => NamedKey::GroupFirstLock, + keysyms::ISO_Last_Group => NamedKey::GroupLast, + // keysyms::ISO_Last_Group_Lock => NamedKey::GroupLastLock, + keysyms::ISO_Left_Tab => NamedKey::Tab, + // keysyms::ISO_Move_Line_Up => NamedKey::IsoMoveLineUp, + // keysyms::ISO_Move_Line_Down => NamedKey::IsoMoveLineDown, + // keysyms::ISO_Partial_Line_Up => NamedKey::IsoPartialLineUp, + // keysyms::ISO_Partial_Line_Down => NamedKey::IsoPartialLineDown, + // keysyms::ISO_Partial_Space_Left => NamedKey::IsoPartialSpaceLeft, + // keysyms::ISO_Partial_Space_Right => NamedKey::IsoPartialSpaceRight, + // keysyms::ISO_Set_Margin_Left => NamedKey::IsoSetMarginLeft, + // keysyms::ISO_Set_Margin_Right => NamedKey::IsoSetMarginRight, + // keysyms::ISO_Release_Margin_Left => NamedKey::IsoReleaseMarginLeft, + // keysyms::ISO_Release_Margin_Right => NamedKey::IsoReleaseMarginRight, + // keysyms::ISO_Release_Both_Margins => NamedKey::IsoReleaseBothMargins, + // keysyms::ISO_Fast_Cursor_Left => NamedKey::IsoFastCursorLeft, + // keysyms::ISO_Fast_Cursor_Right => NamedKey::IsoFastCursorRight, + // keysyms::ISO_Fast_Cursor_Up => NamedKey::IsoFastCursorUp, + // keysyms::ISO_Fast_Cursor_Down => NamedKey::IsoFastCursorDown, + // keysyms::ISO_Continuous_Underline => NamedKey::IsoContinuousUnderline, + // keysyms::ISO_Discontinuous_Underline => NamedKey::IsoDiscontinuousUnderline, + // keysyms::ISO_Emphasize => NamedKey::IsoEmphasize, + // keysyms::ISO_Center_Object => NamedKey::IsoCenterObject, + keysyms::ISO_Enter => NamedKey::Enter, + + // dead_grave..dead_currency + + // dead_lowline..dead_longsolidusoverlay + + // dead_a..dead_capital_schwa + + // dead_greek + + // First_Virtual_Screen..Terminate_Server + + // AccessX_Enable..AudibleBell_Enable + + // Pointer_Left..Pointer_Drag5 + + // Pointer_EnableKeys..Pointer_DfltBtnPrev + + // ch..C_H + + // 3270 terminal keys + // keysyms::3270_Duplicate => NamedKey::Duplicate, + // keysyms::3270_FieldMark => NamedKey::FieldMark, + // keysyms::3270_Right2 => NamedKey::Right2, + // keysyms::3270_Left2 => NamedKey::Left2, + // keysyms::3270_BackTab => NamedKey::BackTab, + keysyms::_3270_EraseEOF => NamedKey::EraseEof, + // keysyms::3270_EraseInput => NamedKey::EraseInput, + // keysyms::3270_Reset => NamedKey::Reset, + // keysyms::3270_Quit => NamedKey::Quit, + // keysyms::3270_PA1 => NamedKey::Pa1, + // keysyms::3270_PA2 => NamedKey::Pa2, + // keysyms::3270_PA3 => NamedKey::Pa3, + // keysyms::3270_Test => NamedKey::Test, + keysyms::_3270_Attn => NamedKey::Attn, + // keysyms::3270_CursorBlink => NamedKey::CursorBlink, + // keysyms::3270_AltCursor => NamedKey::AltCursor, + // keysyms::3270_KeyClick => NamedKey::KeyClick, + // keysyms::3270_Jump => NamedKey::Jump, + // keysyms::3270_Ident => NamedKey::Ident, + // keysyms::3270_Rule => NamedKey::Rule, + // keysyms::3270_Copy => NamedKey::Copy, + keysyms::_3270_Play => NamedKey::Play, + // keysyms::3270_Setup => NamedKey::Setup, + // keysyms::3270_Record => NamedKey::Record, + // keysyms::3270_ChangeScreen => NamedKey::ChangeScreen, + // keysyms::3270_DeleteWord => NamedKey::DeleteWord, + keysyms::_3270_ExSelect => NamedKey::ExSel, + keysyms::_3270_CursorSelect => NamedKey::CrSel, + keysyms::_3270_PrintScreen => NamedKey::PrintScreen, + keysyms::_3270_Enter => NamedKey::Enter, + + keysyms::space => NamedKey::Space, + // exclam..Sinh_kunddaliya + + // XFree86 + // keysyms::XF86_ModeLock => NamedKey::ModeLock, + + // XFree86 - Backlight controls + keysyms::XF86_MonBrightnessUp => NamedKey::BrightnessUp, + keysyms::XF86_MonBrightnessDown => NamedKey::BrightnessDown, + // keysyms::XF86_KbdLightOnOff => NamedKey::LightOnOff, + // keysyms::XF86_KbdBrightnessUp => NamedKey::KeyboardBrightnessUp, + // keysyms::XF86_KbdBrightnessDown => NamedKey::KeyboardBrightnessDown, + + // XFree86 - "Internet" + keysyms::XF86_Standby => NamedKey::Standby, + keysyms::XF86_AudioLowerVolume => NamedKey::AudioVolumeDown, + keysyms::XF86_AudioRaiseVolume => NamedKey::AudioVolumeUp, + keysyms::XF86_AudioPlay => NamedKey::MediaPlay, + keysyms::XF86_AudioStop => NamedKey::MediaStop, + keysyms::XF86_AudioPrev => NamedKey::MediaTrackPrevious, + keysyms::XF86_AudioNext => NamedKey::MediaTrackNext, + keysyms::XF86_HomePage => NamedKey::BrowserHome, + keysyms::XF86_Mail => NamedKey::LaunchMail, + // keysyms::XF86_Start => NamedKey::Start, + keysyms::XF86_Search => NamedKey::BrowserSearch, + keysyms::XF86_AudioRecord => NamedKey::MediaRecord, + + // XFree86 - PDA + keysyms::XF86_Calculator => NamedKey::LaunchApplication2, + // keysyms::XF86_Memo => NamedKey::Memo, + // keysyms::XF86_ToDoList => NamedKey::ToDoList, + keysyms::XF86_Calendar => NamedKey::LaunchCalendar, + keysyms::XF86_PowerDown => NamedKey::Power, + // keysyms::XF86_ContrastAdjust => NamedKey::AdjustContrast, + // keysyms::XF86_RockerUp => NamedKey::RockerUp, + // keysyms::XF86_RockerDown => NamedKey::RockerDown, + // keysyms::XF86_RockerEnter => NamedKey::RockerEnter, + + // XFree86 - More "Internet" + keysyms::XF86_Back => NamedKey::BrowserBack, + keysyms::XF86_Forward => NamedKey::BrowserForward, + // keysyms::XF86_Stop => NamedKey::Stop, + keysyms::XF86_Refresh => NamedKey::BrowserRefresh, + keysyms::XF86_PowerOff => NamedKey::Power, + keysyms::XF86_WakeUp => NamedKey::WakeUp, + keysyms::XF86_Eject => NamedKey::Eject, + keysyms::XF86_ScreenSaver => NamedKey::LaunchScreenSaver, + keysyms::XF86_WWW => NamedKey::LaunchWebBrowser, + keysyms::XF86_Sleep => NamedKey::Standby, + keysyms::XF86_Favorites => NamedKey::BrowserFavorites, + keysyms::XF86_AudioPause => NamedKey::MediaPause, + // keysyms::XF86_AudioMedia => NamedKey::AudioMedia, + keysyms::XF86_MyComputer => NamedKey::LaunchApplication1, + // keysyms::XF86_VendorHome => NamedKey::VendorHome, + // keysyms::XF86_LightBulb => NamedKey::LightBulb, + // keysyms::XF86_Shop => NamedKey::BrowserShop, + // keysyms::XF86_History => NamedKey::BrowserHistory, + // keysyms::XF86_OpenURL => NamedKey::OpenUrl, + // keysyms::XF86_AddFavorite => NamedKey::AddFavorite, + // keysyms::XF86_HotLinks => NamedKey::HotLinks, + // keysyms::XF86_BrightnessAdjust => NamedKey::BrightnessAdjust, + // keysyms::XF86_Finance => NamedKey::BrowserFinance, + // keysyms::XF86_Community => NamedKey::BrowserCommunity, + keysyms::XF86_AudioRewind => NamedKey::MediaRewind, + // keysyms::XF86_BackForward => Key::???, + // XF86_Launch0..XF86_LaunchF + + // XF86_ApplicationLeft..XF86_CD + keysyms::XF86_Calculater => NamedKey::LaunchApplication2, // Nice typo, libxkbcommon :) + // XF86_Clear + keysyms::XF86_Close => NamedKey::Close, + keysyms::XF86_Copy => NamedKey::Copy, + keysyms::XF86_Cut => NamedKey::Cut, + // XF86_Display..XF86_Documents + keysyms::XF86_Excel => NamedKey::LaunchSpreadsheet, + // XF86_Explorer..XF86iTouch + keysyms::XF86_LogOff => NamedKey::LogOff, + // XF86_Market..XF86_MenuPB + keysyms::XF86_MySites => NamedKey::BrowserFavorites, + keysyms::XF86_New => NamedKey::New, + // XF86_News..XF86_OfficeHome + keysyms::XF86_Open => NamedKey::Open, + // XF86_Option + keysyms::XF86_Paste => NamedKey::Paste, + keysyms::XF86_Phone => NamedKey::LaunchPhone, + // XF86_Q + keysyms::XF86_Reply => NamedKey::MailReply, + keysyms::XF86_Reload => NamedKey::BrowserRefresh, + // XF86_RotateWindows..XF86_RotationKB + keysyms::XF86_Save => NamedKey::Save, + // XF86_ScrollUp..XF86_ScrollClick + keysyms::XF86_Send => NamedKey::MailSend, + keysyms::XF86_Spell => NamedKey::SpellCheck, + keysyms::XF86_SplitScreen => NamedKey::SplitScreenToggle, + // XF86_Support..XF86_User2KB + keysyms::XF86_Video => NamedKey::LaunchMediaPlayer, + // XF86_WheelButton + keysyms::XF86_Word => NamedKey::LaunchWordProcessor, + // XF86_Xfer + keysyms::XF86_ZoomIn => NamedKey::ZoomIn, + keysyms::XF86_ZoomOut => NamedKey::ZoomOut, + + // XF86_Away..XF86_Messenger + keysyms::XF86_WebCam => NamedKey::LaunchWebCam, + keysyms::XF86_MailForward => NamedKey::MailForward, + // XF86_Pictures + keysyms::XF86_Music => NamedKey::LaunchMusicPlayer, + + // XF86_Battery..XF86_UWB + keysyms::XF86_AudioForward => NamedKey::MediaFastForward, + // XF86_AudioRepeat + keysyms::XF86_AudioRandomPlay => NamedKey::RandomToggle, + keysyms::XF86_Subtitle => NamedKey::Subtitle, + keysyms::XF86_AudioCycleTrack => NamedKey::MediaAudioTrack, + // XF86_CycleAngle..XF86_Blue + keysyms::XF86_Suspend => NamedKey::Standby, + keysyms::XF86_Hibernate => NamedKey::Hibernate, + // XF86_TouchpadToggle..XF86_TouchpadOff + keysyms::XF86_AudioMute => NamedKey::AudioVolumeMute, + + // XF86_Switch_VT_1..XF86_Switch_VT_12 + + // XF86_Ungrab..XF86_ClearGrab + keysyms::XF86_Next_VMode => NamedKey::VideoModeNext, + // keysyms::XF86_Prev_VMode => NamedKey::VideoModePrevious, + // XF86_LogWindowTree..XF86_LogGrabInfo + + // SunFA_Grave..SunFA_Cedilla + + // keysyms::SunF36 => NamedKey::F36 | NamedKey::F11, + // keysyms::SunF37 => NamedKey::F37 | NamedKey::F12, + + // keysyms::SunSys_Req => NamedKey::PrintScreen, + // The next couple of xkb (until SunStop) are already handled. + // SunPrint_Screen..SunPageDown + + // SunUndo..SunFront + keysyms::SUN_Copy => NamedKey::Copy, + keysyms::SUN_Open => NamedKey::Open, + keysyms::SUN_Paste => NamedKey::Paste, + keysyms::SUN_Cut => NamedKey::Cut, + + // SunPowerSwitch + keysyms::SUN_AudioLowerVolume => NamedKey::AudioVolumeDown, + keysyms::SUN_AudioMute => NamedKey::AudioVolumeMute, + keysyms::SUN_AudioRaiseVolume => NamedKey::AudioVolumeUp, + // SUN_VideoDegauss + keysyms::SUN_VideoLowerBrightness => NamedKey::BrightnessDown, + keysyms::SUN_VideoRaiseBrightness => NamedKey::BrightnessUp, + // SunPowerSwitchShift + 0 => return Key::Unidentified(NativeKey::Unidentified), + _ => return Key::Unidentified(NativeKey::Xkb(keysym)), + }) +} + +pub fn keysym_location(keysym: u32) -> KeyLocation { + use xkbcommon_dl::keysyms; + match keysym { + keysyms::Shift_L + | keysyms::Control_L + | keysyms::Meta_L + | keysyms::Alt_L + | keysyms::Super_L + | keysyms::Hyper_L => KeyLocation::Left, + keysyms::Shift_R + | keysyms::Control_R + | keysyms::Meta_R + | keysyms::Alt_R + | keysyms::Super_R + | keysyms::Hyper_R => KeyLocation::Right, + keysyms::KP_0 + | keysyms::KP_1 + | keysyms::KP_2 + | keysyms::KP_3 + | keysyms::KP_4 + | keysyms::KP_5 + | keysyms::KP_6 + | keysyms::KP_7 + | keysyms::KP_8 + | keysyms::KP_9 + | keysyms::KP_Space + | keysyms::KP_Tab + | keysyms::KP_Enter + | keysyms::KP_F1 + | keysyms::KP_F2 + | keysyms::KP_F3 + | keysyms::KP_F4 + | keysyms::KP_Home + | keysyms::KP_Left + | keysyms::KP_Up + | keysyms::KP_Right + | keysyms::KP_Down + | keysyms::KP_Page_Up + | keysyms::KP_Page_Down + | keysyms::KP_End + | keysyms::KP_Begin + | keysyms::KP_Insert + | keysyms::KP_Delete + | keysyms::KP_Equal + | keysyms::KP_Multiply + | keysyms::KP_Add + | keysyms::KP_Separator + | keysyms::KP_Subtract + | keysyms::KP_Decimal + | keysyms::KP_Divide => KeyLocation::Numpad, + _ => KeyLocation::Standard, + } +} + +#[derive(Debug)] +pub struct XkbKeymap { + keymap: NonNull, + _mods_indices: ModsIndices, + pub _core_keyboard_id: i32, +} + +impl XkbKeymap { + #[cfg(wayland_platform)] + pub fn from_fd(context: &XkbContext, fd: OwnedFd, size: usize) -> Option { + let map = unsafe { MmapOptions::new().len(size).map_copy_read_only(&fd).ok()? }; + + let keymap = unsafe { + let keymap = (XKBH.xkb_keymap_new_from_string)( + (*context).as_ptr(), + map.as_ptr() as *const _, + xkb::xkb_keymap_format::XKB_KEYMAP_FORMAT_TEXT_V1, + xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ); + NonNull::new(keymap)? + }; + + Some(Self::new_inner(keymap, 0)) + } + + #[cfg(x11_platform)] + pub fn from_x11_keymap( + context: &XkbContext, + xcb: *mut xcb_connection_t, + core_keyboard_id: i32, + ) -> Option { + let keymap = unsafe { + (XKBXH.xkb_x11_keymap_new_from_device)( + context.as_ptr(), + xcb, + core_keyboard_id, + xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ) + }; + let keymap = NonNull::new(keymap)?; + Some(Self::new_inner(keymap, core_keyboard_id)) + } + + fn new_inner(keymap: NonNull, _core_keyboard_id: i32) -> Self { + let mods_indices = ModsIndices { + shift: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_SHIFT), + caps: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_CAPS), + ctrl: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_CTRL), + alt: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_ALT), + num: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_NUM), + mod3: mod_index_for_name(keymap, b"Mod3\0"), + logo: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_LOGO), + mod5: mod_index_for_name(keymap, b"Mod5\0"), + }; + + Self { keymap, _mods_indices: mods_indices, _core_keyboard_id } + } + + #[cfg(x11_platform)] + pub fn mods_indices(&self) -> ModsIndices { + self._mods_indices + } + + pub fn first_keysym_by_level( + &mut self, + layout: xkb_layout_index_t, + keycode: xkb_keycode_t, + ) -> xkb_keysym_t { + unsafe { + let mut keysyms = ptr::null(); + let count = (XKBH.xkb_keymap_key_get_syms_by_level)( + self.keymap.as_ptr(), + keycode, + layout, + // NOTE: The level should be zero to ignore modifiers. + 0, + &mut keysyms, + ); + + if count == 1 { + *keysyms + } else { + 0 + } + } + } + + /// Check whether the given key repeats. + pub fn key_repeats(&mut self, keycode: xkb_keycode_t) -> bool { + unsafe { (XKBH.xkb_keymap_key_repeats)(self.keymap.as_ptr(), keycode) == 1 } + } +} + +impl Drop for XkbKeymap { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_keymap_unref)(self.keymap.as_ptr()); + }; + } +} + +impl Deref for XkbKeymap { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.keymap + } +} + +/// Modifier index in the keymap. +#[cfg_attr(not(x11_platform), allow(dead_code))] +#[derive(Default, Debug, Clone, Copy)] +pub struct ModsIndices { + pub shift: Option, + pub caps: Option, + pub ctrl: Option, + pub alt: Option, + pub num: Option, + pub mod3: Option, + pub logo: Option, + pub mod5: Option, +} + +fn mod_index_for_name(keymap: NonNull, name: &[u8]) -> Option { + unsafe { + let mod_index = + (XKBH.xkb_keymap_mod_get_index)(keymap.as_ptr(), name.as_ptr() as *const c_char); + if mod_index == XKB_MOD_INVALID { + None + } else { + Some(mod_index) + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/mod.rs new file mode 100644 index 0000000..706397f --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/mod.rs @@ -0,0 +1,416 @@ +use std::ops::Deref; +use std::os::raw::c_char; +use std::ptr::{self, NonNull}; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::utils::Lazy; +use smol_str::SmolStr; +#[cfg(wayland_platform)] +use std::os::unix::io::OwnedFd; +use tracing::warn; +use xkbcommon_dl::{ + self as xkb, xkb_compose_status, xkb_context, xkb_context_flags, xkbcommon_compose_handle, + xkbcommon_handle, XkbCommon, XkbCommonCompose, +}; +#[cfg(x11_platform)] +use {x11_dl::xlib_xcb::xcb_connection_t, xkbcommon_dl::x11::xkbcommon_x11_handle}; + +use crate::event::{ElementState, KeyEvent}; +use crate::keyboard::{Key, KeyLocation}; +use crate::platform_impl::KeyEventExtra; + +mod compose; +mod keymap; +mod state; + +use compose::{ComposeStatus, XkbComposeState, XkbComposeTable}; +use keymap::XkbKeymap; + +#[cfg(x11_platform)] +pub use keymap::raw_keycode_to_physicalkey; +pub use keymap::{physicalkey_to_scancode, scancode_to_physicalkey}; +pub use state::XkbState; + +// TODO: Wire this up without using a static `AtomicBool`. +static RESET_DEAD_KEYS: AtomicBool = AtomicBool::new(false); + +static XKBH: Lazy<&'static XkbCommon> = Lazy::new(xkbcommon_handle); +static XKBCH: Lazy<&'static XkbCommonCompose> = Lazy::new(xkbcommon_compose_handle); +#[cfg(feature = "x11")] +static XKBXH: Lazy<&'static xkb::x11::XkbCommonX11> = Lazy::new(xkbcommon_x11_handle); + +#[inline(always)] +pub fn reset_dead_keys() { + RESET_DEAD_KEYS.store(true, Ordering::SeqCst); +} + +#[derive(Debug)] +pub enum Error { + /// libxkbcommon is not available + XKBNotFound, +} + +#[derive(Debug)] +pub struct Context { + // NOTE: field order matters. + #[cfg(x11_platform)] + pub core_keyboard_id: i32, + state: Option, + keymap: Option, + compose_state1: Option, + compose_state2: Option, + _compose_table: Option, + context: XkbContext, + scratch_buffer: Vec, +} + +impl Context { + pub fn new() -> Result { + if xkb::xkbcommon_option().is_none() { + return Err(Error::XKBNotFound); + } + + let context = XkbContext::new()?; + let mut compose_table = XkbComposeTable::new(&context); + let mut compose_state1 = compose_table.as_ref().and_then(|table| table.new_state()); + let mut compose_state2 = compose_table.as_ref().and_then(|table| table.new_state()); + + // Disable compose if anything compose related failed to initialize. + if compose_table.is_none() || compose_state1.is_none() || compose_state2.is_none() { + compose_state2 = None; + compose_state1 = None; + compose_table = None; + } + + Ok(Self { + state: None, + keymap: None, + compose_state1, + compose_state2, + #[cfg(x11_platform)] + core_keyboard_id: 0, + _compose_table: compose_table, + context, + scratch_buffer: Vec::with_capacity(8), + }) + } + + #[cfg(feature = "x11")] + pub fn from_x11_xkb(xcb: *mut xcb_connection_t) -> Result { + let result = unsafe { + (XKBXH.xkb_x11_setup_xkb_extension)( + xcb, + 1, + 2, + xkbcommon_dl::x11::xkb_x11_setup_xkb_extension_flags::XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + if result != 1 { + return Err(Error::XKBNotFound); + } + + let mut this = Self::new()?; + this.core_keyboard_id = unsafe { (XKBXH.xkb_x11_get_core_keyboard_device_id)(xcb) }; + this.set_keymap_from_x11(xcb); + Ok(this) + } + + pub fn state_mut(&mut self) -> Option<&mut XkbState> { + self.state.as_mut() + } + + pub fn keymap_mut(&mut self) -> Option<&mut XkbKeymap> { + self.keymap.as_mut() + } + + #[cfg(wayland_platform)] + pub fn set_keymap_from_fd(&mut self, fd: OwnedFd, size: usize) { + let keymap = XkbKeymap::from_fd(&self.context, fd, size); + let state = keymap.as_ref().and_then(XkbState::new_wayland); + if keymap.is_none() || state.is_none() { + warn!("failed to update xkb keymap"); + } + self.state = state; + self.keymap = keymap; + } + + #[cfg(x11_platform)] + pub fn set_keymap_from_x11(&mut self, xcb: *mut xcb_connection_t) { + let keymap = XkbKeymap::from_x11_keymap(&self.context, xcb, self.core_keyboard_id); + let state = keymap.as_ref().and_then(|keymap| XkbState::new_x11(xcb, keymap)); + if keymap.is_none() || state.is_none() { + warn!("failed to update xkb keymap"); + } + self.state = state; + self.keymap = keymap; + } + + /// Key builder context with the user provided xkb state. + pub fn key_context(&mut self) -> Option> { + let state = self.state.as_mut()?; + let keymap = self.keymap.as_mut()?; + let compose_state1 = self.compose_state1.as_mut(); + let compose_state2 = self.compose_state2.as_mut(); + let scratch_buffer = &mut self.scratch_buffer; + Some(KeyContext { state, keymap, compose_state1, compose_state2, scratch_buffer }) + } + + /// Key builder context with the user provided xkb state. + /// + /// Should be used when the original context must not be altered. + #[cfg(x11_platform)] + pub fn key_context_with_state<'a>( + &'a mut self, + state: &'a mut XkbState, + ) -> Option> { + let keymap = self.keymap.as_mut()?; + let compose_state1 = self.compose_state1.as_mut(); + let compose_state2 = self.compose_state2.as_mut(); + let scratch_buffer = &mut self.scratch_buffer; + Some(KeyContext { state, keymap, compose_state1, compose_state2, scratch_buffer }) + } +} + +pub struct KeyContext<'a> { + pub state: &'a mut XkbState, + pub keymap: &'a mut XkbKeymap, + compose_state1: Option<&'a mut XkbComposeState>, + compose_state2: Option<&'a mut XkbComposeState>, + scratch_buffer: &'a mut Vec, +} + +impl KeyContext<'_> { + pub fn process_key_event( + &mut self, + keycode: u32, + state: ElementState, + repeat: bool, + ) -> KeyEvent { + let mut event = + KeyEventResults::new(self, keycode, !repeat && state == ElementState::Pressed); + let physical_key = keymap::raw_keycode_to_physicalkey(keycode); + let (logical_key, location) = event.key(); + let text = event.text(); + let (key_without_modifiers, _) = event.key_without_modifiers(); + let text_with_all_modifiers = event.text_with_all_modifiers(); + + let platform_specific = KeyEventExtra { text_with_all_modifiers, key_without_modifiers }; + + KeyEvent { physical_key, logical_key, text, location, state, repeat, platform_specific } + } + + fn keysym_to_utf8_raw(&mut self, keysym: u32) -> Option { + self.scratch_buffer.clear(); + self.scratch_buffer.reserve(8); + loop { + let bytes_written = unsafe { + (XKBH.xkb_keysym_to_utf8)( + keysym, + self.scratch_buffer.as_mut_ptr().cast(), + self.scratch_buffer.capacity(), + ) + }; + if bytes_written == 0 { + return None; + } else if bytes_written == -1 { + self.scratch_buffer.reserve(8); + } else { + unsafe { self.scratch_buffer.set_len(bytes_written.try_into().unwrap()) }; + break; + } + } + + // Remove the null-terminator + self.scratch_buffer.pop(); + byte_slice_to_smol_str(self.scratch_buffer) + } +} + +struct KeyEventResults<'a, 'b> { + context: &'a mut KeyContext<'b>, + keycode: u32, + keysym: u32, + compose: ComposeStatus, +} + +impl<'a, 'b> KeyEventResults<'a, 'b> { + fn new(context: &'a mut KeyContext<'b>, keycode: u32, compose: bool) -> Self { + let keysym = context.state.get_one_sym_raw(keycode); + + let compose = if let Some(state) = context.compose_state1.as_mut().filter(|_| compose) { + if RESET_DEAD_KEYS.swap(false, Ordering::SeqCst) { + state.reset(); + context.compose_state2.as_mut().unwrap().reset(); + } + state.feed(keysym) + } else { + ComposeStatus::None + }; + + KeyEventResults { context, keycode, keysym, compose } + } + + pub fn key(&mut self) -> (Key, KeyLocation) { + let (key, location) = match self.keysym_to_key(self.keysym) { + Ok(known) => return known, + Err(undefined) => undefined, + }; + + if let ComposeStatus::Accepted(xkb_compose_status::XKB_COMPOSE_COMPOSING) = self.compose { + let compose_state = self.context.compose_state2.as_mut().unwrap(); + // When pressing a dead key twice, the non-combining variant of that character will + // be produced. Since this function only concerns itself with a single keypress, we + // simulate this double press here by feeding the keysym to the compose state + // twice. + + compose_state.feed(self.keysym); + if matches!(compose_state.feed(self.keysym), ComposeStatus::Accepted(_)) { + // Extracting only a single `char` here *should* be fine, assuming that no + // dead key's non-combining variant ever occupies more than one `char`. + let text = compose_state.get_string(self.context.scratch_buffer); + let key = Key::Dead(text.and_then(|s| s.chars().next())); + (key, location) + } else { + (key, location) + } + } else { + let key = self + .composed_text() + .unwrap_or_else(|_| self.context.keysym_to_utf8_raw(self.keysym)) + .map(Key::Character) + .unwrap_or(key); + (key, location) + } + } + + pub fn key_without_modifiers(&mut self) -> (Key, KeyLocation) { + // This will become a pointer to an array which libxkbcommon owns, so we don't need to + // deallocate it. + let layout = self.context.state.layout(self.keycode); + let keysym = self.context.keymap.first_keysym_by_level(layout, self.keycode); + + match self.keysym_to_key(keysym) { + Ok((key, location)) => (key, location), + Err((key, location)) => { + let key = + self.context.keysym_to_utf8_raw(keysym).map(Key::Character).unwrap_or(key); + (key, location) + }, + } + } + + fn keysym_to_key(&self, keysym: u32) -> Result<(Key, KeyLocation), (Key, KeyLocation)> { + let location = keymap::keysym_location(keysym); + let key = keymap::keysym_to_key(keysym); + if matches!(key, Key::Unidentified(_)) { + Err((key, location)) + } else { + Ok((key, location)) + } + } + + pub fn text(&mut self) -> Option { + self.composed_text().unwrap_or_else(|_| self.context.keysym_to_utf8_raw(self.keysym)) + } + + // The current behaviour makes it so composing a character overrides attempts to input a + // control character with the `Ctrl` key. We can potentially add a configuration option + // if someone specifically wants the opposite behaviour. + pub fn text_with_all_modifiers(&mut self) -> Option { + match self.composed_text() { + Ok(text) => text, + Err(_) => self.context.state.get_utf8_raw(self.keycode, self.context.scratch_buffer), + } + } + + fn composed_text(&mut self) -> Result, ()> { + match self.compose { + ComposeStatus::Accepted(status) => match status { + xkb_compose_status::XKB_COMPOSE_COMPOSED => { + let state = self.context.compose_state1.as_mut().unwrap(); + Ok(state.get_string(self.context.scratch_buffer)) + }, + xkb_compose_status::XKB_COMPOSE_COMPOSING + | xkb_compose_status::XKB_COMPOSE_CANCELLED => Ok(None), + xkb_compose_status::XKB_COMPOSE_NOTHING => Err(()), + }, + _ => Err(()), + } + } +} + +#[derive(Debug)] +pub struct XkbContext { + context: NonNull, +} + +impl XkbContext { + pub fn new() -> Result { + let context = unsafe { (XKBH.xkb_context_new)(xkb_context_flags::XKB_CONTEXT_NO_FLAGS) }; + + let context = match NonNull::new(context) { + Some(context) => context, + None => return Err(Error::XKBNotFound), + }; + + Ok(Self { context }) + } +} + +impl Drop for XkbContext { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_context_unref)(self.context.as_ptr()); + } + } +} + +impl Deref for XkbContext { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.context + } +} + +/// Shared logic for constructing a string with `xkb_compose_state_get_utf8` and +/// `xkb_state_key_get_utf8`. +fn make_string_with(scratch_buffer: &mut Vec, mut f: F) -> Option +where + F: FnMut(*mut c_char, usize) -> i32, +{ + let size = f(ptr::null_mut(), 0); + if size == 0 { + return None; + } + let size = usize::try_from(size).unwrap(); + scratch_buffer.clear(); + // The allocated buffer must include space for the null-terminator. + scratch_buffer.reserve(size + 1); + unsafe { + let written = f(scratch_buffer.as_mut_ptr().cast(), scratch_buffer.capacity()); + if usize::try_from(written).unwrap() != size { + // This will likely never happen. + return None; + } + scratch_buffer.set_len(size); + }; + + byte_slice_to_smol_str(scratch_buffer) +} + +// NOTE: This is track_caller so we can have more informative line numbers when logging +#[track_caller] +fn byte_slice_to_smol_str(bytes: &[u8]) -> Option { + std::str::from_utf8(bytes) + .map(SmolStr::new) + .map_err(|e| { + tracing::warn!("UTF-8 received from libxkbcommon ({:?}) was invalid: {e}", bytes) + }) + .ok() +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/state.rs b/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/state.rs new file mode 100644 index 0000000..27c055a --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/common/xkb/state.rs @@ -0,0 +1,189 @@ +//! XKB state. + +use std::os::raw::c_char; +use std::ptr::NonNull; + +use smol_str::SmolStr; +#[cfg(x11_platform)] +use x11_dl::xlib_xcb::xcb_connection_t; +use xkbcommon_dl::{ + self as xkb, xkb_keycode_t, xkb_keysym_t, xkb_layout_index_t, xkb_state, xkb_state_component, +}; + +use crate::platform_impl::common::xkb::keymap::XkbKeymap; +#[cfg(x11_platform)] +use crate::platform_impl::common::xkb::XKBXH; +use crate::platform_impl::common::xkb::{make_string_with, XKBH}; + +#[derive(Debug)] +pub struct XkbState { + state: NonNull, + modifiers: ModifiersState, +} + +impl XkbState { + #[cfg(wayland_platform)] + pub fn new_wayland(keymap: &XkbKeymap) -> Option { + let state = NonNull::new(unsafe { (XKBH.xkb_state_new)(keymap.as_ptr()) })?; + Some(Self::new_inner(state)) + } + + #[cfg(x11_platform)] + pub fn new_x11(xcb: *mut xcb_connection_t, keymap: &XkbKeymap) -> Option { + let state = unsafe { + (XKBXH.xkb_x11_state_new_from_device)(keymap.as_ptr(), xcb, keymap._core_keyboard_id) + }; + let state = NonNull::new(state)?; + Some(Self::new_inner(state)) + } + + fn new_inner(state: NonNull) -> Self { + let modifiers = ModifiersState::default(); + let mut this = Self { state, modifiers }; + this.reload_modifiers(); + this + } + + pub fn get_one_sym_raw(&mut self, keycode: xkb_keycode_t) -> xkb_keysym_t { + unsafe { (XKBH.xkb_state_key_get_one_sym)(self.state.as_ptr(), keycode) } + } + + pub fn layout(&mut self, key: xkb_keycode_t) -> xkb_layout_index_t { + unsafe { (XKBH.xkb_state_key_get_layout)(self.state.as_ptr(), key) } + } + + #[cfg(x11_platform)] + pub fn depressed_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_DEPRESSED, + ) + } + } + + #[cfg(x11_platform)] + pub fn latched_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_LATCHED, + ) + } + } + + #[cfg(x11_platform)] + pub fn locked_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_LOCKED, + ) + } + } + + pub fn get_utf8_raw( + &mut self, + keycode: xkb_keycode_t, + scratch_buffer: &mut Vec, + ) -> Option { + make_string_with(scratch_buffer, |ptr, len| unsafe { + (XKBH.xkb_state_key_get_utf8)(self.state.as_ptr(), keycode, ptr, len) + }) + } + + pub fn modifiers(&self) -> ModifiersState { + self.modifiers + } + + pub fn update_modifiers( + &mut self, + mods_depressed: u32, + mods_latched: u32, + mods_locked: u32, + depressed_group: u32, + latched_group: u32, + locked_group: u32, + ) { + let mask = unsafe { + (XKBH.xkb_state_update_mask)( + self.state.as_ptr(), + mods_depressed, + mods_latched, + mods_locked, + depressed_group, + latched_group, + locked_group, + ) + }; + + if mask.contains(xkb_state_component::XKB_STATE_MODS_EFFECTIVE) { + // Effective value of mods have changed, we need to update our state. + self.reload_modifiers(); + } + } + + /// Reload the modifiers. + fn reload_modifiers(&mut self) { + self.modifiers.ctrl = self.mod_name_is_active(xkb::XKB_MOD_NAME_CTRL); + self.modifiers.alt = self.mod_name_is_active(xkb::XKB_MOD_NAME_ALT); + self.modifiers.shift = self.mod_name_is_active(xkb::XKB_MOD_NAME_SHIFT); + self.modifiers.caps_lock = self.mod_name_is_active(xkb::XKB_MOD_NAME_CAPS); + self.modifiers.logo = self.mod_name_is_active(xkb::XKB_MOD_NAME_LOGO); + self.modifiers.num_lock = self.mod_name_is_active(xkb::XKB_MOD_NAME_NUM); + } + + /// Check if the modifier is active within xkb. + fn mod_name_is_active(&mut self, name: &[u8]) -> bool { + unsafe { + (XKBH.xkb_state_mod_name_is_active)( + self.state.as_ptr(), + name.as_ptr() as *const c_char, + xkb_state_component::XKB_STATE_MODS_EFFECTIVE, + ) > 0 + } + } +} + +impl Drop for XkbState { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_state_unref)(self.state.as_ptr()); + } + } +} + +/// Represents the current state of the keyboard modifiers +/// +/// Each field of this struct represents a modifier and is `true` if this modifier is active. +/// +/// For some modifiers, this means that the key is currently pressed, others are toggled +/// (like caps lock). +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct ModifiersState { + /// The "control" key + pub ctrl: bool, + /// The "alt" key + pub alt: bool, + /// The "shift" key + pub shift: bool, + /// The "Caps lock" key + pub caps_lock: bool, + /// The "logo" key + /// + /// Also known as the "windows" key on most keyboards + pub logo: bool, + /// The "Num lock" key + pub num_lock: bool, +} + +impl From for crate::keyboard::ModifiersState { + fn from(mods: ModifiersState) -> crate::keyboard::ModifiersState { + let mut to_mods = crate::keyboard::ModifiersState::empty(); + to_mods.set(crate::keyboard::ModifiersState::SHIFT, mods.shift); + to_mods.set(crate::keyboard::ModifiersState::CONTROL, mods.ctrl); + to_mods.set(crate::keyboard::ModifiersState::ALT, mods.alt); + to_mods.set(crate::keyboard::ModifiersState::SUPER, mods.logo); + to_mods + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/mod.rs new file mode 100644 index 0000000..bc0e71c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/mod.rs @@ -0,0 +1,1044 @@ +#![cfg(free_unix)] + +#[cfg(all(not(x11_platform), not(wayland_platform)))] +compile_error!("Please select a feature to build for unix: `x11`, `wayland`"); + +use std::collections::VecDeque; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::Arc; +use std::time::Duration; +use std::{env, fmt}; +#[cfg(x11_platform)] +use std::{ffi::CStr, mem::MaybeUninit, os::raw::*, sync::Mutex}; + +#[cfg(x11_platform)] +use crate::utils::Lazy; +use smol_str::SmolStr; + +#[cfg(x11_platform)] +use self::x11::{X11Error, XConnection, XError, XNotSupported}; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{EventLoopError, ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event_loop::{ + ActiveEventLoop as RootELW, AsyncRequestSerial, ControlFlow, DeviceEvents, EventLoopClosed, +}; +use crate::icon::Icon; +use crate::keyboard::Key; +use crate::platform::pump_events::PumpStatus; +#[cfg(x11_platform)] +use crate::platform::x11::{WindowType as XWindowType, XlibErrorHook}; +use crate::window::{ + ActivationToken, Cursor, CursorGrabMode, CustomCursor, CustomCursorSource, ImePurpose, + ResizeDirection, Theme, UserAttentionType, WindowAttributes, WindowButtons, WindowLevel, +}; + +pub(crate) use self::common::xkb::{physicalkey_to_scancode, scancode_to_physicalkey}; +pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSource; +pub(crate) use crate::icon::RgbaIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; + +pub(crate) mod common; +#[cfg(wayland_platform)] +pub(crate) mod wayland; +#[cfg(x11_platform)] +pub(crate) mod x11; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) enum Backend { + #[cfg(x11_platform)] + X, + #[cfg(wayland_platform)] + Wayland, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) forced_backend: Option, + pub(crate) any_thread: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ApplicationName { + pub general: String, + pub instance: String, +} + +impl ApplicationName { + pub fn new(general: String, instance: String) -> Self { + Self { general, instance } + } +} + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + pub name: Option, + pub activation_token: Option, + #[cfg(x11_platform)] + pub x11: X11WindowAttributes, +} + +#[derive(Clone, Debug)] +#[cfg(x11_platform)] +pub struct X11WindowAttributes { + pub visual_id: Option, + pub screen_id: Option, + pub base_size: Option, + pub override_redirect: bool, + pub x11_window_types: Vec, + + /// The parent window to embed this window into. + pub embed_window: Option, +} + +#[cfg_attr(not(x11_platform), allow(clippy::derivable_impls))] +impl Default for PlatformSpecificWindowAttributes { + fn default() -> Self { + Self { + name: None, + activation_token: None, + #[cfg(x11_platform)] + x11: X11WindowAttributes { + visual_id: None, + screen_id: None, + base_size: None, + override_redirect: false, + x11_window_types: vec![XWindowType::Normal], + embed_window: None, + }, + } + } +} + +#[cfg(x11_platform)] +pub(crate) static X11_BACKEND: Lazy, XNotSupported>>> = + Lazy::new(|| Mutex::new(XConnection::new(Some(x_error_callback)).map(Arc::new))); + +#[derive(Debug, Clone)] +pub enum OsError { + Misc(&'static str), + #[cfg(x11_platform)] + XNotSupported(XNotSupported), + #[cfg(x11_platform)] + XError(Arc), + #[cfg(wayland_platform)] + WaylandError(Arc), +} + +impl fmt::Display for OsError { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match *self { + OsError::Misc(e) => _f.pad(e), + #[cfg(x11_platform)] + OsError::XNotSupported(ref e) => fmt::Display::fmt(e, _f), + #[cfg(x11_platform)] + OsError::XError(ref e) => fmt::Display::fmt(e, _f), + #[cfg(wayland_platform)] + OsError::WaylandError(ref e) => fmt::Display::fmt(e, _f), + } + } +} + +pub(crate) enum Window { + #[cfg(x11_platform)] + X(x11::Window), + #[cfg(wayland_platform)] + Wayland(wayland::Window), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(u64); + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id) + } +} + +impl WindowId { + pub const fn dummy() -> Self { + Self(0) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DeviceId { + #[cfg(x11_platform)] + X(x11::DeviceId), + #[cfg(wayland_platform)] + Wayland(wayland::DeviceId), +} + +impl DeviceId { + pub const fn dummy() -> Self { + #[cfg(wayland_platform)] + return DeviceId::Wayland(wayland::DeviceId::dummy()); + #[cfg(all(not(wayland_platform), x11_platform))] + return DeviceId::X(x11::DeviceId::dummy()); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum MonitorHandle { + #[cfg(x11_platform)] + X(x11::MonitorHandle), + #[cfg(wayland_platform)] + Wayland(wayland::MonitorHandle), +} + +/// `x11_or_wayland!(match expr; Enum(foo) => foo.something())` +/// expands to the equivalent of +/// ```ignore +/// match self { +/// Enum::X(foo) => foo.something(), +/// Enum::Wayland(foo) => foo.something(), +/// } +/// ``` +/// The result can be converted to another enum by adding `; as AnotherEnum` +macro_rules! x11_or_wayland { + (match $what:expr; $enum:ident ( $($c1:tt)* ) => $x:expr; as $enum2:ident ) => { + match $what { + #[cfg(x11_platform)] + $enum::X($($c1)*) => $enum2::X($x), + #[cfg(wayland_platform)] + $enum::Wayland($($c1)*) => $enum2::Wayland($x), + } + }; + (match $what:expr; $enum:ident ( $($c1:tt)* ) => $x:expr) => { + match $what { + #[cfg(x11_platform)] + $enum::X($($c1)*) => $x, + #[cfg(wayland_platform)] + $enum::Wayland($($c1)*) => $x, + } + }; +} + +impl MonitorHandle { + #[inline] + pub fn name(&self) -> Option { + x11_or_wayland!(match self; MonitorHandle(m) => m.name()) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + x11_or_wayland!(match self; MonitorHandle(m) => m.native_identifier()) + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + x11_or_wayland!(match self; MonitorHandle(m) => m.size()) + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + x11_or_wayland!(match self; MonitorHandle(m) => m.position()) + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + x11_or_wayland!(match self; MonitorHandle(m) => m.refresh_rate_millihertz()) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + x11_or_wayland!(match self; MonitorHandle(m) => m.scale_factor() as _) + } + + #[inline] + pub fn video_modes(&self) -> Box> { + x11_or_wayland!(match self; MonitorHandle(m) => Box::new(m.video_modes())) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum VideoModeHandle { + #[cfg(x11_platform)] + X(x11::VideoModeHandle), + #[cfg(wayland_platform)] + Wayland(wayland::VideoModeHandle), +} + +impl VideoModeHandle { + #[inline] + pub fn size(&self) -> PhysicalSize { + x11_or_wayland!(match self; VideoModeHandle(m) => m.size()) + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + x11_or_wayland!(match self; VideoModeHandle(m) => m.bit_depth()) + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + x11_or_wayland!(match self; VideoModeHandle(m) => m.refresh_rate_millihertz()) + } + + #[inline] + pub fn monitor(&self) -> MonitorHandle { + x11_or_wayland!(match self; VideoModeHandle(m) => m.monitor(); as MonitorHandle) + } +} + +impl Window { + #[inline] + pub(crate) fn new( + window_target: &ActiveEventLoop, + attribs: WindowAttributes, + ) -> Result { + match *window_target { + #[cfg(wayland_platform)] + ActiveEventLoop::Wayland(ref window_target) => { + wayland::Window::new(window_target, attribs).map(Window::Wayland) + }, + #[cfg(x11_platform)] + ActiveEventLoop::X(ref window_target) => { + x11::Window::new(window_target, attribs).map(Window::X) + }, + } + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + f(self) + } + + #[inline] + pub fn id(&self) -> WindowId { + x11_or_wayland!(match self; Window(w) => w.id()) + } + + #[inline] + pub fn set_title(&self, title: &str) { + x11_or_wayland!(match self; Window(w) => w.set_title(title)); + } + + #[inline] + pub fn set_transparent(&self, transparent: bool) { + x11_or_wayland!(match self; Window(w) => w.set_transparent(transparent)); + } + + #[inline] + pub fn set_blur(&self, blur: bool) { + x11_or_wayland!(match self; Window(w) => w.set_blur(blur)); + } + + #[inline] + pub fn set_visible(&self, visible: bool) { + x11_or_wayland!(match self; Window(w) => w.set_visible(visible)) + } + + #[inline] + pub fn is_visible(&self) -> Option { + x11_or_wayland!(match self; Window(w) => w.is_visible()) + } + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + x11_or_wayland!(match self; Window(w) => w.outer_position()) + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + x11_or_wayland!(match self; Window(w) => w.inner_position()) + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + x11_or_wayland!(match self; Window(w) => w.set_outer_position(position)) + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + x11_or_wayland!(match self; Window(w) => w.inner_size()) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + x11_or_wayland!(match self; Window(w) => w.outer_size()) + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + x11_or_wayland!(match self; Window(w) => w.request_inner_size(size)) + } + + #[inline] + pub(crate) fn request_activation_token(&self) -> Result { + x11_or_wayland!(match self; Window(w) => w.request_activation_token()) + } + + #[inline] + pub fn set_min_inner_size(&self, dimensions: Option) { + x11_or_wayland!(match self; Window(w) => w.set_min_inner_size(dimensions)) + } + + #[inline] + pub fn set_max_inner_size(&self, dimensions: Option) { + x11_or_wayland!(match self; Window(w) => w.set_max_inner_size(dimensions)) + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + x11_or_wayland!(match self; Window(w) => w.resize_increments()) + } + + #[inline] + pub fn set_resize_increments(&self, increments: Option) { + x11_or_wayland!(match self; Window(w) => w.set_resize_increments(increments)) + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + x11_or_wayland!(match self; Window(w) => w.set_resizable(resizable)) + } + + #[inline] + pub fn is_resizable(&self) -> bool { + x11_or_wayland!(match self; Window(w) => w.is_resizable()) + } + + #[inline] + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + x11_or_wayland!(match self; Window(w) => w.set_enabled_buttons(buttons)) + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + x11_or_wayland!(match self; Window(w) => w.enabled_buttons()) + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + x11_or_wayland!(match self; Window(w) => w.set_cursor(cursor)) + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.set_cursor_grab(mode)) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + x11_or_wayland!(match self; Window(window) => window.set_cursor_visible(visible)) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.drag_window()) + } + + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.drag_resize_window(direction)) + } + + #[inline] + pub fn show_window_menu(&self, position: Position) { + x11_or_wayland!(match self; Window(w) => w.show_window_menu(position)) + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(w) => w.set_cursor_hittest(hittest)) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + x11_or_wayland!(match self; Window(w) => w.scale_factor()) + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(w) => w.set_cursor_position(position)) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + x11_or_wayland!(match self; Window(w) => w.set_maximized(maximized)) + } + + #[inline] + pub fn is_maximized(&self) -> bool { + x11_or_wayland!(match self; Window(w) => w.is_maximized()) + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + x11_or_wayland!(match self; Window(w) => w.set_minimized(minimized)) + } + + #[inline] + pub fn is_minimized(&self) -> Option { + x11_or_wayland!(match self; Window(w) => w.is_minimized()) + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + x11_or_wayland!(match self; Window(w) => w.fullscreen()) + } + + #[inline] + pub(crate) fn set_fullscreen(&self, monitor: Option) { + x11_or_wayland!(match self; Window(w) => w.set_fullscreen(monitor)) + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + x11_or_wayland!(match self; Window(w) => w.set_decorations(decorations)) + } + + #[inline] + pub fn is_decorated(&self) -> bool { + x11_or_wayland!(match self; Window(w) => w.is_decorated()) + } + + #[inline] + pub fn set_window_level(&self, level: WindowLevel) { + x11_or_wayland!(match self; Window(w) => w.set_window_level(level)) + } + + #[inline] + pub fn set_window_icon(&self, window_icon: Option) { + x11_or_wayland!(match self; Window(w) => w.set_window_icon(window_icon.map(|icon| icon.inner))) + } + + #[inline] + pub fn set_ime_cursor_area(&self, position: Position, size: Size) { + x11_or_wayland!(match self; Window(w) => w.set_ime_cursor_area(position, size)) + } + + #[inline] + pub fn reset_dead_keys(&self) { + common::xkb::reset_dead_keys() + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + x11_or_wayland!(match self; Window(w) => w.set_ime_allowed(allowed)) + } + + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + x11_or_wayland!(match self; Window(w) => w.set_ime_purpose(purpose)) + } + + #[inline] + pub fn focus_window(&self) { + x11_or_wayland!(match self; Window(w) => w.focus_window()) + } + + pub fn request_user_attention(&self, request_type: Option) { + x11_or_wayland!(match self; Window(w) => w.request_user_attention(request_type)) + } + + #[inline] + pub fn request_redraw(&self) { + x11_or_wayland!(match self; Window(w) => w.request_redraw()) + } + + #[inline] + pub fn pre_present_notify(&self) { + x11_or_wayland!(match self; Window(w) => w.pre_present_notify()) + } + + #[inline] + pub fn current_monitor(&self) -> Option { + Some(x11_or_wayland!(match self; Window(w) => w.current_monitor()?; as MonitorHandle)) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + match self { + #[cfg(x11_platform)] + Window::X(ref window) => { + window.available_monitors().into_iter().map(MonitorHandle::X).collect() + }, + #[cfg(wayland_platform)] + Window::Wayland(ref window) => { + window.available_monitors().into_iter().map(MonitorHandle::Wayland).collect() + }, + } + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + Some(x11_or_wayland!(match self; Window(w) => w.primary_monitor()?; as MonitorHandle)) + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + x11_or_wayland!(match self; Window(window) => window.raw_window_handle_rwh_04()) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + x11_or_wayland!(match self; Window(window) => window.raw_window_handle_rwh_05()) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + x11_or_wayland!(match self; Window(window) => window.raw_display_handle_rwh_05()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + x11_or_wayland!(match self; Window(window) => window.raw_window_handle_rwh_06()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + x11_or_wayland!(match self; Window(window) => window.raw_display_handle_rwh_06()) + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + x11_or_wayland!(match self; Window(window) => window.set_theme(theme)) + } + + #[inline] + pub fn theme(&self) -> Option { + x11_or_wayland!(match self; Window(window) => window.theme()) + } + + pub fn set_content_protected(&self, protected: bool) { + x11_or_wayland!(match self; Window(window) => window.set_content_protected(protected)) + } + + #[inline] + pub fn has_focus(&self) -> bool { + x11_or_wayland!(match self; Window(window) => window.has_focus()) + } + + pub fn title(&self) -> String { + x11_or_wayland!(match self; Window(window) => window.title()) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option, + pub key_without_modifiers: Key, +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) enum PlatformCustomCursor { + #[cfg(wayland_platform)] + Wayland(wayland::CustomCursor), + #[cfg(x11_platform)] + X(x11::CustomCursor), +} + +/// Hooks for X11 errors. +#[cfg(x11_platform)] +pub(crate) static XLIB_ERROR_HOOKS: Mutex> = Mutex::new(Vec::new()); + +#[cfg(x11_platform)] +unsafe extern "C" fn x_error_callback( + display: *mut x11::ffi::Display, + event: *mut x11::ffi::XErrorEvent, +) -> c_int { + let xconn_lock = X11_BACKEND.lock().unwrap_or_else(|e| e.into_inner()); + if let Ok(ref xconn) = *xconn_lock { + // Call all the hooks. + let mut error_handled = false; + for hook in XLIB_ERROR_HOOKS.lock().unwrap().iter() { + error_handled |= hook(display as *mut _, event as *mut _); + } + + // `assume_init` is safe here because the array consists of `MaybeUninit` values, + // which do not require initialization. + let mut buf: [MaybeUninit; 1024] = unsafe { MaybeUninit::uninit().assume_init() }; + unsafe { + (xconn.xlib.XGetErrorText)( + display, + (*event).error_code as c_int, + buf.as_mut_ptr() as *mut c_char, + buf.len() as c_int, + ) + }; + let description = + unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }.to_string_lossy(); + + let error = unsafe { + XError { + description: description.into_owned(), + error_code: (*event).error_code, + request_code: (*event).request_code, + minor_code: (*event).minor_code, + } + }; + + // Don't log error. + if !error_handled { + tracing::error!("X11 error: {:#?}", error); + // XXX only update the error, if it wasn't handled by any of the hooks. + *xconn.latest_error.lock().unwrap() = Some(error); + } + } + // Fun fact: this return value is completely ignored. + 0 +} + +#[allow(clippy::large_enum_variant)] +pub enum EventLoop { + #[cfg(wayland_platform)] + Wayland(Box>), + #[cfg(x11_platform)] + X(x11::EventLoop), +} + +pub enum EventLoopProxy { + #[cfg(x11_platform)] + X(x11::EventLoopProxy), + #[cfg(wayland_platform)] + Wayland(wayland::EventLoopProxy), +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + x11_or_wayland!(match self; EventLoopProxy(proxy) => proxy.clone(); as EventLoopProxy) + } +} + +impl EventLoop { + pub(crate) fn new( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { + if !attributes.any_thread && !is_main_thread() { + 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 \ + `EventLoopBuilderExtX11::any_thread` or `EventLoopBuilderExtWayland::any_thread` \ + functions." + ); + } + + // NOTE: Wayland first because of X11 could be present under Wayland as well. Empty + // variables are also treated as not set. + let backend = match ( + attributes.forced_backend, + env::var("WAYLAND_DISPLAY") + .ok() + .filter(|var| !var.is_empty()) + .or_else(|| env::var("WAYLAND_SOCKET").ok()) + .filter(|var| !var.is_empty()) + .is_some(), + env::var("DISPLAY").map(|var| !var.is_empty()).unwrap_or(false), + ) { + // User is forcing a backend. + (Some(backend), ..) => backend, + // Wayland is present. + #[cfg(wayland_platform)] + (None, true, _) => Backend::Wayland, + // X11 is present. + #[cfg(x11_platform)] + (None, _, true) => Backend::X, + // No backend is present. + (_, wayland_display, x11_display) => { + let msg = if wayland_display && !cfg!(wayland_platform) { + "DISPLAY is not set; note: enable the `winit/wayland` feature to support \ + Wayland" + } else if x11_display && !cfg!(x11_platform) { + "neither WAYLAND_DISPLAY nor WAYLAND_SOCKET is set; note: enable the \ + `winit/x11` feature to support X11" + } else { + "neither WAYLAND_DISPLAY nor WAYLAND_SOCKET nor DISPLAY is set." + }; + return Err(EventLoopError::Os(os_error!(OsError::Misc(msg)))); + }, + }; + + // Create the display based on the backend. + match backend { + #[cfg(wayland_platform)] + Backend::Wayland => EventLoop::new_wayland_any_thread(), + #[cfg(x11_platform)] + Backend::X => EventLoop::new_x11_any_thread(), + } + } + + #[cfg(wayland_platform)] + fn new_wayland_any_thread() -> Result, EventLoopError> { + wayland::EventLoop::new().map(|evlp| EventLoop::Wayland(Box::new(evlp))) + } + + #[cfg(x11_platform)] + fn new_x11_any_thread() -> Result, EventLoopError> { + let xconn = match X11_BACKEND.lock().unwrap_or_else(|e| e.into_inner()).as_ref() { + Ok(xconn) => xconn.clone(), + Err(err) => { + return Err(EventLoopError::Os(os_error!(OsError::XNotSupported(err.clone())))) + }, + }; + + Ok(EventLoop::X(x11::EventLoop::new(xconn))) + } + + #[inline] + pub fn is_wayland(&self) -> bool { + match *self { + #[cfg(wayland_platform)] + EventLoop::Wayland(_) => true, + #[cfg(x11_platform)] + _ => false, + } + } + + pub fn create_proxy(&self) -> EventLoopProxy { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.create_proxy(); as EventLoopProxy) + } + + pub fn run(mut self, callback: F) -> Result<(), EventLoopError> + where + F: FnMut(crate::event::Event, &RootELW), + { + self.run_on_demand(callback) + } + + pub fn run_on_demand(&mut self, callback: F) -> Result<(), EventLoopError> + where + F: FnMut(crate::event::Event, &RootELW), + { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.run_on_demand(callback)) + } + + pub fn pump_events(&mut self, timeout: Option, callback: F) -> PumpStatus + where + F: FnMut(crate::event::Event, &RootELW), + { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.pump_events(timeout, callback)) + } + + pub fn window_target(&self) -> &crate::event_loop::ActiveEventLoop { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.window_target()) + } +} + +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.as_fd()) + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.as_raw_fd()) + } +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + x11_or_wayland!(match self; EventLoopProxy(proxy) => proxy.send_event(event)) + } +} + +#[allow(clippy::large_enum_variant)] +pub enum ActiveEventLoop { + #[cfg(wayland_platform)] + Wayland(wayland::ActiveEventLoop), + #[cfg(x11_platform)] + X(x11::ActiveEventLoop), +} + +impl ActiveEventLoop { + #[inline] + pub fn is_wayland(&self) -> bool { + match *self { + #[cfg(wayland_platform)] + ActiveEventLoop::Wayland(_) => true, + #[cfg(x11_platform)] + _ => false, + } + } + + pub fn create_custom_cursor(&self, cursor: CustomCursorSource) -> CustomCursor { + x11_or_wayland!(match self; ActiveEventLoop(evlp) => evlp.create_custom_cursor(cursor)) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + match *self { + #[cfg(wayland_platform)] + ActiveEventLoop::Wayland(ref evlp) => { + evlp.available_monitors().map(MonitorHandle::Wayland).collect() + }, + #[cfg(x11_platform)] + ActiveEventLoop::X(ref evlp) => { + evlp.available_monitors().map(MonitorHandle::X).collect() + }, + } + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + Some( + x11_or_wayland!(match self; ActiveEventLoop(evlp) => evlp.primary_monitor()?; as MonitorHandle), + ) + } + + #[inline] + pub fn listen_device_events(&self, allowed: DeviceEvents) { + x11_or_wayland!(match self; Self(evlp) => evlp.listen_device_events(allowed)) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + x11_or_wayland!(match self; Self(evlp) => evlp.raw_display_handle_rwh_05()) + } + + #[inline] + pub fn system_theme(&self) -> Option { + None + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + x11_or_wayland!(match self; Self(evlp) => evlp.raw_display_handle_rwh_06()) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + x11_or_wayland!(match self; Self(evlp) => evlp.set_control_flow(control_flow)) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + x11_or_wayland!(match self; Self(evlp) => evlp.control_flow()) + } + + pub(crate) fn clear_exit(&self) { + x11_or_wayland!(match self; Self(evlp) => evlp.clear_exit()) + } + + pub(crate) fn exit(&self) { + x11_or_wayland!(match self; Self(evlp) => evlp.exit()) + } + + pub(crate) fn exiting(&self) -> bool { + x11_or_wayland!(match self; Self(evlp) => evlp.exiting()) + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + match self { + #[cfg(x11_platform)] + Self::X(conn) => OwnedDisplayHandle::X(conn.x_connection().clone()), + #[cfg(wayland_platform)] + Self::Wayland(conn) => OwnedDisplayHandle::Wayland(conn.connection.clone()), + } + } + + #[allow(dead_code)] + fn set_exit_code(&self, code: i32) { + x11_or_wayland!(match self; Self(evlp) => evlp.set_exit_code(code)) + } + + #[allow(dead_code)] + fn exit_code(&self) -> Option { + x11_or_wayland!(match self; Self(evlp) => evlp.exit_code()) + } +} + +#[derive(Clone)] +#[allow(dead_code)] +pub(crate) enum OwnedDisplayHandle { + #[cfg(x11_platform)] + X(Arc), + #[cfg(wayland_platform)] + Wayland(wayland_client::Connection), +} + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + match self { + #[cfg(x11_platform)] + Self::X(xconn) => { + let mut xlib_handle = rwh_05::XlibDisplayHandle::empty(); + xlib_handle.display = xconn.display.cast(); + xlib_handle.screen = xconn.default_screen_index() as _; + xlib_handle.into() + }, + + #[cfg(wayland_platform)] + Self::Wayland(conn) => { + use sctk::reexports::client::Proxy; + + let mut wayland_handle = rwh_05::WaylandDisplayHandle::empty(); + wayland_handle.display = conn.display().id().as_ptr() as *mut _; + wayland_handle.into() + }, + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + use std::ptr::NonNull; + + match self { + #[cfg(x11_platform)] + Self::X(xconn) => Ok(rwh_06::XlibDisplayHandle::new( + NonNull::new(xconn.display.cast()), + xconn.default_screen_index() as _, + ) + .into()), + + #[cfg(wayland_platform)] + Self::Wayland(conn) => { + use sctk::reexports::client::Proxy; + + Ok(rwh_06::WaylandDisplayHandle::new( + NonNull::new(conn.display().id().as_ptr().cast()).unwrap(), + ) + .into()) + }, + } + } +} + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout)))) +} + +#[cfg(target_os = "linux")] +fn is_main_thread() -> bool { + rustix::thread::gettid() == rustix::process::getpid() +} + +#[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") +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/event_loop/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/event_loop/mod.rs new file mode 100644 index 0000000..fef13f4 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -0,0 +1,810 @@ +//! The event-loop routines. + +use std::cell::{Cell, RefCell}; +use std::io::Result as IOResult; +use std::marker::PhantomData; +use std::mem; +use std::os::fd::OwnedFd; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::rc::Rc; +use std::sync::atomic::Ordering; +use std::sync::{Arc, Condvar, Mutex}; +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + +use calloop::ping::Ping; +use rustix::event::{PollFd, PollFlags}; +use rustix::pipe::{self, PipeFlags}; +use sctk::reexports::calloop::Error as CalloopError; +use sctk::reexports::calloop_wayland_source::WaylandSource; +use sctk::reexports::client::{globals, Connection, QueueHandle}; +use tracing::warn; + +use crate::cursor::OnlyCursorImage; +use crate::dpi::LogicalSize; +use crate::error::{EventLoopError, OsError as RootOsError}; +use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents}; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::platform::min_timeout; +use crate::platform_impl::{ + ActiveEventLoop as PlatformActiveEventLoop, OsError, PlatformCustomCursor, +}; +use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource}; + +mod proxy; +pub mod sink; + +pub use proxy::EventLoopProxy; +use sink::EventSink; + +use super::state::{WindowCompositorUpdate, WinitState}; +use super::window::state::FrameCallbackState; +use super::{logical_to_physical_rounded, DeviceId, WaylandError, WindowId}; + +type WaylandDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>; + +/// The Wayland event loop. +pub struct EventLoop { + /// Has `run` or `run_on_demand` been called or a call to `pump_events` that starts the loop + loop_running: bool, + + buffer_sink: EventSink, + compositor_updates: Vec, + window_ids: Vec, + + /// Sender of user events. + user_events_sender: calloop::channel::Sender, + + // XXX can't remove RefCell out of here, unless we can plumb generics into the `Window`, which + // we don't really want, since it'll break public API by a lot. + /// Pending events from the user. + pending_user_events: Rc>>, + + /// The Wayland dispatcher to has raw access to the queue when needed, such as + /// when creating a new window. + wayland_dispatcher: WaylandDispatcher, + + /// Connection to the wayland server. + connection: Connection, + + /// Event loop window target. + window_target: RootActiveEventLoop, + + // XXX drop after everything else, just to be safe. + /// Calloop's event loop. + event_loop: calloop::EventLoop<'static, WinitState>, + + pump_event_notifier: Option, +} + +impl EventLoop { + pub fn new() -> Result, EventLoopError> { + macro_rules! map_err { + ($e:expr, $err:expr) => { + $e.map_err(|error| os_error!($err(error).into())) + }; + } + + let connection = map_err!(Connection::connect_to_env(), WaylandError::Connection)?; + + let (globals, mut event_queue) = + map_err!(globals::registry_queue_init(&connection), WaylandError::Global)?; + let queue_handle = event_queue.handle(); + + let event_loop = + map_err!(calloop::EventLoop::::try_new(), WaylandError::Calloop)?; + + let mut winit_state = WinitState::new(&globals, &queue_handle, event_loop.handle()) + .map_err(|error| os_error!(error))?; + + // NOTE: do a roundtrip after binding the globals to prevent potential + // races with the server. + map_err!(event_queue.roundtrip(&mut winit_state), WaylandError::Dispatch)?; + + // Register Wayland source. + let wayland_source = WaylandSource::new(connection.clone(), event_queue); + let wayland_dispatcher = + calloop::Dispatcher::new(wayland_source, |_, queue, winit_state: &mut WinitState| { + let result = queue.dispatch_pending(winit_state); + if result.is_ok() + && (!winit_state.events_sink.is_empty() + || !winit_state.window_compositor_updates.is_empty()) + { + winit_state.dispatched_events = true; + } + result + }); + + map_err!( + event_loop.handle().register_dispatcher(wayland_dispatcher.clone()), + WaylandError::Calloop + )?; + + // Setup the user proxy. + let pending_user_events = Rc::new(RefCell::new(Vec::new())); + let pending_user_events_clone = pending_user_events.clone(); + let (user_events_sender, user_events_channel) = calloop::channel::channel(); + let result = event_loop + .handle() + .insert_source(user_events_channel, move |event, _, winit_state: &mut WinitState| { + if let calloop::channel::Event::Msg(msg) = event { + winit_state.dispatched_events = true; + pending_user_events_clone.borrow_mut().push(msg); + } + }) + .map_err(|error| error.error); + map_err!(result, WaylandError::Calloop)?; + + // An event's loop awakener to wake up for window events from winit's windows. + let (event_loop_awakener, event_loop_awakener_source) = map_err!( + calloop::ping::make_ping() + .map_err(|error| CalloopError::OtherError(Box::new(error).into())), + WaylandError::Calloop + )?; + + let result = event_loop + .handle() + .insert_source(event_loop_awakener_source, move |_, _, winit_state: &mut WinitState| { + // Mark that we have something to dispatch. + winit_state.dispatched_events = true; + }) + .map_err(|error| error.error); + map_err!(result, WaylandError::Calloop)?; + + let window_target = ActiveEventLoop { + connection: connection.clone(), + wayland_dispatcher: wayland_dispatcher.clone(), + event_loop_awakener, + queue_handle, + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), + state: RefCell::new(winit_state), + }; + + let event_loop = Self { + loop_running: false, + compositor_updates: Vec::new(), + buffer_sink: EventSink::default(), + window_ids: Vec::new(), + connection, + wayland_dispatcher, + user_events_sender, + pending_user_events, + event_loop, + window_target: RootActiveEventLoop { + p: PlatformActiveEventLoop::Wayland(window_target), + _marker: PhantomData, + }, + pump_event_notifier: None, + }; + + Ok(event_loop) + } + + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootActiveEventLoop), + { + let exit = loop { + match self.pump_events(None, &mut event_handler) { + PumpStatus::Exit(0) => { + break Ok(()); + }, + PumpStatus::Exit(code) => { + break Err(EventLoopError::ExitFailure(code)); + }, + _ => { + continue; + }, + } + }; + + // Applications aren't allowed to carry windows between separate + // `run_on_demand` calls but if they have only just dropped their + // windows we need to make sure those last requests are sent to the + // compositor. + let _ = self.roundtrip().map_err(EventLoopError::Os); + + exit + } + + pub fn pump_events(&mut self, timeout: Option, mut callback: F) -> PumpStatus + where + F: FnMut(Event, &RootActiveEventLoop), + { + if !self.loop_running { + self.loop_running = true; + + // Run the initial loop iteration. + self.single_iteration(&mut callback, StartCause::Init); + } + + // Consider the possibility that the `StartCause::Init` iteration could + // request to Exit. + if !self.exiting() { + self.poll_events_with_timeout(timeout, &mut callback); + } + if let Some(code) = self.exit_code() { + self.loop_running = false; + + callback(Event::LoopExiting, self.window_target()); + + PumpStatus::Exit(code) + } else { + // NOTE: spawn a wake-up thread, thus if we have code reading the wayland connection + // in parallel to winit, we ensure that the loop itself is marked as having events. + if timeout.is_some() && self.pump_event_notifier.is_none() { + let awakener = match &self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => { + window_target.event_loop_awakener.clone() + }, + #[cfg(x11_platform)] + PlatformActiveEventLoop::X(_) => unreachable!(), + }; + + self.pump_event_notifier = + Some(PumpEventNotifier::spawn(self.connection.clone(), awakener)); + } + + if let Some(pump_event_notifier) = self.pump_event_notifier.as_ref() { + // Notify that we don't have to wait, since we're out of winit. + *pump_event_notifier.control.0.lock().unwrap() = PumpEventNotifierAction::Monitor; + pump_event_notifier.control.1.notify_one(); + } + + PumpStatus::Continue + } + } + + pub fn poll_events_with_timeout(&mut self, mut timeout: Option, mut callback: F) + where + F: FnMut(Event, &RootActiveEventLoop), + { + let cause = loop { + let start = Instant::now(); + + timeout = { + let control_flow_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + Some(wait_deadline.saturating_duration_since(start)) + }, + }; + min_timeout(control_flow_timeout, timeout) + }; + + // NOTE Ideally we should flush as the last thing we do before polling + // to wait for events, and this should be done by the calloop + // WaylandSource but we currently need to flush writes manually. + // + // Checking for flush error is essential to perform an exit with error, since + // once we have a protocol error, we could get stuck retrying... + if self.connection.flush().is_err() { + self.set_exit_code(1); + return; + } + + if let Err(error) = self.loop_dispatch(timeout) { + // NOTE We exit on errors from dispatches, since if we've got protocol error + // libwayland-client/wayland-rs will inform us anyway, but crashing downstream is + // not really an option. Instead we inform that the event loop got + // destroyed. We may communicate an error that something was + // terminated, but winit doesn't provide us with an API to do that + // via some event. Still, we set the exit code to the error's OS + // error code, or to 1 if not possible. + let exit_code = error.raw_os_error().unwrap_or(1); + self.set_exit_code(exit_code); + return; + } + + // NB: `StartCause::Init` is handled as a special case and doesn't need + // to be considered here + let cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { start, requested_resume: None }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() < deadline { + StartCause::WaitCancelled { start, requested_resume: Some(deadline) } + } else { + StartCause::ResumeTimeReached { start, requested_resume: deadline } + } + }, + }; + + // Reduce spurious wake-ups. + let dispatched_events = self.with_state(|state| state.dispatched_events); + if matches!(cause, StartCause::WaitCancelled { .. }) + && !dispatched_events + && timeout.is_none() + { + continue; + } + + break cause; + }; + + self.single_iteration(&mut callback, cause); + } + + fn single_iteration(&mut self, callback: &mut F, cause: StartCause) + where + F: FnMut(Event, &RootActiveEventLoop), + { + // NOTE currently just indented to simplify the diff + + // We retain these grow-only scratch buffers as part of the EventLoop + // for the sake of avoiding lots of reallocs. We take them here to avoid + // trying to mutably borrow `self` more than once and we swap them back + // when finished. + let mut compositor_updates = std::mem::take(&mut self.compositor_updates); + let mut buffer_sink = std::mem::take(&mut self.buffer_sink); + let mut window_ids = std::mem::take(&mut self.window_ids); + + callback(Event::NewEvents(cause), &self.window_target); + + // NB: For consistency all platforms must emit a 'resumed' event even though Wayland + // applications don't themselves have a formal suspend/resume lifecycle. + if cause == StartCause::Init { + callback(Event::Resumed, &self.window_target); + } + + // Handle pending user events. We don't need back buffer, since we can't dispatch + // user events indirectly via callback to the user. + for user_event in self.pending_user_events.borrow_mut().drain(..) { + callback(Event::UserEvent(user_event), &self.window_target); + } + + // Drain the pending compositor updates. + self.with_state(|state| compositor_updates.append(&mut state.window_compositor_updates)); + + for mut compositor_update in compositor_updates.drain(..) { + let window_id = compositor_update.window_id; + if compositor_update.scale_changed { + let (physical_size, scale_factor) = self.with_state(|state| { + let windows = state.windows.get_mut(); + let window = windows.get(&window_id).unwrap().lock().unwrap(); + let scale_factor = window.scale_factor(); + let size = logical_to_physical_rounded(window.inner_size(), scale_factor); + (size, scale_factor) + }); + + // Stash the old window size. + let old_physical_size = physical_size; + + let new_inner_size = Arc::new(Mutex::new(physical_size)); + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &new_inner_size, + )), + }, + }, + &self.window_target, + ); + + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + + // Resize the window when user altered the size. + if old_physical_size != physical_size { + self.with_state(|state| { + let windows = state.windows.get_mut(); + let mut window = windows.get(&window_id).unwrap().lock().unwrap(); + + let new_logical_size: LogicalSize = + physical_size.to_logical(scale_factor); + window.request_inner_size(new_logical_size.into()); + }); + + // Make it queue resize. + compositor_update.resized = true; + } + } + + // NOTE: Rescale changed the physical size which winit operates in, thus we should + // resize. + if compositor_update.resized || compositor_update.scale_changed { + let physical_size = self.with_state(|state| { + let windows = state.windows.get_mut(); + let window = windows.get(&window_id).unwrap().lock().unwrap(); + + let scale_factor = window.scale_factor(); + let size = logical_to_physical_rounded(window.inner_size(), scale_factor); + + // Mark the window as needed a redraw. + state + .window_requests + .get_mut() + .get_mut(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + + size + }); + + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::Resized(physical_size), + }, + &self.window_target, + ); + } + + if compositor_update.close_window { + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::CloseRequested, + }, + &self.window_target, + ); + } + } + + // Push the events directly from the window. + self.with_state(|state| { + buffer_sink.append(&mut state.window_events_sink.lock().unwrap()); + }); + for event in buffer_sink.drain() { + let event = event.map_nonuser_event().unwrap(); + callback(event, &self.window_target); + } + + // Handle non-synthetic events. + self.with_state(|state| { + buffer_sink.append(&mut state.events_sink); + }); + for event in buffer_sink.drain() { + let event = event.map_nonuser_event().unwrap(); + callback(event, &self.window_target); + } + + // Collect the window ids + self.with_state(|state| { + window_ids.extend(state.window_requests.get_mut().keys()); + }); + + for window_id in window_ids.iter() { + let event = self.with_state(|state| { + let window_requests = state.window_requests.get_mut(); + if window_requests.get(window_id).unwrap().take_closed() { + mem::drop(window_requests.remove(window_id)); + mem::drop(state.windows.get_mut().remove(window_id)); + return Some(WindowEvent::Destroyed); + } + + let mut window = + state.windows.get_mut().get_mut(window_id).unwrap().lock().unwrap(); + + if window.frame_callback_state() == FrameCallbackState::Requested { + return None; + } + + // Reset the frame callbacks state. + window.frame_callback_reset(); + let mut redraw_requested = + window_requests.get(window_id).unwrap().take_redraw_requested(); + + // Redraw the frame while at it. + redraw_requested |= window.refresh_frame(); + + redraw_requested.then_some(WindowEvent::RedrawRequested) + }); + + if let Some(event) = event { + callback( + Event::WindowEvent { window_id: crate::window::WindowId(*window_id), event }, + &self.window_target, + ); + } + } + + // Reset the hint that we've dispatched events. + self.with_state(|state| { + state.dispatched_events = false; + }); + + // This is always the last event we dispatch before poll again + callback(Event::AboutToWait, &self.window_target); + + // Update the window frames and schedule redraws. + let mut wake_up = false; + for window_id in window_ids.drain(..) { + wake_up |= self.with_state(|state| match state.windows.get_mut().get_mut(&window_id) { + Some(window) => { + let refresh = window.lock().unwrap().refresh_frame(); + if refresh { + state + .window_requests + .get_mut() + .get_mut(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + } + + refresh + }, + None => false, + }); + } + + // Wakeup event loop if needed. + // + // If the user draws from the `AboutToWait` this is likely not required, however + // we can't do much about it. + if wake_up { + match &self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => { + window_target.event_loop_awakener.ping(); + }, + #[cfg(x11_platform)] + PlatformActiveEventLoop::X(_) => unreachable!(), + } + } + + std::mem::swap(&mut self.compositor_updates, &mut compositor_updates); + std::mem::swap(&mut self.buffer_sink, &mut buffer_sink); + std::mem::swap(&mut self.window_ids, &mut window_ids); + } + + #[inline] + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.user_events_sender.clone()) + } + + #[inline] + pub fn window_target(&self) -> &RootActiveEventLoop { + &self.window_target + } + + fn with_state<'a, U: 'a, F: FnOnce(&'a mut WinitState) -> U>(&'a mut self, callback: F) -> U { + let state = match &mut self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => window_target.state.get_mut(), + #[cfg(x11_platform)] + _ => unreachable!(), + }; + + callback(state) + } + + fn loop_dispatch>>(&mut self, timeout: D) -> IOResult<()> { + let state = match &mut self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => window_target.state.get_mut(), + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + self.event_loop.dispatch(timeout, state).map_err(|error| { + tracing::error!("Error dispatching event loop: {}", error); + error.into() + }) + } + + fn roundtrip(&mut self) -> Result { + let state = match &mut self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => window_target.state.get_mut(), + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + let mut wayland_source = self.wayland_dispatcher.as_source_mut(); + let event_queue = wayland_source.queue(); + event_queue.roundtrip(state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch(error)))) + }) + } + + fn control_flow(&self) -> ControlFlow { + self.window_target.p.control_flow() + } + + fn exiting(&self) -> bool { + self.window_target.p.exiting() + } + + fn set_exit_code(&self, code: i32) { + self.window_target.p.set_exit_code(code) + } + + fn exit_code(&self) -> Option { + self.window_target.p.exit_code() + } +} + +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() + } +} + +pub struct ActiveEventLoop { + /// The event loop wakeup source. + pub event_loop_awakener: Ping, + + /// The main queue used by the event loop. + pub queue_handle: QueueHandle, + + /// The application's latest control_flow state + pub(crate) control_flow: Cell, + + /// The application's exit state. + pub(crate) exit: Cell>, + + // TODO remove that RefCell once we can pass `&mut` in `Window::new`. + /// Winit state. + pub state: RefCell, + + /// Dispatcher of Wayland events. + pub wayland_dispatcher: WaylandDispatcher, + + /// Connection to the wayland server. + pub connection: Connection, +} + +impl ActiveEventLoop { + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(Some(0)) + } + + pub(crate) fn clear_exit(&self) { + self.exit.set(None) + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get().is_some() + } + + pub(crate) fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub(crate) fn exit_code(&self) -> Option { + self.exit.get() + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + pub(crate) fn create_custom_cursor(&self, cursor: CustomCursorSource) -> RootCustomCursor { + RootCustomCursor { + inner: PlatformCustomCursor::Wayland(OnlyCursorImage(Arc::from(cursor.inner.0))), + } + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + use sctk::reexports::client::Proxy; + + let mut display_handle = rwh_05::WaylandDisplayHandle::empty(); + display_handle.display = self.connection.display().id().as_ptr() as *mut _; + rwh_05::RawDisplayHandle::Wayland(display_handle) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + use sctk::reexports::client::Proxy; + + Ok(rwh_06::WaylandDisplayHandle::new({ + let ptr = self.connection.display().id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_display should never be null") + }) + .into()) + } +} + +#[derive(Debug)] +struct PumpEventNotifier { + /// Whether we're in winit or not. + control: Arc<(Mutex, Condvar)>, + /// Waker handle for the working thread. + worker_waker: Option, + /// Thread handle. + handle: Option>, +} + +impl Drop for PumpEventNotifier { + fn drop(&mut self) { + // Wake-up the thread. + if let Some(worker_waker) = self.worker_waker.as_ref() { + let _ = rustix::io::write(worker_waker.as_fd(), &[0u8]); + } + *self.control.0.lock().unwrap() = PumpEventNotifierAction::Shutdown; + self.control.1.notify_one(); + + if let Some(handle) = self.handle.take() { + let _ = handle.join(); + } + } +} + +impl PumpEventNotifier { + fn spawn(connection: Connection, awakener: Ping) -> Self { + // Start from the waiting state. + let control = Arc::new((Mutex::new(PumpEventNotifierAction::Pause), Condvar::new())); + let control_thread = Arc::clone(&control); + + let (read, write) = match pipe::pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK) { + Ok((read, write)) => (read, write), + Err(_) => return Self { control, handle: None, worker_waker: None }, + }; + + let handle = + std::thread::Builder::new().name(String::from("pump_events mon")).spawn(move || { + let (lock, cvar) = &*control_thread; + 'outer: loop { + let mut wait = lock.lock().unwrap(); + while *wait == PumpEventNotifierAction::Pause { + wait = cvar.wait(wait).unwrap(); + } + + // Exit the loop when we're asked to. Given that we poll + // only once we can take the `prepare_read`, but in some cases + // it could be not possible, we may block on `join`. + if *wait == PumpEventNotifierAction::Shutdown { + break 'outer; + } + + // Wake-up the main loop and put this one back to sleep. + *wait = PumpEventNotifierAction::Pause; + drop(wait); + + while let Some(read_guard) = connection.prepare_read() { + let _ = connection.flush(); + let poll_fd = PollFd::from_borrowed_fd(connection.as_fd(), PollFlags::IN); + let pipe_poll_fd = PollFd::from_borrowed_fd(read.as_fd(), PollFlags::IN); + // Read from the `fd` before going back to poll. + if Ok(1) == rustix::io::read(read.as_fd(), &mut [0u8; 1]) { + break 'outer; + } + let _ = rustix::event::poll(&mut [poll_fd, pipe_poll_fd], -1); + // Non-blocking read the connection. + let _ = read_guard.read_without_dispatch(); + } + + awakener.ping(); + } + }); + + if let Some(err) = handle.as_ref().err() { + warn!("failed to spawn pump_events wake-up thread: {err}"); + } + + PumpEventNotifier { control, handle: handle.ok(), worker_waker: Some(write) } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum PumpEventNotifierAction { + /// Monitor the wayland queue. + Monitor, + /// Pause monitoring. + Pause, + /// Shutdown the thread. + Shutdown, +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/event_loop/proxy.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/event_loop/proxy.rs new file mode 100644 index 0000000..9dc7d99 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/event_loop/proxy.rs @@ -0,0 +1,28 @@ +//! An event loop proxy. + +use std::sync::mpsc::SendError; + +use sctk::reexports::calloop::channel::Sender; + +use crate::event_loop::EventLoopClosed; + +/// A handle that can be sent across the threads and used to wake up the `EventLoop`. +pub struct EventLoopProxy { + user_events_sender: Sender, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { user_events_sender: self.user_events_sender.clone() } + } +} + +impl EventLoopProxy { + pub fn new(user_events_sender: Sender) -> Self { + Self { user_events_sender } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.user_events_sender.send(event).map_err(|SendError(error)| EventLoopClosed(error)) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/event_loop/sink.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/event_loop/sink.rs new file mode 100644 index 0000000..e506b4a --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/event_loop/sink.rs @@ -0,0 +1,53 @@ +//! An event loop's sink to deliver events from the Wayland event callbacks. + +use std::vec::Drain; + +use crate::event::{DeviceEvent, DeviceId as RootDeviceId, Event, WindowEvent}; +use crate::platform_impl::platform::DeviceId as PlatformDeviceId; +use crate::window::WindowId as RootWindowId; + +use super::{DeviceId, WindowId}; + +/// An event loop's sink to deliver events from the Wayland event callbacks +/// to the winit's user. +#[derive(Default)] +pub struct EventSink { + pub window_events: Vec>, +} + +impl EventSink { + pub fn new() -> Self { + Default::default() + } + + /// Return `true` if there're pending events. + #[inline] + pub fn is_empty(&self) -> bool { + self.window_events.is_empty() + } + + /// Add new device event to a queue. + #[inline] + pub fn push_device_event(&mut self, event: DeviceEvent, device_id: DeviceId) { + self.window_events.push(Event::DeviceEvent { + event, + device_id: RootDeviceId(PlatformDeviceId::Wayland(device_id)), + }); + } + + /// Add new window event to a queue. + #[inline] + pub fn push_window_event(&mut self, event: WindowEvent, window_id: WindowId) { + self.window_events.push(Event::WindowEvent { event, window_id: RootWindowId(window_id) }); + } + + #[inline] + pub fn append(&mut self, other: &mut Self) { + self.window_events.append(&mut other.window_events); + } + + #[inline] + pub fn drain(&mut self) -> Drain<'_, Event<()>> { + self.window_events.drain(..) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/mod.rs new file mode 100644 index 0000000..63052b7 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/mod.rs @@ -0,0 +1,85 @@ +//! Winit's Wayland backend. + +use std::fmt::Display; +use std::sync::Arc; + +use sctk::reexports::client::globals::{BindError, GlobalError}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{self, ConnectError, DispatchError, Proxy}; + +pub(super) use crate::cursor::OnlyCursorImage as CustomCursor; +use crate::dpi::{LogicalSize, PhysicalSize}; +pub use crate::platform_impl::platform::{OsError, WindowId}; +pub use event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}; +pub use output::{MonitorHandle, VideoModeHandle}; +pub use window::Window; + +mod event_loop; +mod output; +mod seat; +mod state; +mod types; +mod window; + +#[derive(Debug)] +pub enum WaylandError { + /// Error connecting to the socket. + Connection(ConnectError), + + /// Error binding the global. + Global(GlobalError), + + // Bind error. + Bind(BindError), + + /// Error during the dispatching the event queue. + Dispatch(DispatchError), + + /// Calloop error. + Calloop(calloop::Error), + + /// Wayland + Wire(client::backend::WaylandError), +} + +impl Display for WaylandError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WaylandError::Connection(error) => error.fmt(f), + WaylandError::Global(error) => error.fmt(f), + WaylandError::Bind(error) => error.fmt(f), + WaylandError::Dispatch(error) => error.fmt(f), + WaylandError::Calloop(error) => error.fmt(f), + WaylandError::Wire(error) => error.fmt(f), + } + } +} + +impl From for OsError { + fn from(value: WaylandError) -> Self { + Self::WaylandError(Arc::new(value)) + } +} + +/// Dummy device id, since Wayland doesn't have device events. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId; + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId + } +} + +/// Get the WindowId out of the surface. +#[inline] +fn make_wid(surface: &WlSurface) -> WindowId { + WindowId(surface.id().as_ptr() as u64) +} + +/// The default routine does floor, but we need round on Wayland. +fn logical_to_physical_rounded(size: LogicalSize, scale_factor: f64) -> PhysicalSize { + let width = size.width as f64 * scale_factor; + let height = size.height as f64 * scale_factor; + (width.round(), height.round()).into() +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/output.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/output.rs new file mode 100644 index 0000000..ae6c1b0 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/output.rs @@ -0,0 +1,163 @@ +use sctk::reexports::client::protocol::wl_output::WlOutput; +use sctk::reexports::client::Proxy; + +use sctk::output::OutputData; + +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; +use crate::platform_impl::platform::VideoModeHandle as PlatformVideoModeHandle; + +use super::event_loop::ActiveEventLoop; + +impl ActiveEventLoop { + #[inline] + pub fn available_monitors(&self) -> impl Iterator { + self.state.borrow().output_state.outputs().map(MonitorHandle::new) + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + // There's no primary monitor on Wayland. + None + } +} + +#[derive(Clone, Debug)] +pub struct MonitorHandle { + pub(crate) proxy: WlOutput, +} + +impl MonitorHandle { + #[inline] + pub(crate) fn new(proxy: WlOutput) -> Self { + Self { proxy } + } + + #[inline] + pub fn name(&self) -> Option { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| info.name.clone()) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| info.id) + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + let output_data = self.proxy.data::().unwrap(); + let dimensions = output_data.with_output_info(|info| { + info.modes.iter().find_map(|mode| mode.current.then_some(mode.dimensions)) + }); + + match dimensions { + Some((width, height)) => (width as u32, height as u32), + _ => (0, 0), + } + .into() + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| { + info.logical_position.map_or_else( + || { + LogicalPosition::::from(info.location) + .to_physical(info.scale_factor as f64) + }, + |logical_position| { + LogicalPosition::::from(logical_position) + .to_physical(info.scale_factor as f64) + }, + ) + }) + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| { + info.modes.iter().find_map(|mode| mode.current.then_some(mode.refresh_rate as u32)) + }) + } + + #[inline] + pub fn scale_factor(&self) -> i32 { + let output_data = self.proxy.data::().unwrap(); + output_data.scale_factor() + } + + #[inline] + pub fn video_modes(&self) -> impl Iterator { + let output_data = self.proxy.data::().unwrap(); + let modes = output_data.with_output_info(|info| info.modes.clone()); + + let monitor = self.clone(); + + modes.into_iter().map(move |mode| { + PlatformVideoModeHandle::Wayland(VideoModeHandle { + size: (mode.dimensions.0 as u32, mode.dimensions.1 as u32).into(), + refresh_rate_millihertz: mode.refresh_rate as u32, + bit_depth: 32, + monitor: monitor.clone(), + }) + }) + } +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.native_identifier() == other.native_identifier() + } +} + +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 { + self.native_identifier().cmp(&other.native_identifier()) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.native_identifier().hash(state); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VideoModeHandle { + pub(crate) size: PhysicalSize, + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + pub(crate) monitor: MonitorHandle, +} + +impl VideoModeHandle { + #[inline] + pub fn size(&self) -> PhysicalSize { + self.size + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/keyboard/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/keyboard/mod.rs new file mode 100644 index 0000000..f84c386 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/keyboard/mod.rs @@ -0,0 +1,380 @@ +//! The keyboard input handling. + +use std::sync::Mutex; +use std::time::Duration; + +use calloop::timer::{TimeoutAction, Timer}; +use calloop::{LoopHandle, RegistrationToken}; +use tracing::warn; + +use sctk::reexports::client::protocol::wl_keyboard::{ + Event as WlKeyboardEvent, KeyState as WlKeyState, KeymapFormat as WlKeymapFormat, WlKeyboard, +}; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::{Connection, Dispatch, Proxy, QueueHandle, WEnum}; + +use crate::event::{ElementState, WindowEvent}; +use crate::keyboard::ModifiersState; + +use crate::platform_impl::common::xkb::Context; +use crate::platform_impl::wayland::event_loop::sink::EventSink; +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::wayland::{self, DeviceId, WindowId}; + +impl Dispatch for WinitState { + fn event( + state: &mut WinitState, + wl_keyboard: &WlKeyboard, + event: ::Event, + data: &KeyboardData, + _: &Connection, + _: &QueueHandle, + ) { + let seat_state = match state.seats.get_mut(&data.seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received keyboard event {event:?} without seat"); + return; + }, + }; + let keyboard_state = match seat_state.keyboard_state.as_mut() { + Some(keyboard_state) => keyboard_state, + None => { + warn!("Received keyboard event {event:?} without keyboard"); + return; + }, + }; + + match event { + WlKeyboardEvent::Keymap { format, fd, size } => match format { + WEnum::Value(format) => match format { + WlKeymapFormat::NoKeymap => { + warn!("non-xkb compatible keymap") + }, + WlKeymapFormat::XkbV1 => { + let context = &mut keyboard_state.xkb_context; + context.set_keymap_from_fd(fd, size as usize); + }, + _ => unreachable!(), + }, + WEnum::Unknown(value) => { + warn!("unknown keymap format 0x{:x}", value) + }, + }, + WlKeyboardEvent::Enter { surface, .. } => { + let window_id = wayland::make_wid(&surface); + + // Mark the window as focused. + let was_unfocused = match state.windows.get_mut().get(&window_id) { + Some(window) => { + let mut window = window.lock().unwrap(); + let was_unfocused = !window.has_focus(); + window.add_seat_focus(data.seat.id()); + was_unfocused + }, + None => return, + }; + + // Drop the repeat, if there were any. + keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + + *data.window_id.lock().unwrap() = Some(window_id); + + // The keyboard focus is considered as general focus. + if was_unfocused { + state.events_sink.push_window_event(WindowEvent::Focused(true), window_id); + } + + // HACK: this is just for GNOME not fixing their ordering issue of modifiers. + if std::mem::take(&mut seat_state.modifiers_pending) { + state.events_sink.push_window_event( + WindowEvent::ModifiersChanged(seat_state.modifiers.into()), + window_id, + ); + } + }, + WlKeyboardEvent::Leave { surface, .. } => { + let window_id = wayland::make_wid(&surface); + + // NOTE: we should drop the repeat regardless whether it was for the present + // window of for the window which just went gone. + keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + + // NOTE: The check whether the window exists is essential as we might get a + // nil surface, regardless of what protocol says. + let focused = match state.windows.get_mut().get(&window_id) { + Some(window) => { + let mut window = window.lock().unwrap(); + window.remove_seat_focus(&data.seat.id()); + window.has_focus() + }, + None => return, + }; + + // We don't need to update it above, because the next `Enter` will overwrite + // anyway. + *data.window_id.lock().unwrap() = None; + + if !focused { + // Notify that no modifiers are being pressed. + state.events_sink.push_window_event( + WindowEvent::ModifiersChanged(ModifiersState::empty().into()), + window_id, + ); + + state.events_sink.push_window_event(WindowEvent::Focused(false), window_id); + } + }, + WlKeyboardEvent::Key { key, state: WEnum::Value(WlKeyState::Pressed), .. } => { + let key = key + 8; + + key_input( + keyboard_state, + &mut state.events_sink, + data, + key, + ElementState::Pressed, + false, + ); + + let delay = match keyboard_state.repeat_info { + RepeatInfo::Repeat { delay, .. } => delay, + RepeatInfo::Disable => return, + }; + + if !keyboard_state.xkb_context.keymap_mut().unwrap().key_repeats(key) { + return; + } + + keyboard_state.current_repeat = Some(key); + + // NOTE terminate ongoing timer and start a new timer. + + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + + let timer = Timer::from_duration(delay); + let wl_keyboard = wl_keyboard.clone(); + keyboard_state.repeat_token = keyboard_state + .loop_handle + .insert_source(timer, move |_, _, state| { + // Required to handle the wakeups from the repeat sources. + state.dispatched_events = true; + + let data = wl_keyboard.data::().unwrap(); + let seat_state = match state.seats.get_mut(&data.seat.id()) { + Some(seat_state) => seat_state, + None => return TimeoutAction::Drop, + }; + + let keyboard_state = match seat_state.keyboard_state.as_mut() { + Some(keyboard_state) => keyboard_state, + None => return TimeoutAction::Drop, + }; + + // NOTE: The removed on event source is batched, but key change to `None` + // is instant. + let repeat_keycode = match keyboard_state.current_repeat { + Some(repeat_keycode) => repeat_keycode, + None => return TimeoutAction::Drop, + }; + + key_input( + keyboard_state, + &mut state.events_sink, + data, + repeat_keycode, + ElementState::Pressed, + true, + ); + + // NOTE: the gap could change dynamically while repeat is going. + match keyboard_state.repeat_info { + RepeatInfo::Repeat { gap, .. } => TimeoutAction::ToDuration(gap), + RepeatInfo::Disable => TimeoutAction::Drop, + } + }) + .ok(); + }, + WlKeyboardEvent::Key { key, state: WEnum::Value(WlKeyState::Released), .. } => { + let key = key + 8; + + key_input( + keyboard_state, + &mut state.events_sink, + data, + key, + ElementState::Released, + false, + ); + + if keyboard_state.repeat_info != RepeatInfo::Disable + && keyboard_state.xkb_context.keymap_mut().unwrap().key_repeats(key) + && Some(key) == keyboard_state.current_repeat + { + keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + } + }, + WlKeyboardEvent::Modifiers { + mods_depressed, mods_latched, mods_locked, group, .. + } => { + let xkb_context = &mut keyboard_state.xkb_context; + let xkb_state = match xkb_context.state_mut() { + Some(state) => state, + None => return, + }; + + xkb_state.update_modifiers(mods_depressed, mods_latched, mods_locked, 0, 0, group); + seat_state.modifiers = xkb_state.modifiers().into(); + + // HACK: part of the workaround from `WlKeyboardEvent::Enter`. + let window_id = match *data.window_id.lock().unwrap() { + Some(window_id) => window_id, + None => { + seat_state.modifiers_pending = true; + return; + }, + }; + + state.events_sink.push_window_event( + WindowEvent::ModifiersChanged(seat_state.modifiers.into()), + window_id, + ); + }, + WlKeyboardEvent::RepeatInfo { rate, delay } => { + keyboard_state.repeat_info = if rate == 0 { + // Stop the repeat once we get a disable event. + keyboard_state.current_repeat = None; + if let Some(repeat_token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(repeat_token); + } + RepeatInfo::Disable + } else { + let gap = Duration::from_micros(1_000_000 / rate as u64); + let delay = Duration::from_millis(delay as u64); + RepeatInfo::Repeat { gap, delay } + }; + }, + _ => unreachable!(), + } + } +} + +/// The state of the keyboard on the current seat. +#[derive(Debug)] +pub struct KeyboardState { + /// The underlying WlKeyboard. + pub keyboard: WlKeyboard, + + /// Loop handle to handle key repeat. + pub loop_handle: LoopHandle<'static, WinitState>, + + /// The state of the keyboard. + pub xkb_context: Context, + + /// The information about the repeat rate obtained from the compositor. + pub repeat_info: RepeatInfo, + + /// The token of the current handle inside the calloop's event loop. + pub repeat_token: Option, + + /// The current repeat raw key. + pub current_repeat: Option, +} + +impl KeyboardState { + pub fn new(keyboard: WlKeyboard, loop_handle: LoopHandle<'static, WinitState>) -> Self { + Self { + keyboard, + loop_handle, + xkb_context: Context::new().unwrap(), + repeat_info: RepeatInfo::default(), + repeat_token: None, + current_repeat: None, + } + } +} + +impl Drop for KeyboardState { + fn drop(&mut self) { + if self.keyboard.version() >= 3 { + self.keyboard.release(); + } + + if let Some(token) = self.repeat_token.take() { + self.loop_handle.remove(token); + } + } +} + +/// The rate at which a pressed key is repeated. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RepeatInfo { + /// Keys will be repeated at the specified rate and delay. + Repeat { + /// The time between the key repeats. + gap: Duration, + + /// Delay (in milliseconds) between a key press and the start of repetition. + delay: Duration, + }, + + /// Keys should not be repeated. + Disable, +} + +impl Default for RepeatInfo { + /// The default repeat rate is 25 keys per second with the delay of 200ms. + /// + /// The values are picked based on the default in various compositors and Xorg. + fn default() -> Self { + Self::Repeat { gap: Duration::from_millis(40), delay: Duration::from_millis(200) } + } +} + +/// Keyboard user data. +#[derive(Debug)] +pub struct KeyboardData { + /// The currently focused window surface. Could be `None` on bugged compositors, like mutter. + window_id: Mutex>, + + /// The seat used to create this keyboard. + seat: WlSeat, +} + +impl KeyboardData { + pub fn new(seat: WlSeat) -> Self { + Self { window_id: Default::default(), seat } + } +} + +fn key_input( + keyboard_state: &mut KeyboardState, + event_sink: &mut EventSink, + data: &KeyboardData, + keycode: u32, + state: ElementState, + repeat: bool, +) { + let window_id = match *data.window_id.lock().unwrap() { + Some(window_id) => window_id, + None => return, + }; + + let device_id = crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId)); + if let Some(mut key_context) = keyboard_state.xkb_context.key_context() { + let event = key_context.process_key_event(keycode, state, repeat); + let event = WindowEvent::KeyboardInput { device_id, event, is_synthetic: false }; + event_sink.push_window_event(event, window_id); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/mod.rs new file mode 100644 index 0000000..eaecd93 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/mod.rs @@ -0,0 +1,235 @@ +//! Seat handling. + +use std::sync::Arc; + +use ahash::AHashMap; +use tracing::warn; + +use sctk::reexports::client::backend::ObjectId; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_touch::WlTouch; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::relative_pointer::zv1::client::zwp_relative_pointer_v1::ZwpRelativePointerV1; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3; + +use sctk::seat::pointer::{ThemeSpec, ThemedPointer}; +use sctk::seat::{Capability as SeatCapability, SeatHandler, SeatState}; + +use crate::event::WindowEvent; +use crate::keyboard::ModifiersState; +use crate::platform_impl::wayland::state::WinitState; + +mod keyboard; +mod pointer; +mod text_input; +mod touch; + +pub use pointer::relative_pointer::RelativePointerState; +pub use pointer::{PointerConstraintsState, WinitPointerData, WinitPointerDataExt}; +pub use text_input::{TextInputState, ZwpTextInputV3Ext}; + +use keyboard::{KeyboardData, KeyboardState}; +use text_input::TextInputData; +use touch::TouchPoint; + +#[derive(Debug, Default)] +pub struct WinitSeatState { + /// The pointer bound on the seat. + pointer: Option>>, + + /// The touch bound on the seat. + touch: Option, + + /// The mapping from touched points to the surfaces they're present. + touch_map: AHashMap, + + /// The text input bound on the seat. + text_input: Option>, + + /// The relative pointer bound on the seat. + relative_pointer: Option, + + /// The keyboard bound on the seat. + keyboard_state: Option, + + /// The current modifiers state on the seat. + modifiers: ModifiersState, + + /// Whether we have pending modifiers. + modifiers_pending: bool, +} + +impl WinitSeatState { + pub fn new() -> Self { + Default::default() + } +} + +impl SeatHandler for WinitState { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_capability( + &mut self, + _: &Connection, + queue_handle: &QueueHandle, + seat: WlSeat, + capability: SeatCapability, + ) { + let seat_state = match self.seats.get_mut(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_seat::new_capability for unknown seat"); + return; + }, + }; + + match capability { + SeatCapability::Touch if seat_state.touch.is_none() => { + seat_state.touch = self.seat_state.get_touch(queue_handle, &seat).ok(); + }, + SeatCapability::Keyboard if seat_state.keyboard_state.is_none() => { + let keyboard = seat.get_keyboard(queue_handle, KeyboardData::new(seat.clone())); + seat_state.keyboard_state = + Some(KeyboardState::new(keyboard, self.loop_handle.clone())); + }, + SeatCapability::Pointer if seat_state.pointer.is_none() => { + let surface = self.compositor_state.create_surface(queue_handle); + let viewport = self + .viewporter_state + .as_ref() + .map(|state| state.get_viewport(&surface, queue_handle)); + let surface_id = surface.id(); + let pointer_data = WinitPointerData::new(seat.clone(), viewport); + let themed_pointer = self + .seat_state + .get_pointer_with_theme_and_data( + queue_handle, + &seat, + self.shm.wl_shm(), + surface, + ThemeSpec::System, + pointer_data, + ) + .expect("failed to create pointer with present capability."); + + seat_state.relative_pointer = self.relative_pointer.as_ref().map(|manager| { + manager.get_relative_pointer( + themed_pointer.pointer(), + queue_handle, + sctk::globals::GlobalData, + ) + }); + + let themed_pointer = Arc::new(themed_pointer); + + // Register cursor surface. + self.pointer_surfaces.insert(surface_id, themed_pointer.clone()); + + seat_state.pointer = Some(themed_pointer); + }, + _ => (), + } + + if let Some(text_input_state) = + seat_state.text_input.is_none().then_some(self.text_input_state.as_ref()).flatten() + { + seat_state.text_input = Some(Arc::new(text_input_state.get_text_input( + &seat, + queue_handle, + TextInputData::default(), + ))); + } + } + + fn remove_capability( + &mut self, + _: &Connection, + _queue_handle: &QueueHandle, + seat: WlSeat, + capability: SeatCapability, + ) { + let seat_state = match self.seats.get_mut(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_seat::remove_capability for unknown seat"); + return; + }, + }; + + if let Some(text_input) = seat_state.text_input.take() { + text_input.destroy(); + } + + match capability { + SeatCapability::Touch => { + if let Some(touch) = seat_state.touch.take() { + if touch.version() >= 3 { + touch.release(); + } + } + }, + SeatCapability::Pointer => { + if let Some(relative_pointer) = seat_state.relative_pointer.take() { + relative_pointer.destroy(); + } + + if let Some(pointer) = seat_state.pointer.take() { + let pointer_data = pointer.pointer().winit_data(); + + // Remove the cursor from the mapping. + let surface_id = pointer.surface().id(); + let _ = self.pointer_surfaces.remove(&surface_id); + + // Remove the inner locks/confines before dropping the pointer. + pointer_data.unlock_pointer(); + pointer_data.unconfine_pointer(); + + if pointer.pointer().version() >= 3 { + pointer.pointer().release(); + } + } + }, + SeatCapability::Keyboard => { + seat_state.keyboard_state = None; + self.on_keyboard_destroy(&seat.id()); + }, + _ => (), + } + } + + fn new_seat( + &mut self, + _connection: &Connection, + _queue_handle: &QueueHandle, + seat: WlSeat, + ) { + self.seats.insert(seat.id(), WinitSeatState::new()); + } + + fn remove_seat( + &mut self, + _connection: &Connection, + _queue_handle: &QueueHandle, + seat: WlSeat, + ) { + let _ = self.seats.remove(&seat.id()); + self.on_keyboard_destroy(&seat.id()); + } +} + +impl WinitState { + fn on_keyboard_destroy(&mut self, seat: &ObjectId) { + for (window_id, window) in self.windows.get_mut() { + let mut window = window.lock().unwrap(); + let had_focus = window.has_focus(); + window.remove_seat_focus(seat); + if had_focus != window.has_focus() { + self.events_sink.push_window_event(WindowEvent::Focused(false), *window_id); + } + } + } +} + +sctk::delegate_seat!(WinitState); diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/pointer/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/pointer/mod.rs new file mode 100644 index 0000000..3dcb00b --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/pointer/mod.rs @@ -0,0 +1,502 @@ +//! The pointer events. + +use std::ops::Deref; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use tracing::warn; + +use sctk::reexports::client::delegate_dispatch; +use sctk::reexports::client::protocol::wl_pointer::WlPointer; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Connection, Proxy, QueueHandle, Dispatch}; +use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; +use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1; +use sctk::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1; +use sctk::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_manager_v1::WpCursorShapeManagerV1; +use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1}; +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::csd_frame::FrameClick; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; + +use sctk::compositor::SurfaceData; +use sctk::globals::GlobalData; +use sctk::seat::pointer::{ + PointerData, PointerDataExt, PointerEvent, PointerEventKind, PointerHandler, +}; +use sctk::seat::SeatState; + +use crate::dpi::{LogicalPosition, PhysicalPosition}; +use crate::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent}; + +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::wayland::{self, DeviceId, WindowId}; + +pub mod relative_pointer; + +impl PointerHandler for WinitState { + fn pointer_frame( + &mut self, + connection: &Connection, + _: &QueueHandle, + pointer: &WlPointer, + events: &[PointerEvent], + ) { + let seat = pointer.winit_data().seat(); + let seat_state = match self.seats.get(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received pointer event without seat"); + return; + }, + }; + + let themed_pointer = match seat_state.pointer.as_ref() { + Some(pointer) => pointer, + None => { + warn!("Received pointer event without pointer"); + return; + }, + }; + + let device_id = crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId)); + + for event in events { + let surface = &event.surface; + + // The parent surface. + let parent_surface = match event.surface.data::() { + Some(data) => data.parent_surface().unwrap_or(surface), + None => continue, + }; + + let window_id = wayland::make_wid(parent_surface); + + // Ensure that window exists. + let mut window = match self.windows.get_mut().get_mut(&window_id) { + Some(window) => window.lock().unwrap(), + None => continue, + }; + + let scale_factor = window.scale_factor(); + let position: PhysicalPosition = + LogicalPosition::new(event.position.0, event.position.1).to_physical(scale_factor); + + match event.kind { + // Pointer movements on decorations. + PointerEventKind::Enter { .. } | PointerEventKind::Motion { .. } + if parent_surface != surface => + { + if let Some(icon) = window.frame_point_moved( + seat, + surface, + Duration::ZERO, + event.position.0, + event.position.1, + ) { + let _ = themed_pointer.set_cursor(connection, icon); + } + }, + PointerEventKind::Leave { .. } if parent_surface != surface => { + window.frame_point_left(); + }, + ref kind @ PointerEventKind::Press { button, serial, time } + | ref kind @ PointerEventKind::Release { button, serial, time } + if parent_surface != surface => + { + let click = match wayland_button_to_winit(button) { + MouseButton::Left => FrameClick::Normal, + MouseButton::Right => FrameClick::Alternate, + _ => continue, + }; + let pressed = matches!(kind, PointerEventKind::Press { .. }); + + // Emulate click on the frame. + window.frame_click( + click, + pressed, + seat, + serial, + Duration::from_millis(time as u64), + window_id, + &mut self.window_compositor_updates, + ); + }, + // Regular events on the main surface. + PointerEventKind::Enter { .. } => { + self.events_sink + .push_window_event(WindowEvent::CursorEntered { device_id }, window_id); + + window.pointer_entered(Arc::downgrade(themed_pointer)); + + // Set the currently focused surface. + pointer.winit_data().inner.lock().unwrap().surface = Some(window_id); + + self.events_sink.push_window_event( + WindowEvent::CursorMoved { device_id, position }, + window_id, + ); + }, + PointerEventKind::Leave { .. } => { + window.pointer_left(Arc::downgrade(themed_pointer)); + + // Remove the active surface. + pointer.winit_data().inner.lock().unwrap().surface = None; + + self.events_sink + .push_window_event(WindowEvent::CursorLeft { device_id }, window_id); + }, + PointerEventKind::Motion { .. } => { + self.events_sink.push_window_event( + WindowEvent::CursorMoved { device_id, position }, + window_id, + ); + }, + ref kind @ PointerEventKind::Press { button, serial, .. } + | ref kind @ PointerEventKind::Release { button, serial, .. } => { + // Update the last button serial. + pointer.winit_data().inner.lock().unwrap().latest_button_serial = serial; + + let button = wayland_button_to_winit(button); + let state = if matches!(kind, PointerEventKind::Press { .. }) { + ElementState::Pressed + } else { + ElementState::Released + }; + self.events_sink.push_window_event( + WindowEvent::MouseInput { device_id, state, button }, + window_id, + ); + }, + PointerEventKind::Axis { horizontal, vertical, .. } => { + // Get the current phase. + let mut pointer_data = pointer.winit_data().inner.lock().unwrap(); + + let has_discrete_scroll = horizontal.discrete != 0 || vertical.discrete != 0; + + // Figure out what to do about start/ended phases here. + // + // Figure out how to deal with `Started`. Also the `Ended` is not guaranteed + // to be sent for mouse wheels. + let phase = if horizontal.stop || vertical.stop { + TouchPhase::Ended + } else { + match pointer_data.phase { + // Discrete scroll only results in moved events. + _ if has_discrete_scroll => TouchPhase::Moved, + TouchPhase::Started | TouchPhase::Moved => TouchPhase::Moved, + _ => TouchPhase::Started, + } + }; + + // Update the phase. + pointer_data.phase = phase; + + // Mice events have both pixel and discrete delta's at the same time. So prefer + // the discrete values if they are present. + let delta = if has_discrete_scroll { + // NOTE: Wayland sign convention is the inverse of winit. + MouseScrollDelta::LineDelta( + (-horizontal.discrete) as f32, + (-vertical.discrete) as f32, + ) + } else { + // NOTE: Wayland sign convention is the inverse of winit. + MouseScrollDelta::PixelDelta( + LogicalPosition::new(-horizontal.absolute, -vertical.absolute) + .to_physical(scale_factor), + ) + }; + + self.events_sink.push_window_event( + WindowEvent::MouseWheel { device_id, delta, phase }, + window_id, + ) + }, + } + } + } +} + +#[derive(Debug)] +pub struct WinitPointerData { + /// The inner winit data associated with the pointer. + inner: Mutex, + + /// The data required by the sctk. + sctk_data: PointerData, + + /// Viewport for fractional cursor. + viewport: Option, +} + +impl WinitPointerData { + pub fn new(seat: WlSeat, viewport: Option) -> Self { + Self { + inner: Mutex::new(WinitPointerDataInner::default()), + sctk_data: PointerData::new(seat), + viewport, + } + } + + pub fn lock_pointer( + &self, + pointer_constraints: &PointerConstraintsState, + surface: &WlSurface, + pointer: &WlPointer, + queue_handle: &QueueHandle, + ) { + let mut inner = self.inner.lock().unwrap(); + if inner.locked_pointer.is_none() { + inner.locked_pointer = Some(pointer_constraints.lock_pointer( + surface, + pointer, + None, + Lifetime::Persistent, + queue_handle, + GlobalData, + )); + } + } + + pub fn unlock_pointer(&self) { + let mut inner = self.inner.lock().unwrap(); + if let Some(locked_pointer) = inner.locked_pointer.take() { + locked_pointer.destroy(); + } + } + + pub fn confine_pointer( + &self, + pointer_constraints: &PointerConstraintsState, + surface: &WlSurface, + pointer: &WlPointer, + queue_handle: &QueueHandle, + ) { + self.inner.lock().unwrap().confined_pointer = Some(pointer_constraints.confine_pointer( + surface, + pointer, + None, + Lifetime::Persistent, + queue_handle, + GlobalData, + )); + } + + pub fn unconfine_pointer(&self) { + let inner = self.inner.lock().unwrap(); + if let Some(confined_pointer) = inner.confined_pointer.as_ref() { + confined_pointer.destroy(); + } + } + + /// Seat associated with this pointer. + pub fn seat(&self) -> &WlSeat { + self.sctk_data.seat() + } + + /// Active window. + pub fn focused_window(&self) -> Option { + self.inner.lock().unwrap().surface + } + + /// Last button serial. + pub fn latest_button_serial(&self) -> u32 { + self.sctk_data.latest_button_serial().unwrap_or_default() + } + + /// Last enter serial. + pub fn latest_enter_serial(&self) -> u32 { + self.sctk_data.latest_enter_serial().unwrap_or_default() + } + + pub fn set_locked_cursor_position(&self, surface_x: f64, surface_y: f64) { + let inner = self.inner.lock().unwrap(); + if let Some(locked_pointer) = inner.locked_pointer.as_ref() { + locked_pointer.set_cursor_position_hint(surface_x, surface_y); + } + } + + pub fn viewport(&self) -> Option<&WpViewport> { + self.viewport.as_ref() + } +} + +impl Drop for WinitPointerData { + fn drop(&mut self) { + if let Some(viewport) = self.viewport.take() { + viewport.destroy(); + } + } +} + +impl PointerDataExt for WinitPointerData { + fn pointer_data(&self) -> &PointerData { + &self.sctk_data + } +} + +#[derive(Debug)] +pub struct WinitPointerDataInner { + /// The associated locked pointer. + locked_pointer: Option, + + /// The associated confined pointer. + confined_pointer: Option, + + /// Serial of the last button event. + latest_button_serial: u32, + + /// Currently focused window. + surface: Option, + + /// Current axis phase. + phase: TouchPhase, +} + +impl Drop for WinitPointerDataInner { + fn drop(&mut self) { + if let Some(locked_pointer) = self.locked_pointer.take() { + locked_pointer.destroy(); + } + + if let Some(confined_pointer) = self.confined_pointer.take() { + confined_pointer.destroy(); + } + } +} + +impl Default for WinitPointerDataInner { + fn default() -> Self { + Self { + surface: None, + locked_pointer: None, + confined_pointer: None, + latest_button_serial: 0, + phase: TouchPhase::Ended, + } + } +} + +/// Convert the Wayland button into winit. +fn wayland_button_to_winit(button: u32) -> MouseButton { + // These values are coming from . + const BTN_LEFT: u32 = 0x110; + const BTN_RIGHT: u32 = 0x111; + const BTN_MIDDLE: u32 = 0x112; + const BTN_SIDE: u32 = 0x113; + const BTN_EXTRA: u32 = 0x114; + const BTN_FORWARD: u32 = 0x115; + const BTN_BACK: u32 = 0x116; + + match button { + BTN_LEFT => MouseButton::Left, + BTN_RIGHT => MouseButton::Right, + BTN_MIDDLE => MouseButton::Middle, + BTN_BACK | BTN_SIDE => MouseButton::Back, + BTN_FORWARD | BTN_EXTRA => MouseButton::Forward, + button => MouseButton::Other(button as u16), + } +} + +pub trait WinitPointerDataExt { + fn winit_data(&self) -> &WinitPointerData; +} + +impl WinitPointerDataExt for WlPointer { + fn winit_data(&self) -> &WinitPointerData { + self.data::().expect("failed to get pointer data.") + } +} + +pub struct PointerConstraintsState { + pointer_constraints: ZwpPointerConstraintsV1, +} + +impl PointerConstraintsState { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let pointer_constraints = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { pointer_constraints }) + } +} + +impl Deref for PointerConstraintsState { + type Target = ZwpPointerConstraintsV1; + + fn deref(&self) -> &Self::Target { + &self.pointer_constraints + } +} + +impl Dispatch for PointerConstraintsState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpPointerConstraintsV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for PointerConstraintsState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpLockedPointerV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for PointerConstraintsState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpConfinedPointerV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for SeatState { + fn event( + _: &mut WinitState, + _: &WpCursorShapeDeviceV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("wp_cursor_shape_manager has no events") + } +} + +impl Dispatch for SeatState { + fn event( + _: &mut WinitState, + _: &WpCursorShapeManagerV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("wp_cursor_device_manager has no events") + } +} + +delegate_dispatch!(WinitState: [ WlPointer: WinitPointerData] => SeatState); +delegate_dispatch!(WinitState: [ WpCursorShapeManagerV1: GlobalData] => SeatState); +delegate_dispatch!(WinitState: [ WpCursorShapeDeviceV1: GlobalData] => SeatState); +delegate_dispatch!(WinitState: [ZwpPointerConstraintsV1: GlobalData] => PointerConstraintsState); +delegate_dispatch!(WinitState: [ZwpLockedPointerV1: GlobalData] => PointerConstraintsState); +delegate_dispatch!(WinitState: [ZwpConfinedPointerV1: GlobalData] => PointerConstraintsState); diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs new file mode 100644 index 0000000..a9ce276 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs @@ -0,0 +1,83 @@ +//! Relative pointer. + +use std::ops::Deref; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::{delegate_dispatch, Dispatch}; +use sctk::reexports::client::{Connection, QueueHandle}; +use sctk::reexports::protocols::wp::relative_pointer::zv1::{ + client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, + client::zwp_relative_pointer_v1::{self, ZwpRelativePointerV1}, +}; + +use sctk::globals::GlobalData; + +use crate::event::DeviceEvent; +use crate::platform_impl::wayland::state::WinitState; + +/// Wrapper around the relative pointer. +pub struct RelativePointerState { + manager: ZwpRelativePointerManagerV1, +} + +impl RelativePointerState { + /// Create new relative pointer manager. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { manager }) + } +} + +impl Deref for RelativePointerState { + type Target = ZwpRelativePointerManagerV1; + + fn deref(&self) -> &Self::Target { + &self.manager + } +} + +impl Dispatch for RelativePointerState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpRelativePointerManagerV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for RelativePointerState { + fn event( + state: &mut WinitState, + _proxy: &ZwpRelativePointerV1, + event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + let (dx_unaccel, dy_unaccel) = match event { + zwp_relative_pointer_v1::Event::RelativeMotion { dx_unaccel, dy_unaccel, .. } => { + (dx_unaccel, dy_unaccel) + }, + _ => return, + }; + state + .events_sink + .push_device_event(DeviceEvent::Motion { axis: 0, value: dx_unaccel }, super::DeviceId); + state + .events_sink + .push_device_event(DeviceEvent::Motion { axis: 1, value: dy_unaccel }, super::DeviceId); + state.events_sink.push_device_event( + DeviceEvent::MouseMotion { delta: (dx_unaccel, dy_unaccel) }, + super::DeviceId, + ); + } +} + +delegate_dispatch!(WinitState: [ZwpRelativePointerV1: GlobalData] => RelativePointerState); +delegate_dispatch!(WinitState: [ZwpRelativePointerManagerV1: GlobalData] => RelativePointerState); diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/text_input/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/text_input/mod.rs new file mode 100644 index 0000000..db72489 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/text_input/mod.rs @@ -0,0 +1,201 @@ +use std::ops::Deref; + +use sctk::globals::GlobalData; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Dispatch}; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ + ContentHint, ContentPurpose, Event as TextInputEvent, ZwpTextInputV3, +}; + +use crate::event::{Ime, WindowEvent}; +use crate::platform_impl::wayland; +use crate::platform_impl::wayland::state::WinitState; +use crate::window::ImePurpose; + +pub struct TextInputState { + text_input_manager: ZwpTextInputManagerV3, +} + +impl TextInputState { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let text_input_manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { text_input_manager }) + } +} + +impl Deref for TextInputState { + type Target = ZwpTextInputManagerV3; + + fn deref(&self) -> &Self::Target { + &self.text_input_manager + } +} + +impl Dispatch for TextInputState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpTextInputManagerV3, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for TextInputState { + fn event( + state: &mut WinitState, + text_input: &ZwpTextInputV3, + event: ::Event, + data: &TextInputData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + let windows = state.windows.get_mut(); + let mut text_input_data = data.inner.lock().unwrap(); + match event { + TextInputEvent::Enter { surface } => { + let window_id = wayland::make_wid(&surface); + text_input_data.surface = Some(surface); + + let mut window = match windows.get(&window_id) { + Some(window) => window.lock().unwrap(), + None => return, + }; + + if window.ime_allowed() { + text_input.enable(); + text_input.set_content_type_by_purpose(window.ime_purpose()); + text_input.commit(); + state.events_sink.push_window_event(WindowEvent::Ime(Ime::Enabled), window_id); + } + + window.text_input_entered(text_input); + }, + TextInputEvent::Leave { surface } => { + text_input_data.surface = None; + + // Always issue a disable. + text_input.disable(); + text_input.commit(); + + let window_id = wayland::make_wid(&surface); + + // XXX this check is essential, because `leave` could have a + // reference to nil surface... + let mut window = match windows.get(&window_id) { + Some(window) => window.lock().unwrap(), + None => return, + }; + + window.text_input_left(text_input); + + state.events_sink.push_window_event(WindowEvent::Ime(Ime::Disabled), window_id); + }, + TextInputEvent::PreeditString { text, cursor_begin, cursor_end } => { + let text = text.unwrap_or_default(); + let cursor_begin = usize::try_from(cursor_begin) + .ok() + .and_then(|idx| text.is_char_boundary(idx).then_some(idx)); + let cursor_end = usize::try_from(cursor_end) + .ok() + .and_then(|idx| text.is_char_boundary(idx).then_some(idx)); + + text_input_data.pending_preedit = Some(Preedit { text, cursor_begin, cursor_end }) + }, + TextInputEvent::CommitString { text } => { + text_input_data.pending_preedit = None; + text_input_data.pending_commit = text; + }, + TextInputEvent::Done { .. } => { + let window_id = match text_input_data.surface.as_ref() { + Some(surface) => wayland::make_wid(surface), + None => return, + }; + + // Clear preedit, unless all we'll be doing next is sending a new preedit. + if text_input_data.pending_commit.is_some() + || text_input_data.pending_preedit.is_none() + { + state.events_sink.push_window_event( + WindowEvent::Ime(Ime::Preedit(String::new(), None)), + window_id, + ); + } + + // Send `Commit`. + if let Some(text) = text_input_data.pending_commit.take() { + state + .events_sink + .push_window_event(WindowEvent::Ime(Ime::Commit(text)), window_id); + } + + // Send preedit. + if let Some(preedit) = text_input_data.pending_preedit.take() { + let cursor_range = + preedit.cursor_begin.map(|b| (b, preedit.cursor_end.unwrap_or(b))); + + state.events_sink.push_window_event( + WindowEvent::Ime(Ime::Preedit(preedit.text, cursor_range)), + window_id, + ); + } + }, + TextInputEvent::DeleteSurroundingText { .. } => { + // Not handled. + }, + _ => {}, + } + } +} + +pub trait ZwpTextInputV3Ext { + fn set_content_type_by_purpose(&self, purpose: ImePurpose); +} + +impl ZwpTextInputV3Ext for ZwpTextInputV3 { + fn set_content_type_by_purpose(&self, purpose: ImePurpose) { + let (hint, purpose) = match purpose { + ImePurpose::Normal => (ContentHint::None, ContentPurpose::Normal), + ImePurpose::Password => (ContentHint::SensitiveData, ContentPurpose::Password), + ImePurpose::Terminal => (ContentHint::None, ContentPurpose::Terminal), + }; + self.set_content_type(hint, purpose); + } +} + +/// The Data associated with the text input. +#[derive(Default)] +pub struct TextInputData { + inner: std::sync::Mutex, +} + +#[derive(Default)] +pub struct TextInputDataInner { + /// The `WlSurface` we're performing input to. + surface: Option, + + /// The commit to submit on `done`. + pending_commit: Option, + + /// The preedit to submit on `done`. + pending_preedit: Option, +} + +/// The state of the preedit. +struct Preedit { + text: String, + cursor_begin: Option, + cursor_end: Option, +} + +delegate_dispatch!(WinitState: [ZwpTextInputManagerV3: GlobalData] => TextInputState); +delegate_dispatch!(WinitState: [ZwpTextInputV3: TextInputData] => TextInputState); diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/touch/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/touch/mod.rs new file mode 100644 index 0000000..124504f --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/seat/touch/mod.rs @@ -0,0 +1,220 @@ +//! Touch handling. + +use tracing::warn; + +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::protocol::wl_touch::WlTouch; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; + +use sctk::seat::touch::{TouchData, TouchHandler}; + +use crate::dpi::LogicalPosition; +use crate::event::{Touch, TouchPhase, WindowEvent}; + +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::wayland::{self, DeviceId}; + +impl TouchHandler for WinitState { + fn down( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _: u32, + _: u32, + surface: WlSurface, + id: i32, + position: (f64, f64), + ) { + let window_id = wayland::make_wid(&surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::down without seat"); + return; + }, + }; + + // Update the state of the point. + let location = LogicalPosition::::from(position); + seat_state.touch_map.insert(id, TouchPoint { surface, location }); + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Started, + location: location.to_physical(scale_factor), + force: None, + id: id as u64, + }), + window_id, + ); + } + + fn up( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _: u32, + _: u32, + id: i32, + ) { + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::up without seat"); + return; + }, + }; + + // Remove the touch point. + let touch_point = match seat_state.touch_map.remove(&id) { + Some(touch_point) => touch_point, + None => return, + }; + + let window_id = wayland::make_wid(&touch_point.surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Ended, + location: touch_point.location.to_physical(scale_factor), + force: None, + id: id as u64, + }), + window_id, + ); + } + + fn motion( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _: u32, + id: i32, + position: (f64, f64), + ) { + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::motion without seat"); + return; + }, + }; + + // Remove the touch point. + let touch_point = match seat_state.touch_map.get_mut(&id) { + Some(touch_point) => touch_point, + None => return, + }; + + let window_id = wayland::make_wid(&touch_point.surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + touch_point.location = LogicalPosition::::from(position); + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Moved, + location: touch_point.location.to_physical(scale_factor), + force: None, + id: id as u64, + }), + window_id, + ); + } + + fn cancel(&mut self, _: &Connection, _: &QueueHandle, touch: &WlTouch) { + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::cancel without seat"); + return; + }, + }; + + for (id, touch_point) in seat_state.touch_map.drain() { + let window_id = wayland::make_wid(&touch_point.surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + let location = touch_point.location.to_physical(scale_factor); + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Cancelled, + location, + force: None, + id: id as u64, + }), + window_id, + ); + } + } + + fn shape( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlTouch, + _: i32, + _: f64, + _: f64, + ) { + // Blank. + } + + fn orientation(&mut self, _: &Connection, _: &QueueHandle, _: &WlTouch, _: i32, _: f64) { + // Blank. + } +} + +/// The state of the touch point. +#[derive(Debug)] +pub struct TouchPoint { + /// The surface on which the point is present. + pub surface: WlSurface, + + /// The location of the point on the surface. + pub location: LogicalPosition, +} + +pub trait TouchDataExt { + fn seat(&self) -> &WlSeat; +} + +impl TouchDataExt for WlTouch { + fn seat(&self) -> &WlSeat { + self.data::().expect("failed to get touch data.").seat() + } +} + +sctk::delegate_touch!(WinitState); diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/state.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/state.rs new file mode 100644 index 0000000..13ef99c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/state.rs @@ -0,0 +1,435 @@ +use std::cell::RefCell; +use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex}; + +use ahash::AHashMap; + +use sctk::reexports::calloop::LoopHandle; +use sctk::reexports::client::backend::ObjectId; +use sctk::reexports::client::globals::GlobalList; +use sctk::reexports::client::protocol::wl_output::WlOutput; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; + +use sctk::compositor::{CompositorHandler, CompositorState}; +use sctk::output::{OutputHandler, OutputState}; +use sctk::registry::{ProvidesRegistryState, RegistryState}; +use sctk::seat::pointer::ThemedPointer; +use sctk::seat::SeatState; +use sctk::shell::xdg::window::{Window, WindowConfigure, WindowHandler}; +use sctk::shell::xdg::XdgShell; +use sctk::shell::WaylandSurface; +use sctk::shm::slot::SlotPool; +use sctk::shm::{Shm, ShmHandler}; +use sctk::subcompositor::SubcompositorState; + +use crate::platform_impl::wayland::event_loop::sink::EventSink; +use crate::platform_impl::wayland::output::MonitorHandle; +use crate::platform_impl::wayland::seat::{ + PointerConstraintsState, RelativePointerState, TextInputState, WinitPointerData, + WinitPointerDataExt, WinitSeatState, +}; +use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; +use crate::platform_impl::wayland::types::wp_fractional_scaling::FractionalScalingManager; +use crate::platform_impl::wayland::types::wp_viewporter::ViewporterState; +use crate::platform_impl::wayland::types::xdg_activation::XdgActivationState; +use crate::platform_impl::wayland::window::{WindowRequests, WindowState}; +use crate::platform_impl::wayland::{WaylandError, WindowId}; +use crate::platform_impl::OsError; + +/// Winit's Wayland state. +pub struct WinitState { + /// The WlRegistry. + pub registry_state: RegistryState, + + /// The state of the WlOutput handling. + pub output_state: OutputState, + + /// The compositor state which is used to create new windows and regions. + pub compositor_state: Arc, + + /// The state of the subcompositor. + pub subcompositor_state: Option>, + + /// The seat state responsible for all sorts of input. + pub seat_state: SeatState, + + /// The shm for software buffers, such as cursors. + pub shm: Shm, + + /// The pool where custom cursors are allocated. + pub custom_cursor_pool: Arc>, + + /// The XDG shell that is used for windows. + pub xdg_shell: XdgShell, + + /// The currently present windows. + pub windows: RefCell>>>, + + /// The requests from the `Window` to EventLoop, such as close operations and redraw requests. + pub window_requests: RefCell>>, + + /// The events that were generated directly from the window. + pub window_events_sink: Arc>, + + /// The update for the `windows` coming from the compositor. + pub window_compositor_updates: Vec, + + /// Currently handled seats. + pub seats: AHashMap, + + /// Currently present cursor surfaces. + pub pointer_surfaces: AHashMap>>, + + /// The state of the text input on the client. + pub text_input_state: Option, + + /// Observed monitors. + pub monitors: Arc>>, + + /// Sink to accumulate window events from the compositor, which is latter dispatched in + /// event loop run. + pub events_sink: EventSink, + + /// Xdg activation. + pub xdg_activation: Option, + + /// Relative pointer. + pub relative_pointer: Option, + + /// Pointer constraints to handle pointer locking and confining. + pub pointer_constraints: Option>, + + /// Viewporter state on the given window. + pub viewporter_state: Option, + + /// Fractional scaling manager. + pub fractional_scaling_manager: Option, + + /// KWin blur manager. + pub kwin_blur_manager: Option, + + /// Loop handle to re-register event sources, such as keyboard repeat. + pub loop_handle: LoopHandle<'static, Self>, + + /// Whether we have dispatched events to the user thus we want to + /// send `AboutToWait` and normally wakeup the user. + pub dispatched_events: bool, +} + +impl WinitState { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + loop_handle: LoopHandle<'static, WinitState>, + ) -> Result { + let registry_state = RegistryState::new(globals); + let compositor_state = + CompositorState::bind(globals, queue_handle).map_err(WaylandError::Bind)?; + let subcompositor_state = match SubcompositorState::bind( + compositor_state.wl_compositor().clone(), + globals, + queue_handle, + ) { + Ok(c) => Some(c), + Err(e) => { + tracing::warn!("Subcompositor protocol not available, ignoring CSD: {e:?}"); + None + }, + }; + + let output_state = OutputState::new(globals, queue_handle); + let monitors = output_state.outputs().map(MonitorHandle::new).collect(); + + let seat_state = SeatState::new(globals, queue_handle); + + let mut seats = AHashMap::default(); + for seat in seat_state.seats() { + seats.insert(seat.id(), WinitSeatState::new()); + } + + let (viewporter_state, fractional_scaling_manager) = + if let Ok(fsm) = FractionalScalingManager::new(globals, queue_handle) { + (ViewporterState::new(globals, queue_handle).ok(), Some(fsm)) + } else { + (None, None) + }; + + let shm = Shm::bind(globals, queue_handle).map_err(WaylandError::Bind)?; + let custom_cursor_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap())); + + Ok(Self { + registry_state, + compositor_state: Arc::new(compositor_state), + subcompositor_state: subcompositor_state.map(Arc::new), + output_state, + seat_state, + shm, + custom_cursor_pool, + + xdg_shell: XdgShell::bind(globals, queue_handle).map_err(WaylandError::Bind)?, + xdg_activation: XdgActivationState::bind(globals, queue_handle).ok(), + + windows: Default::default(), + window_requests: Default::default(), + window_compositor_updates: Vec::new(), + window_events_sink: Default::default(), + viewporter_state, + fractional_scaling_manager, + kwin_blur_manager: KWinBlurManager::new(globals, queue_handle).ok(), + + seats, + text_input_state: TextInputState::new(globals, queue_handle).ok(), + + relative_pointer: RelativePointerState::new(globals, queue_handle).ok(), + pointer_constraints: PointerConstraintsState::new(globals, queue_handle) + .map(Arc::new) + .ok(), + pointer_surfaces: Default::default(), + + monitors: Arc::new(Mutex::new(monitors)), + events_sink: EventSink::new(), + loop_handle, + // Make it true by default. + dispatched_events: true, + }) + } + + pub fn scale_factor_changed( + &mut self, + surface: &WlSurface, + scale_factor: f64, + is_legacy: bool, + ) { + // Check if the cursor surface. + let window_id = super::make_wid(surface); + + if let Some(window) = self.windows.get_mut().get(&window_id) { + // Don't update the scaling factor, when legacy method is used. + if is_legacy && self.fractional_scaling_manager.is_some() { + return; + } + + // The scale factor change is for the window. + let pos = if let Some(pos) = self + .window_compositor_updates + .iter() + .position(|update| update.window_id == window_id) + { + pos + } else { + self.window_compositor_updates.push(WindowCompositorUpdate::new(window_id)); + self.window_compositor_updates.len() - 1 + }; + + // Update the scale factor right away. + window.lock().unwrap().set_scale_factor(scale_factor); + self.window_compositor_updates[pos].scale_changed = true; + } else if let Some(pointer) = self.pointer_surfaces.get(&surface.id()) { + // Get the window, where the pointer resides right now. + let focused_window = match pointer.pointer().winit_data().focused_window() { + Some(focused_window) => focused_window, + None => return, + }; + + if let Some(window_state) = self.windows.get_mut().get(&focused_window) { + window_state.lock().unwrap().reload_cursor_style() + } + } + } + + pub fn queue_close(updates: &mut Vec, window_id: WindowId) { + let pos = if let Some(pos) = updates.iter().position(|update| update.window_id == window_id) + { + pos + } else { + updates.push(WindowCompositorUpdate::new(window_id)); + updates.len() - 1 + }; + + updates[pos].close_window = true; + } +} + +impl ShmHandler for WinitState { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm + } +} + +impl WindowHandler for WinitState { + fn request_close(&mut self, _: &Connection, _: &QueueHandle, window: &Window) { + let window_id = super::make_wid(window.wl_surface()); + Self::queue_close(&mut self.window_compositor_updates, window_id); + } + + fn configure( + &mut self, + _: &Connection, + _: &QueueHandle, + window: &Window, + configure: WindowConfigure, + _serial: u32, + ) { + let window_id = super::make_wid(window.wl_surface()); + + let pos = if let Some(pos) = + self.window_compositor_updates.iter().position(|update| update.window_id == window_id) + { + pos + } else { + self.window_compositor_updates.push(WindowCompositorUpdate::new(window_id)); + self.window_compositor_updates.len() - 1 + }; + + // Populate the configure to the window. + self.window_compositor_updates[pos].resized |= self + .windows + .get_mut() + .get_mut(&window_id) + .expect("got configure for dead window.") + .lock() + .unwrap() + .configure(configure, &self.shm, &self.subcompositor_state); + + // NOTE: configure demands wl_surface::commit, however winit doesn't commit on behalf of the + // users, since it can break a lot of things, thus it'll ask users to redraw instead. + self.window_requests + .get_mut() + .get(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + + // Manually mark that we've got an event, since configure may not generate a resize. + self.dispatched_events = true; + } +} + +impl OutputHandler for WinitState { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output(&mut self, _: &Connection, _: &QueueHandle, output: WlOutput) { + self.monitors.lock().unwrap().push(MonitorHandle::new(output)); + } + + fn update_output(&mut self, _: &Connection, _: &QueueHandle, updated: WlOutput) { + let mut monitors = self.monitors.lock().unwrap(); + let updated = MonitorHandle::new(updated); + if let Some(pos) = monitors.iter().position(|output| output == &updated) { + monitors[pos] = updated + } else { + monitors.push(updated) + } + } + + fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle, removed: WlOutput) { + let mut monitors = self.monitors.lock().unwrap(); + let removed = MonitorHandle::new(removed); + if let Some(pos) = monitors.iter().position(|output| output == &removed) { + monitors.remove(pos); + } + } +} + +impl CompositorHandler for WinitState { + fn transform_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: wayland_client::protocol::wl_output::Transform, + ) { + // TODO(kchibisov) we need to expose it somehow in winit. + } + + fn surface_enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: &WlOutput, + ) { + } + + fn surface_leave( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: &WlOutput, + ) { + } + + fn scale_factor_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + surface: &WlSurface, + scale_factor: i32, + ) { + self.scale_factor_changed(surface, scale_factor as f64, true) + } + + fn frame(&mut self, _: &Connection, _: &QueueHandle, surface: &WlSurface, _: u32) { + let window_id = super::make_wid(surface); + let window = match self.windows.get_mut().get(&window_id) { + Some(window) => window, + None => return, + }; + + // In case we have a redraw requested we must indicate the wake up. + if self + .window_requests + .get_mut() + .get(&window_id) + .unwrap() + .redraw_requested + .load(Ordering::Relaxed) + { + self.dispatched_events = true; + } + + window.lock().unwrap().frame_callback_received(); + } +} + +impl ProvidesRegistryState for WinitState { + sctk::registry_handlers![OutputState, SeatState]; + + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } +} + +// The window update coming from the compositor. +#[derive(Debug, Clone, Copy)] +pub struct WindowCompositorUpdate { + /// The id of the window this updates belongs to. + pub window_id: WindowId, + + /// New window size. + pub resized: bool, + + /// New scale factor. + pub scale_changed: bool, + + /// Close the window. + pub close_window: bool, +} + +impl WindowCompositorUpdate { + fn new(window_id: WindowId) -> Self { + Self { window_id, resized: false, scale_changed: false, close_window: false } + } +} + +sctk::delegate_subcompositor!(WinitState); +sctk::delegate_compositor!(WinitState); +sctk::delegate_output!(WinitState); +sctk::delegate_registry!(WinitState); +sctk::delegate_shm!(WinitState); +sctk::delegate_xdg_shell!(WinitState); +sctk::delegate_xdg_window!(WinitState); diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/cursor.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/cursor.rs new file mode 100644 index 0000000..c1a0c26 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/cursor.rs @@ -0,0 +1,59 @@ +use cursor_icon::CursorIcon; + +use sctk::reexports::client::protocol::wl_shm::Format; +use sctk::shm::slot::{Buffer, SlotPool}; + +use crate::cursor::CursorImage; + +#[derive(Debug)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(CustomCursor), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} + +#[derive(Debug)] +pub struct CustomCursor { + pub buffer: Buffer, + pub w: i32, + pub h: i32, + pub hotspot_x: i32, + pub hotspot_y: i32, +} + +impl CustomCursor { + pub(crate) fn new(pool: &mut SlotPool, image: &CursorImage) -> Self { + let (buffer, canvas) = pool + .create_buffer( + image.width as i32, + image.height as i32, + 4 * (image.width as i32), + Format::Argb8888, + ) + .unwrap(); + + for (canvas_chunk, rgba) in canvas.chunks_exact_mut(4).zip(image.rgba.chunks_exact(4)) { + // Alpha in buffer is premultiplied. + let alpha = rgba[3] as f32 / 255.; + let r = (rgba[0] as f32 * alpha) as u32; + let g = (rgba[1] as f32 * alpha) as u32; + let b = (rgba[2] as f32 * alpha) as u32; + let color = ((rgba[3] as u32) << 24) + (r << 16) + (g << 8) + b; + let array: &mut [u8; 4] = canvas_chunk.try_into().unwrap(); + *array = color.to_le_bytes(); + } + + CustomCursor { + buffer, + w: image.width as i32, + h: image.height as i32, + hotspot_x: image.hotspot_x as i32, + hotspot_y: image.hotspot_y as i32, + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/kwin_blur.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/kwin_blur.rs new file mode 100644 index 0000000..83e82ad --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/kwin_blur.rs @@ -0,0 +1,68 @@ +//! Handling of KDE-compatible blur. + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle}; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur_manager::OrgKdeKwinBlurManager; + +use sctk::globals::GlobalData; + +use crate::platform_impl::wayland::state::WinitState; + +/// KWin blur manager. +#[derive(Debug, Clone)] +pub struct KWinBlurManager { + manager: OrgKdeKwinBlurManager, +} + +impl KWinBlurManager { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { manager }) + } + + pub fn blur( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> OrgKdeKwinBlur { + self.manager.create(surface, queue_handle, ()) + } + + pub fn unset(&self, surface: &WlSurface) { + self.manager.unset(surface) + } +} + +impl Dispatch for KWinBlurManager { + fn event( + _: &mut WinitState, + _: &OrgKdeKwinBlurManager, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("no events defined for org_kde_kwin_blur_manager"); + } +} + +impl Dispatch for KWinBlurManager { + fn event( + _: &mut WinitState, + _: &OrgKdeKwinBlur, + _: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("no events defined for org_kde_kwin_blur"); + } +} + +delegate_dispatch!(WinitState: [OrgKdeKwinBlurManager: GlobalData] => KWinBlurManager); +delegate_dispatch!(WinitState: [OrgKdeKwinBlur: ()] => KWinBlurManager); diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/mod.rs new file mode 100644 index 0000000..77e67f4 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/mod.rs @@ -0,0 +1,7 @@ +//! Wayland protocol implementation boilerplate. + +pub mod cursor; +pub mod kwin_blur; +pub mod wp_fractional_scaling; +pub mod wp_viewporter; +pub mod xdg_activation; diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs new file mode 100644 index 0000000..2dfc9db --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs @@ -0,0 +1,78 @@ +//! Handling of the fractional scaling. + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::{ + Event as FractionalScalingEvent, WpFractionalScaleV1, +}; + +use sctk::globals::GlobalData; + +use crate::platform_impl::wayland::state::WinitState; + +/// The scaling factor denominator. +const SCALE_DENOMINATOR: f64 = 120.; + +/// Fractional scaling manager. +#[derive(Debug)] +pub struct FractionalScalingManager { + manager: WpFractionalScaleManagerV1, +} + +pub struct FractionalScaling { + /// The surface used for scaling. + surface: WlSurface, +} + +impl FractionalScalingManager { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { manager }) + } + + pub fn fractional_scaling( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> WpFractionalScaleV1 { + let data = FractionalScaling { surface: surface.clone() }; + self.manager.get_fractional_scale(surface, queue_handle, data) + } +} + +impl Dispatch for FractionalScalingManager { + fn event( + _: &mut WinitState, + _: &WpFractionalScaleManagerV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} + +impl Dispatch for FractionalScalingManager { + fn event( + state: &mut WinitState, + _: &WpFractionalScaleV1, + event: ::Event, + data: &FractionalScaling, + _: &Connection, + _: &QueueHandle, + ) { + if let FractionalScalingEvent::PreferredScale { scale } = event { + state.scale_factor_changed(&data.surface, scale as f64 / SCALE_DENOMINATOR, false); + } + } +} + +delegate_dispatch!(WinitState: [WpFractionalScaleManagerV1: GlobalData] => FractionalScalingManager); +delegate_dispatch!(WinitState: [WpFractionalScaleV1: FractionalScaling] => FractionalScalingManager); diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/wp_viewporter.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/wp_viewporter.rs new file mode 100644 index 0000000..9076482 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/wp_viewporter.rs @@ -0,0 +1,65 @@ +//! Handling of the wp-viewporter. + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewporter::WpViewporter; + +use sctk::globals::GlobalData; + +use crate::platform_impl::wayland::state::WinitState; + +/// Viewporter. +#[derive(Debug)] +pub struct ViewporterState { + viewporter: WpViewporter, +} + +impl ViewporterState { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let viewporter = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { viewporter }) + } + + /// Get the viewport for the given object. + pub fn get_viewport( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> WpViewport { + self.viewporter.get_viewport(surface, queue_handle, GlobalData) + } +} + +impl Dispatch for ViewporterState { + fn event( + _: &mut WinitState, + _: &WpViewporter, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} +impl Dispatch for ViewporterState { + fn event( + _: &mut WinitState, + _: &WpViewport, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} + +delegate_dispatch!(WinitState: [WpViewporter: GlobalData] => ViewporterState); +delegate_dispatch!(WinitState: [WpViewport: GlobalData] => ViewporterState); diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/xdg_activation.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/xdg_activation.rs new file mode 100644 index 0000000..9efc75d --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/types/xdg_activation.rs @@ -0,0 +1,103 @@ +//! Handling of xdg activation, which is used for user attention requests. + +use std::sync::atomic::AtomicBool; +use std::sync::Weak; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle}; +use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_token_v1::{ + Event as ActivationTokenEvent, XdgActivationTokenV1, +}; +use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_v1::XdgActivationV1; + +use sctk::globals::GlobalData; + +use crate::event_loop::AsyncRequestSerial; +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::WindowId; +use crate::window::ActivationToken; + +pub struct XdgActivationState { + xdg_activation: XdgActivationV1, +} + +impl XdgActivationState { + pub fn bind( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let xdg_activation = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { xdg_activation }) + } + + pub fn global(&self) -> &XdgActivationV1 { + &self.xdg_activation + } +} + +impl Dispatch for XdgActivationState { + fn event( + _state: &mut WinitState, + _proxy: &XdgActivationV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for XdgActivationState { + fn event( + state: &mut WinitState, + proxy: &XdgActivationTokenV1, + event: ::Event, + data: &XdgActivationTokenData, + _: &Connection, + _: &QueueHandle, + ) { + let token = match event { + ActivationTokenEvent::Done { token } => token, + _ => return, + }; + + let global = state + .xdg_activation + .as_ref() + .expect("got xdg_activation event without global.") + .global(); + + match data { + XdgActivationTokenData::Attention((surface, fence)) => { + global.activate(token, surface); + // Mark that no request attention is in process. + if let Some(attention_requested) = fence.upgrade() { + attention_requested.store(false, std::sync::atomic::Ordering::Relaxed); + } + }, + XdgActivationTokenData::Obtain((window_id, serial)) => { + state.events_sink.push_window_event( + crate::event::WindowEvent::ActivationTokenDone { + serial: *serial, + token: ActivationToken::from_raw(token), + }, + *window_id, + ); + }, + } + + proxy.destroy(); + } +} + +/// The data associated with the activation request. +pub enum XdgActivationTokenData { + /// Request user attention for the given surface. + Attention((WlSurface, Weak)), + /// Get a token to be passed outside of the winit. + Obtain((WindowId, AsyncRequestSerial)), +} + +delegate_dispatch!(WinitState: [ XdgActivationV1: GlobalData] => XdgActivationState); +delegate_dispatch!(WinitState: [ XdgActivationTokenV1: XdgActivationTokenData] => XdgActivationState); diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/window/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/window/mod.rs new file mode 100644 index 0000000..6d29a5a --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/window/mod.rs @@ -0,0 +1,763 @@ +//! The Wayland window. + +use std::ffi::c_void; +use std::ptr::NonNull; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use sctk::reexports::client::protocol::wl_display::WlDisplay; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Proxy, QueueHandle}; + +use sctk::compositor::{CompositorState, Region, SurfaceData}; +use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_v1::XdgActivationV1; +use sctk::shell::xdg::window::{Window as SctkWindow, WindowDecorations}; +use sctk::shell::WaylandSurface; + +use tracing::warn; + +use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::{Ime, WindowEvent}; +use crate::event_loop::AsyncRequestSerial; +use crate::platform_impl::{ + Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformIcon, +}; +use crate::window::{ + Cursor, CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, +}; + +use super::event_loop::sink::EventSink; +use super::output::MonitorHandle; +use super::state::WinitState; +use super::types::xdg_activation::XdgActivationTokenData; +use super::{ActiveEventLoop, WaylandError, WindowId}; + +pub(crate) mod state; + +pub use state::WindowState; + +/// The Wayland window. +pub struct Window { + /// Reference to the underlying SCTK window. + window: SctkWindow, + + /// Window id. + window_id: WindowId, + + /// The state of the window. + window_state: Arc>, + + /// Compositor to handle WlRegion stuff. + compositor: Arc, + + /// The wayland display used solely for raw window handle. + #[allow(dead_code)] + display: WlDisplay, + + /// Xdg activation to request user attention. + xdg_activation: Option, + + /// The state of the requested attention from the `xdg_activation`. + attention_requested: Arc, + + /// Handle to the main queue to perform requests. + queue_handle: QueueHandle, + + /// Window requests to the event loop. + window_requests: Arc, + + /// Observed monitors. + monitors: Arc>>, + + /// Source to wake-up the event-loop for window requests. + event_loop_awakener: calloop::ping::Ping, + + /// The event sink to deliver synthetic events. + window_events_sink: Arc>, +} + +impl Window { + pub(crate) fn new( + event_loop_window_target: &ActiveEventLoop, + attributes: WindowAttributes, + ) -> Result { + let queue_handle = event_loop_window_target.queue_handle.clone(); + let mut state = event_loop_window_target.state.borrow_mut(); + + let monitors = state.monitors.clone(); + + let surface = state.compositor_state.create_surface(&queue_handle); + let compositor = state.compositor_state.clone(); + let xdg_activation = + state.xdg_activation.as_ref().map(|activation_state| activation_state.global().clone()); + let display = event_loop_window_target.connection.display(); + + let size: Size = attributes.inner_size.unwrap_or(LogicalSize::new(800., 600.).into()); + + // We prefer server side decorations, however to not have decorations we ask for client + // side decorations instead. + let default_decorations = if attributes.decorations { + WindowDecorations::RequestServer + } else { + WindowDecorations::RequestClient + }; + + let window = + state.xdg_shell.create_window(surface.clone(), default_decorations, &queue_handle); + + let mut window_state = WindowState::new( + event_loop_window_target.connection.clone(), + &event_loop_window_target.queue_handle, + &state, + size, + window.clone(), + attributes.preferred_theme, + ); + + // Set transparency hint. + window_state.set_transparent(attributes.transparent); + + window_state.set_blur(attributes.blur); + + // Set the decorations hint. + window_state.set_decorate(attributes.decorations); + + // Set the app_id. + if let Some(name) = attributes.platform_specific.name.map(|name| name.general) { + window.set_app_id(name); + } + + // Set the window title. + window_state.set_title(attributes.title); + + // Set the min and max sizes. We must set the hints upon creating a window, so + // we use the default `1.` scaling... + let min_size = attributes.min_inner_size.map(|size| size.to_logical(1.)); + let max_size = attributes.max_inner_size.map(|size| size.to_logical(1.)); + window_state.set_min_inner_size(min_size); + window_state.set_max_inner_size(max_size); + + // Non-resizable implies that the min and max sizes are set to the same value. + window_state.set_resizable(attributes.resizable); + + // Set startup mode. + match attributes.fullscreen.map(Into::into) { + Some(Fullscreen::Exclusive(_)) => { + warn!("`Fullscreen::Exclusive` is ignored on Wayland"); + }, + #[cfg_attr(not(x11_platform), allow(clippy::bind_instead_of_map))] + Some(Fullscreen::Borderless(monitor)) => { + let output = monitor.and_then(|monitor| match monitor { + PlatformMonitorHandle::Wayland(monitor) => Some(monitor.proxy), + #[cfg(x11_platform)] + PlatformMonitorHandle::X(_) => None, + }); + + window.set_fullscreen(output.as_ref()) + }, + _ if attributes.maximized => window.set_maximized(), + _ => (), + }; + + match attributes.cursor { + Cursor::Icon(icon) => window_state.set_cursor(icon), + Cursor::Custom(cursor) => window_state.set_custom_cursor(cursor), + } + + // Apply resize increments. + if let Some(increments) = attributes.resize_increments { + let increments = increments.to_logical(window_state.scale_factor()); + window_state.set_resize_increments(Some(increments)); + } + + // Activate the window when the token is passed. + if let (Some(xdg_activation), Some(token)) = + (xdg_activation.as_ref(), attributes.platform_specific.activation_token) + { + xdg_activation.activate(token.token, &surface); + } + + // XXX Do initial commit. + window.commit(); + + // Add the window and window requests into the state. + let window_state = Arc::new(Mutex::new(window_state)); + let window_id = super::make_wid(&surface); + state.windows.get_mut().insert(window_id, window_state.clone()); + + let window_requests = WindowRequests { + redraw_requested: AtomicBool::new(true), + closed: AtomicBool::new(false), + }; + let window_requests = Arc::new(window_requests); + state.window_requests.get_mut().insert(window_id, window_requests.clone()); + + // Setup the event sync to insert `WindowEvents` right from the window. + let window_events_sink = state.window_events_sink.clone(); + + let mut wayland_source = event_loop_window_target.wayland_dispatcher.as_source_mut(); + let event_queue = wayland_source.queue(); + + // Do a roundtrip. + event_queue.roundtrip(&mut state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch(error)))) + })?; + + // XXX Wait for the initial configure to arrive. + while !window_state.lock().unwrap().is_configured() { + event_queue.blocking_dispatch(&mut state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch(error)))) + })?; + } + + // Wake-up event loop, so it'll send initial redraw requested. + let event_loop_awakener = event_loop_window_target.event_loop_awakener.clone(); + event_loop_awakener.ping(); + + Ok(Self { + window, + display, + monitors, + window_id, + compositor, + window_state, + queue_handle, + xdg_activation, + attention_requested: Arc::new(AtomicBool::new(false)), + event_loop_awakener, + window_requests, + window_events_sink, + }) + } + + pub(crate) fn xdg_toplevel(&self) -> Option> { + NonNull::new(self.window.xdg_toplevel().id().as_ptr().cast()) + } +} + +impl Window { + #[inline] + pub fn id(&self) -> WindowId { + self.window_id + } + + #[inline] + pub fn set_title(&self, title: impl ToString) { + let new_title = title.to_string(); + self.window_state.lock().unwrap().set_title(new_title); + } + + #[inline] + pub fn set_visible(&self, _visible: bool) { + // Not possible on Wayland. + } + + #[inline] + pub fn is_visible(&self) -> Option { + None + } + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + Err(NotSupportedError::new()) + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + Err(NotSupportedError::new()) + } + + #[inline] + pub fn set_outer_position(&self, _: Position) { + // Not possible on Wayland. + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + super::logical_to_physical_rounded(window_state.inner_size(), scale_factor) + } + + #[inline] + pub fn request_redraw(&self) { + // NOTE: try to not wake up the loop when the event was already scheduled and not yet + // processed by the loop, because if at this point the value was `true` it could only + // mean that the loop still haven't dispatched the value to the client and will do + // eventually, resetting it to `false`. + if self + .window_requests + .redraw_requested + .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + self.event_loop_awakener.ping(); + } + } + + #[inline] + pub fn pre_present_notify(&self) { + self.window_state.lock().unwrap().request_frame_callback(); + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + super::logical_to_physical_rounded(window_state.outer_size(), scale_factor) + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let mut window_state = self.window_state.lock().unwrap(); + let new_size = window_state.request_inner_size(size); + self.request_redraw(); + Some(new_size) + } + + /// Set the minimum inner size for the window. + #[inline] + pub fn set_min_inner_size(&self, min_size: Option) { + let scale_factor = self.scale_factor(); + let min_size = min_size.map(|size| size.to_logical(scale_factor)); + self.window_state.lock().unwrap().set_min_inner_size(min_size); + // NOTE: Requires commit to be applied. + self.request_redraw(); + } + + /// Set the maximum inner size for the window. + #[inline] + pub fn set_max_inner_size(&self, max_size: Option) { + let scale_factor = self.scale_factor(); + let max_size = max_size.map(|size| size.to_logical(scale_factor)); + self.window_state.lock().unwrap().set_max_inner_size(max_size); + // NOTE: Requires commit to be applied. + self.request_redraw(); + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + let window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + window_state + .resize_increments() + .map(|size| super::logical_to_physical_rounded(size, scale_factor)) + } + + #[inline] + pub fn set_resize_increments(&self, increments: Option) { + let mut window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + let increments = increments.map(|size| size.to_logical(scale_factor)); + window_state.set_resize_increments(increments); + } + + #[inline] + pub fn set_transparent(&self, transparent: bool) { + self.window_state.lock().unwrap().set_transparent(transparent); + } + + #[inline] + pub fn has_focus(&self) -> bool { + self.window_state.lock().unwrap().has_focus() + } + + #[inline] + pub fn is_minimized(&self) -> Option { + // XXX clients don't know whether they are minimized or not. + None + } + + #[inline] + pub fn show_window_menu(&self, position: Position) { + let scale_factor = self.scale_factor(); + let position = position.to_logical(scale_factor); + self.window_state.lock().unwrap().show_window_menu(position); + } + + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + self.window_state.lock().unwrap().drag_resize_window(direction) + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + if self.window_state.lock().unwrap().set_resizable(resizable) { + // NOTE: Requires commit to be applied. + self.request_redraw(); + } + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.window_state.lock().unwrap().resizable() + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) { + // TODO(kchibisov) v5 of the xdg_shell allows that. + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + // TODO(kchibisov) v5 of the xdg_shell allows that. + WindowButtons::all() + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.window_state.lock().unwrap().scale_factor() + } + + #[inline] + pub fn set_blur(&self, blur: bool) { + self.window_state.lock().unwrap().set_blur(blur); + } + + #[inline] + pub fn set_decorations(&self, decorate: bool) { + self.window_state.lock().unwrap().set_decorate(decorate) + } + + #[inline] + pub fn is_decorated(&self) -> bool { + self.window_state.lock().unwrap().is_decorated() + } + + #[inline] + pub fn set_window_level(&self, _level: WindowLevel) {} + + #[inline] + pub(crate) fn set_window_icon(&self, _window_icon: Option) {} + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + // You can't unminimize the window on Wayland. + if !minimized { + warn!("Unminimizing is ignored on Wayland."); + return; + } + + self.window.set_minimized(); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + self.window_state + .lock() + .unwrap() + .last_configure + .as_ref() + .map(|last_configure| last_configure.is_maximized()) + .unwrap_or_default() + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + if maximized { + self.window.set_maximized() + } else { + self.window.unset_maximized() + } + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + let is_fullscreen = self + .window_state + .lock() + .unwrap() + .last_configure + .as_ref() + .map(|last_configure| last_configure.is_fullscreen()) + .unwrap_or_default(); + + if is_fullscreen { + let current_monitor = self.current_monitor().map(PlatformMonitorHandle::Wayland); + Some(Fullscreen::Borderless(current_monitor)) + } else { + None + } + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + match fullscreen { + Some(Fullscreen::Exclusive(_)) => { + warn!("`Fullscreen::Exclusive` is ignored on Wayland"); + }, + #[cfg_attr(not(x11_platform), allow(clippy::bind_instead_of_map))] + Some(Fullscreen::Borderless(monitor)) => { + let output = monitor.and_then(|monitor| match monitor { + PlatformMonitorHandle::Wayland(monitor) => Some(monitor.proxy), + #[cfg(x11_platform)] + PlatformMonitorHandle::X(_) => None, + }); + + self.window.set_fullscreen(output.as_ref()) + }, + None => self.window.unset_fullscreen(), + } + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + let window_state = &mut self.window_state.lock().unwrap(); + + match cursor { + Cursor::Icon(icon) => window_state.set_cursor(icon), + Cursor::Custom(cursor) => window_state.set_custom_cursor(cursor), + } + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + self.window_state.lock().unwrap().set_cursor_visible(visible); + } + + pub fn request_user_attention(&self, request_type: Option) { + let xdg_activation = match self.xdg_activation.as_ref() { + Some(xdg_activation) => xdg_activation, + None => { + warn!("`request_user_attention` isn't supported"); + return; + }, + }; + + // Urgency is only removed by the compositor and there's no need to raise urgency when it + // was already raised. + if request_type.is_none() || self.attention_requested.load(Ordering::Relaxed) { + return; + } + + self.attention_requested.store(true, Ordering::Relaxed); + let surface = self.surface().clone(); + let data = XdgActivationTokenData::Attention(( + surface.clone(), + Arc::downgrade(&self.attention_requested), + )); + let xdg_activation_token = xdg_activation.get_activation_token(&self.queue_handle, data); + xdg_activation_token.set_surface(&surface); + xdg_activation_token.commit(); + } + + pub fn request_activation_token(&self) -> Result { + let xdg_activation = match self.xdg_activation.as_ref() { + Some(xdg_activation) => xdg_activation, + None => return Err(NotSupportedError::new()), + }; + + let serial = AsyncRequestSerial::get(); + + let data = XdgActivationTokenData::Obtain((self.window_id, serial)); + let xdg_activation_token = xdg_activation.get_activation_token(&self.queue_handle, data); + xdg_activation_token.set_surface(self.surface()); + xdg_activation_token.commit(); + + Ok(serial) + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + self.window_state.lock().unwrap().set_cursor_grab(mode) + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + let scale_factor = self.scale_factor(); + let position = position.to_logical(scale_factor); + self.window_state + .lock() + .unwrap() + .set_cursor_position(position) + // Request redraw on success, since the state is double buffered. + .map(|_| self.request_redraw()) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + self.window_state.lock().unwrap().drag_window() + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let surface = self.window.wl_surface(); + + if hittest { + surface.set_input_region(None); + Ok(()) + } else { + let region = Region::new(&*self.compositor).map_err(|_| { + ExternalError::Os(os_error!(OsError::Misc("failed to set input region."))) + })?; + region.add(0, 0, 0, 0); + surface.set_input_region(Some(region.wl_region())); + Ok(()) + } + } + + #[inline] + pub fn set_ime_cursor_area(&self, position: Position, size: Size) { + let window_state = self.window_state.lock().unwrap(); + if window_state.ime_allowed() { + let scale_factor = window_state.scale_factor(); + let position = position.to_logical(scale_factor); + let size = size.to_logical(scale_factor); + window_state.set_ime_cursor_area(position, size); + } + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let mut window_state = self.window_state.lock().unwrap(); + + if window_state.ime_allowed() != allowed && window_state.set_ime_allowed(allowed) { + let event = WindowEvent::Ime(if allowed { Ime::Enabled } else { Ime::Disabled }); + self.window_events_sink.lock().unwrap().push_window_event(event, self.window_id); + self.event_loop_awakener.ping(); + } + } + + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + self.window_state.lock().unwrap().set_ime_purpose(purpose); + } + + #[inline] + pub fn focus_window(&self) {} + + #[inline] + pub fn surface(&self) -> &WlSurface { + self.window.wl_surface() + } + + #[inline] + pub fn current_monitor(&self) -> Option { + let data = self.window.wl_surface().data::()?; + data.outputs().next().map(MonitorHandle::new) + } + + #[inline] + pub fn available_monitors(&self) -> Vec { + self.monitors.lock().unwrap().clone() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + // XXX there's no such concept on Wayland. + None + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::WaylandHandle::empty(); + window_handle.surface = self.window.wl_surface().id().as_ptr() as *mut _; + window_handle.display = self.display.id().as_ptr() as *mut _; + rwh_04::RawWindowHandle::Wayland(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::WaylandWindowHandle::empty(); + window_handle.surface = self.window.wl_surface().id().as_ptr() as *mut _; + rwh_05::RawWindowHandle::Wayland(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + let mut display_handle = rwh_05::WaylandDisplayHandle::empty(); + display_handle.display = self.display.id().as_ptr() as *mut _; + rwh_05::RawDisplayHandle::Wayland(display_handle) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + Ok(rwh_06::WaylandWindowHandle::new({ + let ptr = self.window.wl_surface().id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_surface will never be null") + }) + .into()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::WaylandDisplayHandle::new({ + let ptr = self.display.id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_proxy should never be null") + }) + .into()) + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + self.window_state.lock().unwrap().set_theme(theme) + } + + #[inline] + pub fn theme(&self) -> Option { + self.window_state.lock().unwrap().theme() + } + + pub fn set_content_protected(&self, _protected: bool) {} + + #[inline] + pub fn title(&self) -> String { + self.window_state.lock().unwrap().title().to_owned() + } +} + +impl Drop for Window { + fn drop(&mut self) { + self.window_requests.closed.store(true, Ordering::Relaxed); + self.event_loop_awakener.ping(); + } +} + +/// The request from the window to the event loop. +#[derive(Debug)] +pub struct WindowRequests { + /// The window was closed. + pub closed: AtomicBool, + + /// Redraw Requested. + pub redraw_requested: AtomicBool, +} + +impl WindowRequests { + pub fn take_closed(&self) -> bool { + self.closed.swap(false, Ordering::Relaxed) + } + + pub fn take_redraw_requested(&self) -> bool { + self.redraw_requested.swap(false, Ordering::Relaxed) + } +} + +impl TryFrom<&str> for Theme { + type Error = (); + + /// ``` + /// use winit::window::Theme; + /// + /// assert_eq!("dark".try_into(), Ok(Theme::Dark)); + /// assert_eq!("lIghT".try_into(), Ok(Theme::Light)); + /// ``` + fn try_from(theme: &str) -> Result { + if theme.eq_ignore_ascii_case("dark") { + Ok(Self::Dark) + } else if theme.eq_ignore_ascii_case("light") { + Ok(Self::Light) + } else { + Err(()) + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/wayland/window/state.rs b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/window/state.rs new file mode 100644 index 0000000..1ef7a06 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/wayland/window/state.rs @@ -0,0 +1,1220 @@ +//! The state of the window, which is shared with the event-loop. + +use std::num::NonZeroU32; +use std::sync::{Arc, Mutex, Weak}; +use std::time::Duration; + +use ahash::HashSet; +use tracing::{info, warn}; + +use sctk::reexports::client::backend::ObjectId; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_shm::WlShm; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; +use sctk::reexports::csd_frame::{ + DecorationsFrame, FrameAction, FrameClick, ResizeEdge, WindowState as XdgWindowState, +}; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; +use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as XdgResizeEdge; + +use sctk::compositor::{CompositorState, Region, SurfaceData, SurfaceDataExt}; +use sctk::seat::pointer::{PointerDataExt, ThemedPointer}; +use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure}; +use sctk::shell::xdg::XdgSurface; +use sctk::shell::WaylandSurface; +use sctk::shm::slot::SlotPool; +use sctk::shm::Shm; +use sctk::subcompositor::SubcompositorState; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; + +use crate::cursor::CustomCursor as RootCustomCursor; +use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Size}; +use crate::error::{ExternalError, NotSupportedError}; +use crate::platform_impl::wayland::logical_to_physical_rounded; +use crate::platform_impl::wayland::types::cursor::{CustomCursor, SelectedCursor}; +use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; +use crate::platform_impl::{PlatformCustomCursor, WindowId}; +use crate::window::{CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme}; + +use crate::platform_impl::wayland::seat::{ + PointerConstraintsState, WinitPointerData, WinitPointerDataExt, ZwpTextInputV3Ext, +}; +use crate::platform_impl::wayland::state::{WindowCompositorUpdate, WinitState}; + +#[cfg(feature = "sctk-adwaita")] +pub type WinitFrame = sctk_adwaita::AdwaitaFrame; +#[cfg(not(feature = "sctk-adwaita"))] +pub type WinitFrame = sctk::shell::xdg::fallback_frame::FallbackFrame; + +// Minimum window inner size. +const MIN_WINDOW_SIZE: LogicalSize = LogicalSize::new(2, 1); + +/// The state of the window which is being updated from the [`WinitState`]. +pub struct WindowState { + /// The connection to Wayland server. + pub connection: Connection, + + /// The `Shm` to set cursor. + pub shm: WlShm, + + // A shared pool where to allocate custom cursors. + custom_cursor_pool: Arc>, + + /// The last received configure. + pub last_configure: Option, + + /// The pointers observed on the window. + pub pointers: Vec>>, + + selected_cursor: SelectedCursor, + + /// Whether the cursor is visible. + pub cursor_visible: bool, + + /// Pointer constraints to lock/confine pointer. + pub pointer_constraints: Option>, + + /// Queue handle. + pub queue_handle: QueueHandle, + + /// Theme variant. + theme: Option, + + /// The current window title. + title: String, + + /// Whether the frame is resizable. + resizable: bool, + + // NOTE: we can't use simple counter, since it's racy when seat getting destroyed and new + // is created, since add/removed stuff could be delivered a bit out of order. + /// Seats that has keyboard focus on that window. + seat_focus: HashSet, + + /// The scale factor of the window. + scale_factor: f64, + + /// Whether the window is transparent. + transparent: bool, + + /// The state of the compositor to create WlRegions. + compositor: Arc, + + /// The current cursor grabbing mode. + cursor_grab_mode: GrabState, + + /// Whether the IME input is allowed for that window. + ime_allowed: bool, + + /// The current IME purpose. + ime_purpose: ImePurpose, + + /// The text inputs observed on the window. + text_inputs: Vec, + + /// The inner size of the window, as in without client side decorations. + size: LogicalSize, + + /// Whether the CSD fail to create, so we don't try to create them on each iteration. + csd_fails: bool, + + /// Whether we should decorate the frame. + decorate: bool, + + /// Min size. + min_inner_size: LogicalSize, + max_inner_size: Option>, + resize_increments: Option>, + + /// The size of the window when no states were applied to it. The primary use for it + /// is to fallback to original window size, before it was maximized, if the compositor + /// sends `None` for the new size in the configure. + stateless_size: LogicalSize, + + /// Initial window size provided by the user. Removed on the first + /// configure. + initial_size: Option, + + /// The state of the frame callback. + frame_callback_state: FrameCallbackState, + + viewport: Option, + fractional_scale: Option, + blur: Option, + blur_manager: Option, + + /// Whether the client side decorations have pending move operations. + /// + /// The value is the serial of the event triggered moved. + has_pending_move: Option, + + /// The underlying SCTK window. + pub window: Window, + + // NOTE: The spec says that destroying parent(`window` in our case), will unmap the + // subsurfaces. Thus to achieve atomic unmap of the client, drop the decorations + // frame after the `window` is dropped. To achieve that we rely on rust's struct + // field drop order guarantees. + /// The window frame, which is created from the configure request. + frame: Option, +} + +impl WindowState { + /// Create new window state. + pub fn new( + connection: Connection, + queue_handle: &QueueHandle, + winit_state: &WinitState, + initial_size: Size, + window: Window, + theme: Option, + ) -> Self { + let compositor = winit_state.compositor_state.clone(); + let pointer_constraints = winit_state.pointer_constraints.clone(); + let viewport = winit_state + .viewporter_state + .as_ref() + .map(|state| state.get_viewport(window.wl_surface(), queue_handle)); + let fractional_scale = winit_state + .fractional_scaling_manager + .as_ref() + .map(|fsm| fsm.fractional_scaling(window.wl_surface(), queue_handle)); + + Self { + blur: None, + blur_manager: winit_state.kwin_blur_manager.clone(), + compositor, + connection, + csd_fails: false, + cursor_grab_mode: GrabState::new(), + selected_cursor: Default::default(), + cursor_visible: true, + decorate: true, + fractional_scale, + frame: None, + frame_callback_state: FrameCallbackState::None, + seat_focus: Default::default(), + has_pending_move: None, + ime_allowed: false, + ime_purpose: ImePurpose::Normal, + last_configure: None, + max_inner_size: None, + min_inner_size: MIN_WINDOW_SIZE, + resize_increments: None, + pointer_constraints, + pointers: Default::default(), + queue_handle: queue_handle.clone(), + resizable: true, + scale_factor: 1., + shm: winit_state.shm.wl_shm().clone(), + custom_cursor_pool: winit_state.custom_cursor_pool.clone(), + size: initial_size.to_logical(1.), + stateless_size: initial_size.to_logical(1.), + initial_size: Some(initial_size), + text_inputs: Vec::new(), + theme, + title: String::default(), + transparent: false, + viewport, + window, + } + } + + /// Apply closure on the given pointer. + fn apply_on_pointer, &WinitPointerData)>( + &self, + mut callback: F, + ) { + self.pointers.iter().filter_map(Weak::upgrade).for_each(|pointer| { + let data = pointer.pointer().winit_data(); + callback(pointer.as_ref(), data); + }) + } + + /// Get the current state of the frame callback. + pub fn frame_callback_state(&self) -> FrameCallbackState { + self.frame_callback_state + } + + /// The frame callback was received, but not yet sent to the user. + pub fn frame_callback_received(&mut self) { + self.frame_callback_state = FrameCallbackState::Received; + } + + /// Reset the frame callbacks state. + pub fn frame_callback_reset(&mut self) { + self.frame_callback_state = FrameCallbackState::None; + } + + /// Request a frame callback if we don't have one for this window in flight. + pub fn request_frame_callback(&mut self) { + let surface = self.window.wl_surface(); + match self.frame_callback_state { + FrameCallbackState::None | FrameCallbackState::Received => { + self.frame_callback_state = FrameCallbackState::Requested; + surface.frame(&self.queue_handle, surface.clone()); + }, + FrameCallbackState::Requested => (), + } + } + + pub fn configure( + &mut self, + configure: WindowConfigure, + shm: &Shm, + subcompositor: &Option>, + ) -> bool { + // NOTE: when using fractional scaling or wl_compositor@v6 the scaling + // should be delivered before the first configure, thus apply it to + // properly scale the physical sizes provided by the users. + if let Some(initial_size) = self.initial_size.take() { + self.size = initial_size.to_logical(self.scale_factor()); + self.stateless_size = self.size; + } + + if let Some(subcompositor) = subcompositor.as_ref().filter(|_| { + configure.decoration_mode == DecorationMode::Client + && self.frame.is_none() + && !self.csd_fails + }) { + match WinitFrame::new( + &self.window, + shm, + #[cfg(feature = "sctk-adwaita")] + self.compositor.clone(), + subcompositor.clone(), + self.queue_handle.clone(), + #[cfg(feature = "sctk-adwaita")] + into_sctk_adwaita_config(self.theme), + ) { + Ok(mut frame) => { + frame.set_title(&self.title); + frame.set_scaling_factor(self.scale_factor); + // Hide the frame if we were asked to not decorate. + frame.set_hidden(!self.decorate); + self.frame = Some(frame); + }, + Err(err) => { + warn!("Failed to create client side decorations frame: {err}"); + self.csd_fails = true; + }, + } + } else if configure.decoration_mode == DecorationMode::Server { + // Drop the frame for server side decorations to save resources. + self.frame = None; + } + + let stateless = Self::is_stateless(&configure); + + let (mut new_size, constrain) = if let Some(frame) = self.frame.as_mut() { + // Configure the window states. + frame.update_state(configure.state); + + match configure.new_size { + (Some(width), Some(height)) => { + let (width, height) = frame.subtract_borders(width, height); + let width = width.map(|w| w.get()).unwrap_or(1); + let height = height.map(|h| h.get()).unwrap_or(1); + ((width, height).into(), false) + }, + (..) if stateless => (self.stateless_size, true), + _ => (self.size, true), + } + } else { + match configure.new_size { + (Some(width), Some(height)) => ((width.get(), height.get()).into(), false), + _ if stateless => (self.stateless_size, true), + _ => (self.size, true), + } + }; + + // Apply configure bounds only when compositor let the user decide what size to pick. + if constrain { + let bounds = self.inner_size_bounds(&configure); + new_size.width = + bounds.0.map(|bound_w| new_size.width.min(bound_w.get())).unwrap_or(new_size.width); + new_size.height = bounds + .1 + .map(|bound_h| new_size.height.min(bound_h.get())) + .unwrap_or(new_size.height); + } + + // Apply size increments. + // + // We conditionally apply increments to avoid conflicts with the compositor's layout rules: + // 1. If the window is floating (constrain == true), we snap to increments to ensure the + // app's grid alignment. + // 2. If the user is interactively resizing (is_resizing), we snap the size to provide + // feedback. + // + // However, we MUST NOT snap if the compositor enforces a specific size (constrain == false, + // or states like Maximized/Tiled). Snapping in these cases (e.g. corner tiling) would + // shrink the window below the allocated area, creating visible gaps between valid + // windows or screen edges. + if (constrain || configure.is_resizing()) + && !configure.is_maximized() + && !configure.is_fullscreen() + && !configure.is_tiled() + { + if let Some(increments) = self.resize_increments { + // We use min size as a base size for the increments, similar to how X11 does it. + // + // This ensures that we can always reach the min size and the increments are + // calculated from it. + let (delta_width, delta_height) = ( + new_size.width.saturating_sub(self.min_inner_size.width), + new_size.height.saturating_sub(self.min_inner_size.height), + ); + + let width = + self.min_inner_size.width + (delta_width / increments.width) * increments.width; + let height = self.min_inner_size.height + + (delta_height / increments.height) * increments.height; + + new_size = (width, height).into(); + } + } + + let new_state = configure.state; + let old_state = self.last_configure.as_ref().map(|configure| configure.state); + + let state_change_requires_resize = old_state + .map(|old_state| { + !old_state + .symmetric_difference(new_state) + .difference(XdgWindowState::ACTIVATED | XdgWindowState::SUSPENDED) + .is_empty() + }) + // NOTE: `None` is present for the initial configure, thus we must always resize. + .unwrap_or(true); + + // NOTE: Set the configure before doing a resize, since we query it during it. + self.last_configure = Some(configure); + + if state_change_requires_resize || new_size != self.inner_size() { + self.resize(new_size); + true + } else { + false + } + } + + /// Compute the bounds for the inner size of the surface. + fn inner_size_bounds( + &self, + configure: &WindowConfigure, + ) -> (Option, Option) { + let configure_bounds = match configure.suggested_bounds { + Some((width, height)) => (NonZeroU32::new(width), NonZeroU32::new(height)), + None => (None, None), + }; + + if let Some(frame) = self.frame.as_ref() { + let (width, height) = frame.subtract_borders( + configure_bounds.0.unwrap_or(NonZeroU32::new(1).unwrap()), + configure_bounds.1.unwrap_or(NonZeroU32::new(1).unwrap()), + ); + (configure_bounds.0.and(width), configure_bounds.1.and(height)) + } else { + configure_bounds + } + } + + #[inline] + fn is_stateless(configure: &WindowConfigure) -> bool { + !(configure.is_maximized() || configure.is_fullscreen() || configure.is_tiled()) + } + + /// Start interacting drag resize. + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + let xdg_toplevel = self.window.xdg_toplevel(); + + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + xdg_toplevel.resize(seat, serial, direction.into()); + }); + + Ok(()) + } + + /// Start the window drag. + pub fn drag_window(&self) -> Result<(), ExternalError> { + let xdg_toplevel = self.window.xdg_toplevel(); + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + xdg_toplevel._move(seat, serial); + }); + + Ok(()) + } + + /// Tells whether the window should be closed. + #[allow(clippy::too_many_arguments)] + pub fn frame_click( + &mut self, + click: FrameClick, + pressed: bool, + seat: &WlSeat, + serial: u32, + timestamp: Duration, + window_id: WindowId, + updates: &mut Vec, + ) -> Option { + match self.frame.as_mut()?.on_click(timestamp, click, pressed)? { + FrameAction::Minimize => self.window.set_minimized(), + FrameAction::Maximize => self.window.set_maximized(), + FrameAction::UnMaximize => self.window.unset_maximized(), + FrameAction::Close => WinitState::queue_close(updates, window_id), + FrameAction::Move => self.has_pending_move = Some(serial), + FrameAction::Resize(edge) => { + let edge = match edge { + ResizeEdge::None => XdgResizeEdge::None, + ResizeEdge::Top => XdgResizeEdge::Top, + ResizeEdge::Bottom => XdgResizeEdge::Bottom, + ResizeEdge::Left => XdgResizeEdge::Left, + ResizeEdge::TopLeft => XdgResizeEdge::TopLeft, + ResizeEdge::BottomLeft => XdgResizeEdge::BottomLeft, + ResizeEdge::Right => XdgResizeEdge::Right, + ResizeEdge::TopRight => XdgResizeEdge::TopRight, + ResizeEdge::BottomRight => XdgResizeEdge::BottomRight, + _ => return None, + }; + self.window.resize(seat, serial, edge); + }, + FrameAction::ShowMenu(x, y) => self.window.show_window_menu(seat, serial, (x, y)), + _ => (), + }; + + Some(false) + } + + pub fn frame_point_left(&mut self) { + if let Some(frame) = self.frame.as_mut() { + frame.click_point_left(); + } + } + + // Move the point over decorations. + pub fn frame_point_moved( + &mut self, + seat: &WlSeat, + surface: &WlSurface, + timestamp: Duration, + x: f64, + y: f64, + ) -> Option { + // Take the serial if we had any, so it doesn't stick around. + let serial = self.has_pending_move.take(); + + if let Some(frame) = self.frame.as_mut() { + let cursor = frame.click_point_moved(timestamp, &surface.id(), x, y); + // If we have a cursor change, that means that cursor is over the decorations, + // so try to apply move. + if let Some(serial) = cursor.is_some().then_some(serial).flatten() { + self.window.move_(seat, serial); + None + } else { + cursor + } + } else { + None + } + } + + /// Get the stored resizable state. + #[inline] + pub fn resizable(&self) -> bool { + self.resizable + } + + /// Set the resizable state on the window. + /// + /// Returns `true` when the state was applied. + #[inline] + pub fn set_resizable(&mut self, resizable: bool) -> bool { + if self.resizable == resizable { + return false; + } + + self.resizable = resizable; + if resizable { + // Restore min/max sizes of the window. + self.reload_min_max_hints(); + } else { + self.set_min_inner_size(Some(self.size)); + self.set_max_inner_size(Some(self.size)); + } + + // Reload the state on the frame as well. + if let Some(frame) = self.frame.as_mut() { + frame.set_resizable(resizable); + } + + true + } + + /// Whether the window is focused by any seat. + #[inline] + pub fn has_focus(&self) -> bool { + !self.seat_focus.is_empty() + } + + /// Whether the IME is allowed. + #[inline] + pub fn ime_allowed(&self) -> bool { + self.ime_allowed + } + + /// Get the size of the window. + #[inline] + pub fn inner_size(&self) -> LogicalSize { + self.size + } + + /// Whether the window received initial configure event from the compositor. + #[inline] + pub fn is_configured(&self) -> bool { + self.last_configure.is_some() + } + + #[inline] + pub fn is_decorated(&mut self) -> bool { + let csd = self + .last_configure + .as_ref() + .map(|configure| configure.decoration_mode == DecorationMode::Client) + .unwrap_or(false); + if let Some(frame) = csd.then_some(self.frame.as_ref()).flatten() { + !frame.is_hidden() + } else { + // Server side decorations. + true + } + } + + /// Get the outer size of the window. + #[inline] + pub fn outer_size(&self) -> LogicalSize { + self.frame + .as_ref() + .map(|frame| frame.add_borders(self.size.width, self.size.height).into()) + .unwrap_or(self.size) + } + + /// Register pointer on the top-level. + pub fn pointer_entered(&mut self, added: Weak>) { + self.pointers.push(added); + self.reload_cursor_style(); + + let mode = self.cursor_grab_mode.user_grab_mode; + let _ = self.set_cursor_grab_inner(mode); + } + + /// Pointer has left the top-level. + pub fn pointer_left(&mut self, removed: Weak>) { + let mut new_pointers = Vec::new(); + for pointer in self.pointers.drain(..) { + if let Some(pointer) = pointer.upgrade() { + if pointer.pointer() != removed.upgrade().unwrap().pointer() { + new_pointers.push(Arc::downgrade(&pointer)); + } + } + } + + self.pointers = new_pointers; + } + + /// Refresh the decorations frame if it's present returning whether the client should redraw. + pub fn refresh_frame(&mut self) -> bool { + if let Some(frame) = self.frame.as_mut() { + if !frame.is_hidden() && frame.is_dirty() { + return frame.draw(); + } + } + + false + } + + /// Reload the cursor style on the given window. + pub fn reload_cursor_style(&mut self) { + if self.cursor_visible { + match &self.selected_cursor { + SelectedCursor::Named(icon) => self.set_cursor(*icon), + SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor), + } + } else { + self.set_cursor_visible(self.cursor_visible); + } + } + + /// Reissue the transparency hint to the compositor. + pub fn reload_transparency_hint(&self) { + let surface = self.window.wl_surface(); + + if self.transparent { + surface.set_opaque_region(None); + } else if let Ok(region) = Region::new(&*self.compositor) { + region.add(0, 0, i32::MAX, i32::MAX); + surface.set_opaque_region(Some(region.wl_region())); + } else { + warn!("Failed to mark window opaque."); + } + } + + /// Try to resize the window when the user can do so. + pub fn request_inner_size(&mut self, inner_size: Size) -> PhysicalSize { + if self.last_configure.as_ref().map(Self::is_stateless).unwrap_or(true) { + self.resize(inner_size.to_logical(self.scale_factor())) + } + + logical_to_physical_rounded(self.inner_size(), self.scale_factor()) + } + + /// Resize the window to the new inner size. + fn resize(&mut self, inner_size: LogicalSize) { + self.size = inner_size; + + // Update the stateless size. + if Some(true) == self.last_configure.as_ref().map(Self::is_stateless) { + self.stateless_size = inner_size; + } + + // Update the inner frame. + let ((x, y), outer_size) = if let Some(frame) = self.frame.as_mut() { + // Resize only visible frame. + if !frame.is_hidden() { + frame.resize( + NonZeroU32::new(self.size.width).unwrap(), + NonZeroU32::new(self.size.height).unwrap(), + ); + } + + (frame.location(), frame.add_borders(self.size.width, self.size.height).into()) + } else { + ((0, 0), self.size) + }; + + // Reload the hint. + self.reload_transparency_hint(); + + // Set the window geometry. + self.window.xdg_surface().set_window_geometry( + x, + y, + outer_size.width as i32, + outer_size.height as i32, + ); + + // Update the target viewport, this is used if and only if fractional scaling is in use. + if let Some(viewport) = self.viewport.as_ref() { + // Set inner size without the borders. + viewport.set_destination(self.size.width as _, self.size.height as _); + } + } + + /// Get the scale factor of the window. + #[inline] + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } + + /// Set the cursor icon. + pub fn set_cursor(&mut self, cursor_icon: CursorIcon) { + self.selected_cursor = SelectedCursor::Named(cursor_icon); + + if !self.cursor_visible { + return; + } + + self.apply_on_pointer(|pointer, _| { + if pointer.set_cursor(&self.connection, cursor_icon).is_err() { + warn!("Failed to set cursor to {:?}", cursor_icon); + } + }) + } + + /// Set the custom cursor icon. + pub(crate) fn set_custom_cursor(&mut self, cursor: RootCustomCursor) { + let cursor = match cursor { + RootCustomCursor { inner: PlatformCustomCursor::Wayland(cursor) } => cursor.0, + #[cfg(x11_platform)] + RootCustomCursor { inner: PlatformCustomCursor::X(_) } => { + tracing::error!("passed a X11 cursor to Wayland backend"); + return; + }, + }; + + let cursor = { + let mut pool = self.custom_cursor_pool.lock().unwrap(); + CustomCursor::new(&mut pool, &cursor) + }; + + if self.cursor_visible { + self.apply_custom_cursor(&cursor); + } + + self.selected_cursor = SelectedCursor::Custom(cursor); + } + + /// Set the resize increments of the window. + pub fn set_resize_increments(&mut self, increments: Option>) { + self.resize_increments = increments; + // NOTE: We don't update the window size here, because it will be done on the next resize + // or configure event. + } + + /// Get the resize increments of the window. + pub fn resize_increments(&self) -> Option> { + self.resize_increments + } + + fn apply_custom_cursor(&self, cursor: &CustomCursor) { + self.apply_on_pointer(|pointer, data| { + let surface = pointer.surface(); + + let scale = if let Some(viewport) = data.viewport() { + let scale = self.scale_factor(); + let size = PhysicalSize::new(cursor.w, cursor.h).to_logical(scale); + viewport.set_destination(size.width, size.height); + scale + } else { + let scale = surface.data::().unwrap().surface_data().scale_factor(); + surface.set_buffer_scale(scale); + scale as f64 + }; + + surface.attach(Some(cursor.buffer.wl_buffer()), 0, 0); + if surface.version() >= 4 { + surface.damage_buffer(0, 0, cursor.w, cursor.h); + } else { + let size = PhysicalSize::new(cursor.w, cursor.h).to_logical(scale); + surface.damage(0, 0, size.width, size.height); + } + surface.commit(); + + let serial = pointer + .pointer() + .data::() + .and_then(|data| data.pointer_data().latest_enter_serial()) + .unwrap(); + + let hotspot = + PhysicalPosition::new(cursor.hotspot_x, cursor.hotspot_y).to_logical(scale); + pointer.pointer().set_cursor(serial, Some(surface), hotspot.x, hotspot.y); + }); + } + + /// Set maximum inner window size. + pub fn set_min_inner_size(&mut self, size: Option>) { + // Ensure that the window has the right minimum size. + let mut size = size.unwrap_or(MIN_WINDOW_SIZE); + size.width = size.width.max(MIN_WINDOW_SIZE.width); + size.height = size.height.max(MIN_WINDOW_SIZE.height); + + // Add the borders. + let size = self + .frame + .as_ref() + .map(|frame| frame.add_borders(size.width, size.height).into()) + .unwrap_or(size); + + self.min_inner_size = size; + self.window.set_min_size(Some(size.into())); + } + + /// Set maximum inner window size. + pub fn set_max_inner_size(&mut self, size: Option>) { + let size = size.map(|size| { + self.frame + .as_ref() + .map(|frame| frame.add_borders(size.width, size.height).into()) + .unwrap_or(size) + }); + + self.max_inner_size = size; + self.window.set_max_size(size.map(Into::into)); + } + + /// Set the CSD theme. + pub fn set_theme(&mut self, theme: Option) { + self.theme = theme; + #[cfg(feature = "sctk-adwaita")] + if let Some(frame) = self.frame.as_mut() { + frame.set_config(into_sctk_adwaita_config(theme)) + } + } + + /// The current theme for CSD decorations. + #[inline] + pub fn theme(&self) -> Option { + self.theme + } + + /// Set the cursor grabbing state on the top-level. + pub fn set_cursor_grab(&mut self, mode: CursorGrabMode) -> Result<(), ExternalError> { + if self.cursor_grab_mode.user_grab_mode == mode { + return Ok(()); + } + + self.set_cursor_grab_inner(mode)?; + // Update user grab on success. + self.cursor_grab_mode.user_grab_mode = mode; + Ok(()) + } + + /// Reload the hints for minimum and maximum sizes. + pub fn reload_min_max_hints(&mut self) { + self.set_min_inner_size(Some(self.min_inner_size)); + self.set_max_inner_size(self.max_inner_size); + } + + /// Set the grabbing state on the surface. + fn set_cursor_grab_inner(&mut self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let pointer_constraints = match self.pointer_constraints.as_ref() { + Some(pointer_constraints) => pointer_constraints, + None if mode == CursorGrabMode::None => return Ok(()), + None => return Err(ExternalError::NotSupported(NotSupportedError::new())), + }; + + let mut unset_old = false; + match self.cursor_grab_mode.current_grab_mode { + CursorGrabMode::None => unset_old = true, + CursorGrabMode::Confined => self.apply_on_pointer(|_, data| { + data.unconfine_pointer(); + unset_old = true; + }), + CursorGrabMode::Locked => { + self.apply_on_pointer(|_, data| { + data.unlock_pointer(); + unset_old = true; + }); + }, + } + + // In case we haven't unset the old mode, it means that we don't have a cursor above + // the window, thus just wait for it to re-appear. + if !unset_old { + return Ok(()); + } + + let mut set_mode = false; + let surface = self.window.wl_surface(); + match mode { + CursorGrabMode::Locked => self.apply_on_pointer(|pointer, data| { + let pointer = pointer.pointer(); + data.lock_pointer(pointer_constraints, surface, pointer, &self.queue_handle); + set_mode = true; + }), + CursorGrabMode::Confined => self.apply_on_pointer(|pointer, data| { + let pointer = pointer.pointer(); + data.confine_pointer(pointer_constraints, surface, pointer, &self.queue_handle); + set_mode = true; + }), + CursorGrabMode::None => { + // Current lock/confine was already removed. + set_mode = true; + }, + } + + // Replace the current grab mode after we've ensure that it got updated. + if set_mode { + self.cursor_grab_mode.current_grab_mode = mode; + } + + Ok(()) + } + + pub fn show_window_menu(&self, position: LogicalPosition) { + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + self.window.show_window_menu(seat, serial, position.into()); + }); + } + + /// Set the position of the cursor. + pub fn set_cursor_position(&self, position: LogicalPosition) -> Result<(), ExternalError> { + if self.pointer_constraints.is_none() { + return Err(ExternalError::NotSupported(NotSupportedError::new())); + } + + // Position can be set only for locked cursor. + if self.cursor_grab_mode.current_grab_mode != CursorGrabMode::Locked { + return Err(ExternalError::Os(os_error!(crate::platform_impl::OsError::Misc( + "cursor position can be set only for locked cursor." + )))); + } + + self.apply_on_pointer(|_, data| { + data.set_locked_cursor_position(position.x, position.y); + }); + + Ok(()) + } + + /// Set the visibility state of the cursor. + pub fn set_cursor_visible(&mut self, cursor_visible: bool) { + self.cursor_visible = cursor_visible; + + if self.cursor_visible { + match &self.selected_cursor { + SelectedCursor::Named(icon) => self.set_cursor(*icon), + SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor), + } + } else { + for pointer in self.pointers.iter().filter_map(|pointer| pointer.upgrade()) { + let latest_enter_serial = pointer.pointer().winit_data().latest_enter_serial(); + + pointer.pointer().set_cursor(latest_enter_serial, None, 0, 0); + } + } + } + + /// Whether show or hide client side decorations. + #[inline] + pub fn set_decorate(&mut self, decorate: bool) { + if decorate == self.decorate { + return; + } + + self.decorate = decorate; + + match self.last_configure.as_ref().map(|configure| configure.decoration_mode) { + Some(DecorationMode::Server) if !self.decorate => { + // To disable decorations we should request client and hide the frame. + self.window.request_decoration_mode(Some(DecorationMode::Client)) + }, + _ if self.decorate => self.window.request_decoration_mode(Some(DecorationMode::Server)), + _ => (), + } + + if let Some(frame) = self.frame.as_mut() { + frame.set_hidden(!decorate); + // Force the resize. + self.resize(self.size); + } + } + + /// Add seat focus for the window. + #[inline] + pub fn add_seat_focus(&mut self, seat: ObjectId) { + self.seat_focus.insert(seat); + } + + /// Remove seat focus from the window. + #[inline] + pub fn remove_seat_focus(&mut self, seat: &ObjectId) { + self.seat_focus.remove(seat); + } + + /// Returns `true` if the requested state was applied. + pub fn set_ime_allowed(&mut self, allowed: bool) -> bool { + self.ime_allowed = allowed; + + let mut applied = false; + for text_input in &self.text_inputs { + applied = true; + if allowed { + text_input.enable(); + text_input.set_content_type_by_purpose(self.ime_purpose); + } else { + text_input.disable(); + } + text_input.commit(); + } + + applied + } + + /// Set the IME position. + pub fn set_ime_cursor_area(&self, position: LogicalPosition, size: LogicalSize) { + // FIXME: This won't fly unless user will have a way to request IME window per seat, since + // the ime windows will be overlapping, but winit doesn't expose API to specify for + // which seat we're setting IME position. + let (x, y) = (position.x as i32, position.y as i32); + let (width, height) = (size.width as i32, size.height as i32); + for text_input in self.text_inputs.iter() { + text_input.set_cursor_rectangle(x, y, width, height); + text_input.commit(); + } + } + + /// Set the IME purpose. + pub fn set_ime_purpose(&mut self, purpose: ImePurpose) { + self.ime_purpose = purpose; + + for text_input in &self.text_inputs { + text_input.set_content_type_by_purpose(purpose); + text_input.commit(); + } + } + + /// Get the IME purpose. + pub fn ime_purpose(&self) -> ImePurpose { + self.ime_purpose + } + + /// Set the scale factor for the given window. + #[inline] + pub fn set_scale_factor(&mut self, scale_factor: f64) { + self.scale_factor = scale_factor; + + // NOTE: When fractional scaling is not used update the buffer scale. + if self.fractional_scale.is_none() { + let _ = self.window.set_buffer_scale(self.scale_factor as _); + } + + if let Some(frame) = self.frame.as_mut() { + frame.set_scaling_factor(scale_factor); + } + } + + /// Make window background blurred + #[inline] + pub fn set_blur(&mut self, blurred: bool) { + if blurred && self.blur.is_none() { + if let Some(blur_manager) = self.blur_manager.as_ref() { + let blur = blur_manager.blur(self.window.wl_surface(), &self.queue_handle); + blur.commit(); + self.blur = Some(blur); + } else { + info!("Blur manager unavailable, unable to change blur") + } + } else if !blurred && self.blur.is_some() { + self.blur_manager.as_ref().unwrap().unset(self.window.wl_surface()); + self.blur.take().unwrap().release(); + } + } + + /// Set the window title to a new value. + /// + /// This will automatically truncate the title to something meaningful. + pub fn set_title(&mut self, mut title: String) { + // Truncate the title to at most 1024 bytes, so that it does not blow up the protocol + // messages + if title.len() > 1024 { + let mut new_len = 1024; + while !title.is_char_boundary(new_len) { + new_len -= 1; + } + title.truncate(new_len); + } + + // Update the CSD title. + if let Some(frame) = self.frame.as_mut() { + frame.set_title(&title); + } + + self.window.set_title(&title); + self.title = title; + } + + /// Mark the window as transparent. + #[inline] + pub fn set_transparent(&mut self, transparent: bool) { + self.transparent = transparent; + self.reload_transparency_hint(); + } + + /// Register text input on the top-level. + #[inline] + pub fn text_input_entered(&mut self, text_input: &ZwpTextInputV3) { + if !self.text_inputs.iter().any(|t| t == text_input) { + self.text_inputs.push(text_input.clone()); + } + } + + /// The text input left the top-level. + #[inline] + pub fn text_input_left(&mut self, text_input: &ZwpTextInputV3) { + if let Some(position) = self.text_inputs.iter().position(|t| t == text_input) { + self.text_inputs.remove(position); + } + } + + /// Get the cached title. + #[inline] + pub fn title(&self) -> &str { + &self.title + } +} + +impl Drop for WindowState { + fn drop(&mut self) { + if let Some(blur) = self.blur.take() { + blur.release(); + } + + if let Some(fs) = self.fractional_scale.take() { + fs.destroy(); + } + + if let Some(viewport) = self.viewport.take() { + viewport.destroy(); + } + + // NOTE: the wl_surface used by the window is being cleaned up when + // dropping SCTK `Window`. + } +} + +/// The state of the cursor grabs. +#[derive(Clone, Copy)] +struct GrabState { + /// The grab mode requested by the user. + user_grab_mode: CursorGrabMode, + + /// The current grab mode. + current_grab_mode: CursorGrabMode, +} + +impl GrabState { + fn new() -> Self { + Self { user_grab_mode: CursorGrabMode::None, current_grab_mode: CursorGrabMode::None } + } +} + +/// The state of the frame callback. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameCallbackState { + /// No frame callback was requested. + #[default] + None, + /// The frame callback was requested, but not yet arrived, the redraw events are throttled. + Requested, + /// The callback was marked as done, and user could receive redraw requested + Received, +} + +impl From for XdgResizeEdge { + fn from(value: ResizeDirection) -> Self { + match value { + ResizeDirection::North => XdgResizeEdge::Top, + ResizeDirection::West => XdgResizeEdge::Left, + ResizeDirection::NorthWest => XdgResizeEdge::TopLeft, + ResizeDirection::NorthEast => XdgResizeEdge::TopRight, + ResizeDirection::East => XdgResizeEdge::Right, + ResizeDirection::SouthWest => XdgResizeEdge::BottomLeft, + ResizeDirection::SouthEast => XdgResizeEdge::BottomRight, + ResizeDirection::South => XdgResizeEdge::Bottom, + } + } +} + +// NOTE: Rust doesn't allow `From>`. +#[cfg(feature = "sctk-adwaita")] +fn into_sctk_adwaita_config(theme: Option) -> sctk_adwaita::FrameConfig { + match theme { + Some(Theme::Light) => sctk_adwaita::FrameConfig::light(), + Some(Theme::Dark) => sctk_adwaita::FrameConfig::dark(), + None => sctk_adwaita::FrameConfig::auto(), + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/activation.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/activation.rs new file mode 100644 index 0000000..8f4aa79 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/activation.rs @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! X11 activation handling. +//! +//! X11 has a "startup notification" specification similar to Wayland's, see this URL: +//! + +use super::atoms::*; +use super::{VoidCookie, X11Error, XConnection}; + +use std::ffi::CString; +use std::fmt::Write; + +use x11rb::protocol::xproto::{self, ConnectionExt as _}; + +impl XConnection { + /// "Request" a new activation token from the server. + pub(crate) fn request_activation_token(&self, window_title: &str) -> Result { + // The specification recommends the format "hostname+pid+"_TIME"+current time" + let uname = rustix::system::uname(); + let pid = rustix::process::getpid(); + let time = self.timestamp(); + + let activation_token = format!( + "{}{}_TIME{}", + uname.nodename().to_str().unwrap_or("winit"), + pid.as_raw_nonzero(), + time + ); + + // Set up the new startup notification. + let notification = { + let mut buffer = Vec::new(); + buffer.extend_from_slice(b"new: ID="); + quote_string(&activation_token, &mut buffer); + buffer.extend_from_slice(b" NAME="); + quote_string(window_title, &mut buffer); + buffer.extend_from_slice(b" SCREEN="); + push_display(&mut buffer, &self.default_screen_index()); + + CString::new(buffer) + .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))? + .into_bytes_with_nul() + }; + self.send_message(¬ification)?; + + Ok(activation_token) + } + + /// Finish launching a window with the given startup ID. + pub(crate) fn remove_activation_token( + &self, + window: xproto::Window, + startup_id: &str, + ) -> Result<(), X11Error> { + let atoms = self.atoms(); + + // Set the _NET_STARTUP_ID property on the window. + self.xcb_connection() + .change_property( + xproto::PropMode::REPLACE, + window, + atoms[_NET_STARTUP_ID], + xproto::AtomEnum::STRING, + 8, + startup_id.len().try_into().unwrap(), + startup_id.as_bytes(), + )? + .check()?; + + // Send the message indicating that the startup is over. + let message = { + const MESSAGE_ROOT: &str = "remove: ID="; + + let mut buffer = Vec::with_capacity( + MESSAGE_ROOT + .len() + .checked_add(startup_id.len()) + .and_then(|x| x.checked_add(1)) + .unwrap(), + ); + buffer.extend_from_slice(MESSAGE_ROOT.as_bytes()); + quote_string(startup_id, &mut buffer); + CString::new(buffer) + .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))? + .into_bytes_with_nul() + }; + + self.send_message(&message) + } + + /// Send a startup notification message to the window manager. + fn send_message(&self, message: &[u8]) -> Result<(), X11Error> { + let atoms = self.atoms(); + + // Create a new window to send the message over. + let screen = self.default_root(); + let window = xproto::WindowWrapper::create_window( + self.xcb_connection(), + screen.root_depth, + screen.root, + -100, + -100, + 1, + 1, + 0, + xproto::WindowClass::INPUT_OUTPUT, + screen.root_visual, + &xproto::CreateWindowAux::new().override_redirect(1).event_mask( + xproto::EventMask::STRUCTURE_NOTIFY | xproto::EventMask::PROPERTY_CHANGE, + ), + )?; + + // Serialize the messages in 20-byte chunks. + let mut message_type = atoms[_NET_STARTUP_INFO_BEGIN]; + message + .chunks(20) + .map(|chunk| { + let mut buffer = [0u8; 20]; + buffer[..chunk.len()].copy_from_slice(chunk); + let event = + xproto::ClientMessageEvent::new(8, window.window(), message_type, buffer); + + // Set the message type to the continuation atom for the next chunk. + message_type = atoms[_NET_STARTUP_INFO]; + + event + }) + .try_for_each(|event| { + // Send each event in order. + self.xcb_connection() + .send_event(false, screen.root, xproto::EventMask::PROPERTY_CHANGE, event) + .map(VoidCookie::ignore_error) + })?; + + Ok(()) + } +} + +/// Quote a literal string as per the startup notification specification. +fn quote_string(s: &str, target: &mut Vec) { + let total_len = s.len().checked_add(3).expect("quote string overflow"); + target.reserve(total_len); + + // Add the opening quote. + target.push(b'"'); + + // Iterate over the string split by literal quotes. + s.as_bytes().split(|&b| b == b'"').for_each(|part| { + // Add the part. + target.extend_from_slice(part); + + // Escape the quote. + target.push(b'\\'); + target.push(b'"'); + }); + + // Un-escape the last quote. + target.remove(target.len() - 2); +} + +/// Push a `Display` implementation to the buffer. +fn push_display(buffer: &mut Vec, display: &impl std::fmt::Display) { + struct Writer<'a> { + buffer: &'a mut Vec, + } + + impl std::fmt::Write for Writer<'_> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.buffer.extend_from_slice(s.as_bytes()); + Ok(()) + } + } + + write!(Writer { buffer }, "{display}").unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn properly_escapes_x11_messages() { + let assert_eq = |input: &str, output: &[u8]| { + let mut buf = vec![]; + quote_string(input, &mut buf); + assert_eq!(buf, output); + }; + + assert_eq("", b"\"\""); + assert_eq("foo", b"\"foo\""); + assert_eq("foo\"bar", b"\"foo\\\"bar\""); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/atoms.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/atoms.rs new file mode 100644 index 0000000..c6d96c8 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/atoms.rs @@ -0,0 +1,117 @@ +//! Collects every atom used by the platform implementation. + +use core::ops::Index; + +macro_rules! atom_manager { + ($($name:ident $(:$lit:literal)?),*) => { + x11rb::atom_manager! { + /// The atoms used by `winit` + pub Atoms: AtomsCookie { + $($name $(:$lit)?,)* + } + } + + /// Indices into the `Atoms` struct. + #[derive(Copy, Clone, Debug)] + #[allow(non_camel_case_types)] + pub enum AtomName { + $($name,)* + } + + impl AtomName { + pub(crate) fn atom_from( + self, + atoms: &Atoms + ) -> &x11rb::protocol::xproto::Atom { + match self { + $(AtomName::$name => &atoms.$name,)* + } + } + } + }; +} + +atom_manager! { + // General Use Atoms + CARD32, + UTF8_STRING, + WM_CHANGE_STATE, + WM_CLIENT_MACHINE, + WM_DELETE_WINDOW, + WM_PROTOCOLS, + WM_STATE, + XIM_SERVERS, + + // Assorted ICCCM Atoms + _NET_WM_ICON, + _NET_WM_MOVERESIZE, + _NET_WM_NAME, + _NET_WM_PID, + _NET_WM_PING, + _NET_WM_STATE, + _NET_WM_STATE_ABOVE, + _NET_WM_STATE_BELOW, + _NET_WM_STATE_FULLSCREEN, + _NET_WM_STATE_HIDDEN, + _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_MAXIMIZED_VERT, + _NET_WM_WINDOW_TYPE, + + // Activation atoms. + _NET_STARTUP_INFO_BEGIN, + _NET_STARTUP_INFO, + _NET_STARTUP_ID, + + // WM window types. + _NET_WM_WINDOW_TYPE_DESKTOP, + _NET_WM_WINDOW_TYPE_DOCK, + _NET_WM_WINDOW_TYPE_TOOLBAR, + _NET_WM_WINDOW_TYPE_MENU, + _NET_WM_WINDOW_TYPE_UTILITY, + _NET_WM_WINDOW_TYPE_SPLASH, + _NET_WM_WINDOW_TYPE_DIALOG, + _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + _NET_WM_WINDOW_TYPE_POPUP_MENU, + _NET_WM_WINDOW_TYPE_TOOLTIP, + _NET_WM_WINDOW_TYPE_NOTIFICATION, + _NET_WM_WINDOW_TYPE_COMBO, + _NET_WM_WINDOW_TYPE_DND, + _NET_WM_WINDOW_TYPE_NORMAL, + + // Drag-N-Drop Atoms + XdndAware, + XdndEnter, + XdndLeave, + XdndDrop, + XdndPosition, + XdndStatus, + XdndActionPrivate, + XdndSelection, + XdndFinished, + XdndTypeList, + TextUriList: b"text/uri-list", + None: b"None", + + // Miscellaneous Atoms + _GTK_THEME_VARIANT, + _MOTIF_WM_HINTS, + _NET_ACTIVE_WINDOW, + _NET_CLIENT_LIST, + _NET_FRAME_EXTENTS, + _NET_SUPPORTED, + _NET_SUPPORTING_WM_CHECK, + _XEMBED, + _XSETTINGS_SETTINGS +} + +impl Index for Atoms { + type Output = x11rb::protocol::xproto::Atom; + + fn index(&self, index: AtomName) -> &Self::Output { + index.atom_from(self) + } +} + +pub(crate) use AtomName::*; +// Make sure `None` is still defined. +pub(crate) use core::option::Option::None; diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/dnd.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/dnd.rs new file mode 100644 index 0000000..691e40a --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/dnd.rs @@ -0,0 +1,174 @@ +use std::io; +use std::os::raw::*; +use std::path::{Path, PathBuf}; +use std::str::Utf8Error; +use std::sync::Arc; + +use percent_encoding::percent_decode; +use x11rb::protocol::xproto::{self, ConnectionExt}; + +use super::atoms::AtomName::None as DndNone; +use super::atoms::*; +use super::{util, CookieResultExt, X11Error, XConnection}; + +#[derive(Debug, Clone, Copy)] +pub enum DndState { + Accepted, + Rejected, +} + +#[derive(Debug)] +pub enum DndDataParseError { + EmptyData, + InvalidUtf8(#[allow(dead_code)] Utf8Error), + HostnameSpecified(#[allow(dead_code)] String), + UnexpectedProtocol(#[allow(dead_code)] String), + UnresolvablePath(#[allow(dead_code)] io::Error), +} + +impl From for DndDataParseError { + fn from(e: Utf8Error) -> Self { + DndDataParseError::InvalidUtf8(e) + } +} + +impl From for DndDataParseError { + fn from(e: io::Error) -> Self { + DndDataParseError::UnresolvablePath(e) + } +} + +pub struct Dnd { + xconn: Arc, + // Populated by XdndEnter event handler + pub version: Option, + pub type_list: Option>, + // Populated by XdndPosition event handler + pub source_window: Option, + // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) + pub result: Option, DndDataParseError>>, +} + +impl Dnd { + pub fn new(xconn: Arc) -> Result { + Ok(Dnd { xconn, version: None, type_list: None, source_window: None, result: None }) + } + + pub fn reset(&mut self) { + self.version = None; + self.type_list = None; + self.source_window = None; + self.result = None; + } + + pub unsafe fn send_status( + &self, + this_window: xproto::Window, + target_window: xproto::Window, + state: DndState, + ) -> Result<(), X11Error> { + let atoms = self.xconn.atoms(); + let (accepted, action) = match state { + DndState::Accepted => (1, atoms[XdndActionPrivate]), + DndState::Rejected => (0, atoms[DndNone]), + }; + self.xconn + .send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [ + this_window, + accepted, + 0, + 0, + action as _, + ])? + .ignore_error(); + + Ok(()) + } + + pub unsafe fn send_finished( + &self, + this_window: xproto::Window, + target_window: xproto::Window, + state: DndState, + ) -> Result<(), X11Error> { + let atoms = self.xconn.atoms(); + let (accepted, action) = match state { + DndState::Accepted => (1, atoms[XdndActionPrivate]), + DndState::Rejected => (0, atoms[DndNone]), + }; + self.xconn + .send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [ + this_window, + accepted, + action as _, + 0, + 0, + ])? + .ignore_error(); + + Ok(()) + } + + pub unsafe fn get_type_list( + &self, + source_window: xproto::Window, + ) -> Result, util::GetPropertyError> { + let atoms = self.xconn.atoms(); + self.xconn.get_property( + source_window, + atoms[XdndTypeList], + xproto::Atom::from(xproto::AtomEnum::ATOM), + ) + } + + pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) { + let atoms = self.xconn.atoms(); + self.xconn + .xcb_connection() + .convert_selection( + window, + atoms[XdndSelection], + atoms[TextUriList], + atoms[XdndSelection], + time, + ) + .expect_then_ignore_error("Failed to send XdndSelection event") + } + + pub unsafe fn read_data( + &self, + window: xproto::Window, + ) -> Result, util::GetPropertyError> { + let atoms = self.xconn.atoms(); + self.xconn.get_property(window, atoms[XdndSelection], atoms[TextUriList]) + } + + pub fn parse_data(&self, data: &mut [c_uchar]) -> Result, DndDataParseError> { + if !data.is_empty() { + let mut path_list = Vec::new(); + let decoded = percent_decode(data).decode_utf8()?.into_owned(); + for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) { + // The format is specified as protocol://host/path + // However, it's typically simply protocol:///path + let path_str = if uri.starts_with("file://") { + let path_str = uri.replace("file://", ""); + if !path_str.starts_with('/') { + // A hostname is specified + // Supporting this case is beyond the scope of my mental health + return Err(DndDataParseError::HostnameSpecified(path_str)); + } + path_str + } else { + // Only the file protocol is supported + return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned())); + }; + + let path = Path::new(&path_str).canonicalize()?; + path_list.push(path); + } + Ok(path_list) + } else { + Err(DndDataParseError::EmptyData) + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/event_processor.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/event_processor.rs new file mode 100644 index 0000000..79f5c11 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/event_processor.rs @@ -0,0 +1,1883 @@ +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, VecDeque}; +use std::os::raw::{c_char, c_int, c_long, c_ulong}; +use std::slice; +use std::sync::{Arc, Mutex}; + +use x11_dl::xinput2::{ + self, XIDeviceEvent, XIEnterEvent, XIFocusInEvent, XIFocusOutEvent, XIHierarchyEvent, + XILeaveEvent, XIModifierState, XIRawEvent, +}; +use x11_dl::xlib::{ + self, Display as XDisplay, Window as XWindow, XAnyEvent, XClientMessageEvent, XConfigureEvent, + XDestroyWindowEvent, XEvent, XExposeEvent, XKeyEvent, XMapEvent, XPropertyEvent, + XReparentEvent, XSelectionEvent, XVisibilityEvent, XkbAnyEvent, XkbStateRec, +}; +use x11rb::protocol::xinput; +use x11rb::protocol::xkb::ID as XkbId; +use x11rb::protocol::xproto::{self, ConnectionExt as _, ModMask}; +use x11rb::x11_utils::{ExtensionInformation, Serialize}; +use xkbcommon_dl::xkb_mod_mask_t; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::event::{ + DeviceEvent, ElementState, Event, Ime, InnerSizeWriter, MouseButton, MouseScrollDelta, + RawKeyEvent, Touch, TouchPhase, WindowEvent, +}; +use crate::event_loop::ActiveEventLoop as RootAEL; +use crate::keyboard::ModifiersState; +use crate::platform_impl::common::xkb::{self, XkbState}; +use crate::platform_impl::platform::common::xkb::Context; +use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventReceiver, ImeRequest}; +use crate::platform_impl::platform::x11::ActiveEventLoop; +use crate::platform_impl::platform::ActiveEventLoop as PlatformActiveEventLoop; +use crate::platform_impl::x11::atoms::*; +use crate::platform_impl::x11::util::cookie::GenericEventCookie; +use crate::platform_impl::x11::{ + mkdid, mkwid, util, CookieResultExt, Device, DeviceId, DeviceInfo, Dnd, DndState, ImeReceiver, + ScrollOrientation, UnownedWindow, WindowId, +}; + +/// The maximum amount of X modifiers to replay. +pub const MAX_MOD_REPLAY_LEN: usize = 32; + +/// The X11 documentation states: "Keycodes lie in the inclusive range `[8, 255]`". +const KEYCODE_OFFSET: u8 = 8; + +pub struct EventProcessor { + pub dnd: Dnd, + pub ime_receiver: ImeReceiver, + pub ime_event_receiver: ImeEventReceiver, + pub randr_event_offset: u8, + pub devices: RefCell>, + pub xi2ext: ExtensionInformation, + pub xkbext: ExtensionInformation, + pub target: RootAEL, + pub xkb_context: Context, + // Number of touch events currently in progress + pub num_touch: u32, + // This is the last pressed key that is repeatable (if it hasn't been + // released). + // + // Used to detect key repeats. + pub held_key_press: Option, + pub first_touch: Option, + // Currently focused window belonging to this process + pub active_window: Option, + /// Latest modifiers we've sent for the user to trigger change in event. + pub modifiers: Cell, + // Track modifiers based on keycodes. NOTE: that serials generally don't work for tracking + // since they are not unique and could be duplicated in case of sequence of key events is + // delivered at near the same time. + pub xfiltered_modifiers: VecDeque, + pub xmodmap: util::ModifierKeymap, + pub is_composing: bool, +} + +impl EventProcessor { + pub fn process_event(&mut self, xev: &mut XEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + self.process_xevent(xev, &mut callback); + + let window_target = Self::window_target_mut(&mut self.target); + + // Handle IME requests. + while let Ok(request) = self.ime_receiver.try_recv() { + let ime = match window_target.ime.as_mut() { + Some(ime) => ime, + None => continue, + }; + let ime = ime.get_mut(); + match request { + ImeRequest::Position(window_id, x, y) => { + ime.send_xim_spot(window_id, x, y); + }, + ImeRequest::Allow(window_id, allowed) => { + ime.set_ime_allowed(window_id, allowed); + }, + } + } + + // Drain IME events. + while let Ok((window, event)) = self.ime_event_receiver.try_recv() { + let window_id = mkwid(window as xproto::Window); + let event = match event { + ImeEvent::Enabled => WindowEvent::Ime(Ime::Enabled), + ImeEvent::Start => { + self.is_composing = true; + WindowEvent::Ime(Ime::Preedit("".to_owned(), None)) + }, + ImeEvent::Update(text, position) if self.is_composing => { + WindowEvent::Ime(Ime::Preedit(text, Some((position, position)))) + }, + ImeEvent::End => { + self.is_composing = false; + // Issue empty preedit on `Done`. + WindowEvent::Ime(Ime::Preedit(String::new(), None)) + }, + ImeEvent::Disabled => { + self.is_composing = false; + WindowEvent::Ime(Ime::Disabled) + }, + _ => continue, + }; + + callback(&self.target, Event::WindowEvent { window_id, 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. + #[must_use] + fn filter_event(&mut self, xev: &mut XEvent) -> bool { + let wt = Self::window_target(&self.target); + unsafe { + (wt.xconn.xlib.XFilterEvent)(xev, { + let xev: &XAnyEvent = xev.as_ref(); + xev.window + }) == xlib::True + } + } + + fn process_xevent(&mut self, xev: &mut XEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let event_type = xev.get_type(); + + // If we have IME disabled, don't try to `filter_event`, since only IME can consume them + // and forward back. This is not desired for e.g. games since some IMEs may delay the input + // and game can toggle IME back when e.g. typing into some field where latency won't really + // matter. + let filtered = if event_type == xlib::KeyPress || event_type == xlib::KeyRelease { + let wt = Self::window_target(&self.target); + let ime = wt.ime.as_ref(); + let window = self.active_window.map(|window| window as XWindow); + let forward_to_ime = ime + .and_then(|ime| window.map(|window| ime.borrow().is_ime_allowed(window))) + .unwrap_or(false); + + let filtered = forward_to_ime && self.filter_event(xev); + if filtered { + let xev: &XKeyEvent = xev.as_ref(); + if self.xmodmap.is_modifier(xev.keycode as u8) { + // Don't grow the buffer past the `MAX_MOD_REPLAY_LEN`. This could happen + // when the modifiers are consumed entirely. + if self.xfiltered_modifiers.len() == MAX_MOD_REPLAY_LEN { + self.xfiltered_modifiers.pop_back(); + } + self.xfiltered_modifiers.push_front(xev.keycode as u8); + } + } + + filtered + } else { + self.filter_event(xev) + }; + + // Don't process event if it was filtered. + if filtered { + return; + } + + match event_type { + xlib::ClientMessage => self.client_message(xev.as_ref(), &mut callback), + xlib::SelectionNotify => self.selection_notify(xev.as_ref(), &mut callback), + xlib::ConfigureNotify => self.configure_notify(xev.as_ref(), &mut callback), + xlib::ReparentNotify => self.reparent_notify(xev.as_ref()), + xlib::MapNotify => self.map_notify(xev.as_ref(), &mut callback), + xlib::DestroyNotify => self.destroy_notify(xev.as_ref(), &mut callback), + xlib::PropertyNotify => self.property_notify(xev.as_ref(), &mut callback), + xlib::VisibilityNotify => self.visibility_notify(xev.as_ref(), &mut callback), + xlib::Expose => self.expose(xev.as_ref(), &mut callback), + // Note that in compose/pre-edit sequences, we'll always receive KeyRelease events. + ty @ xlib::KeyPress | ty @ xlib::KeyRelease => { + let state = if ty == xlib::KeyPress { + ElementState::Pressed + } else { + ElementState::Released + }; + + self.xinput_key_input(xev.as_mut(), state, &mut callback); + }, + xlib::GenericEvent => { + let wt = Self::window_target(&self.target); + let xev: GenericEventCookie = + match GenericEventCookie::from_event(wt.xconn.clone(), *xev) { + Some(xev) if xev.extension() == self.xi2ext.major_opcode => xev, + _ => return, + }; + + let evtype = xev.evtype(); + + match evtype { + ty @ xinput2::XI_ButtonPress | ty @ xinput2::XI_ButtonRelease => { + let state = if ty == xinput2::XI_ButtonPress { + ElementState::Pressed + } else { + ElementState::Released + }; + + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_button_input(xev, state, &mut callback); + }, + xinput2::XI_Motion => { + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_mouse_motion(xev, &mut callback); + }, + xinput2::XI_Enter => { + let xev: &XIEnterEvent = unsafe { xev.as_event() }; + self.xinput2_mouse_enter(xev, &mut callback); + }, + xinput2::XI_Leave => { + let xev: &XILeaveEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_mouse_left(xev, &mut callback); + }, + xinput2::XI_FocusIn => { + let xev: &XIFocusInEvent = unsafe { xev.as_event() }; + self.xinput2_focused(xev, &mut callback); + }, + xinput2::XI_FocusOut => { + let xev: &XIFocusOutEvent = unsafe { xev.as_event() }; + self.xinput2_unfocused(xev, &mut callback); + }, + xinput2::XI_TouchBegin | xinput2::XI_TouchUpdate | xinput2::XI_TouchEnd => { + let phase = match evtype { + xinput2::XI_TouchBegin => TouchPhase::Started, + xinput2::XI_TouchUpdate => TouchPhase::Moved, + xinput2::XI_TouchEnd => TouchPhase::Ended, + _ => unreachable!(), + }; + + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.xinput2_touch(xev, phase, &mut callback); + }, + xinput2::XI_RawButtonPress | xinput2::XI_RawButtonRelease => { + let state = match evtype { + xinput2::XI_RawButtonPress => ElementState::Pressed, + xinput2::XI_RawButtonRelease => ElementState::Released, + _ => unreachable!(), + }; + + let xev: &XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_button_input(xev, state, &mut callback); + }, + xinput2::XI_RawMotion => { + let xev: &XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_mouse_motion(xev, &mut callback); + }, + xinput2::XI_RawKeyPress | xinput2::XI_RawKeyRelease => { + let state = match evtype { + xinput2::XI_RawKeyPress => ElementState::Pressed, + xinput2::XI_RawKeyRelease => ElementState::Released, + _ => unreachable!(), + }; + + let xev: &xinput2::XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_key_input(xev, state, &mut callback); + }, + + xinput2::XI_HierarchyChanged => { + let xev: &XIHierarchyEvent = unsafe { xev.as_event() }; + self.xinput2_hierarchy_changed(xev, &mut callback); + }, + _ => {}, + } + }, + _ => { + if event_type == self.xkbext.first_event as _ { + let xev: &XkbAnyEvent = unsafe { &*(xev as *const _ as *const XkbAnyEvent) }; + self.xkb_event(xev, &mut callback); + } + if event_type == self.randr_event_offset as c_int { + self.process_dpi_change(&mut callback); + } + }, + } + } + + pub fn poll(&self) -> bool { + let window_target = Self::window_target(&self.target); + let result = unsafe { (window_target.xconn.xlib.XPending)(window_target.xconn.display) }; + + result != 0 + } + + pub unsafe fn poll_one_event(&mut self, event_ptr: *mut XEvent) -> bool { + let window_target = Self::window_target(&self.target); + // This function is used to poll and remove a single event + // from the Xlib event queue in a non-blocking, atomic way. + // XCheckIfEvent is non-blocking and removes events from queue. + // XNextEvent can't be used because it blocks while holding the + // global Xlib mutex. + // XPeekEvent does not remove events from the queue. + unsafe extern "C" fn predicate( + _display: *mut XDisplay, + _event: *mut XEvent, + _arg: *mut c_char, + ) -> c_int { + // This predicate always returns "true" (1) to accept all events + 1 + } + + let result = unsafe { + (window_target.xconn.xlib.XCheckIfEvent)( + window_target.xconn.display, + event_ptr, + Some(predicate), + std::ptr::null_mut(), + ) + }; + + result != 0 + } + + pub fn init_device(&self, device: xinput::DeviceId) { + let window_target = Self::window_target(&self.target); + let mut devices = self.devices.borrow_mut(); + if let Some(info) = DeviceInfo::get(&window_target.xconn, device as _) { + for info in info.iter() { + devices.insert(DeviceId(info.deviceid as _), Device::new(info)); + } + } + } + + pub fn with_window(&self, window_id: xproto::Window, callback: F) -> Option + where + F: Fn(&Arc) -> Ret, + { + let mut deleted = false; + let window_id = WindowId(window_id as _); + let window_target = Self::window_target(&self.target); + let result = window_target + .windows + .borrow() + .get(&window_id) + .and_then(|window| { + let arc = window.upgrade(); + deleted = arc.is_none(); + arc + }) + .map(|window| callback(&window)); + + if deleted { + // Garbage collection + window_target.windows.borrow_mut().remove(&window_id); + } + + result + } + + // NOTE: we avoid `self` to not borrow the entire `self` as not mut. + /// Get the platform window target. + pub fn window_target(window_target: &RootAEL) -> &ActiveEventLoop { + match &window_target.p { + PlatformActiveEventLoop::X(target) => target, + #[cfg(wayland_platform)] + _ => unreachable!(), + } + } + + /// Get the platform window target. + pub fn window_target_mut(window_target: &mut RootAEL) -> &mut ActiveEventLoop { + match &mut window_target.p { + PlatformActiveEventLoop::X(target) => target, + #[cfg(wayland_platform)] + _ => unreachable!(), + } + } + + fn client_message(&mut self, xev: &XClientMessageEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.xconn.atoms(); + + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + if xev.data.get_long(0) as xproto::Atom == wt.wm_delete_window { + let event = Event::WindowEvent { window_id, event: WindowEvent::CloseRequested }; + callback(&self.target, event); + return; + } + + if xev.data.get_long(0) as xproto::Atom == wt.net_wm_ping { + let client_msg = xproto::ClientMessageEvent { + response_type: xproto::CLIENT_MESSAGE_EVENT, + format: xev.format as _, + sequence: xev.serial as _, + window: wt.root, + type_: xev.message_type as _, + data: xproto::ClientMessageData::from({ + let [a, b, c, d, e]: [c_long; 5] = xev.data.as_longs().try_into().unwrap(); + [a as u32, b as u32, c as u32, d as u32, e as u32] + }), + }; + + wt.xconn + .xcb_connection() + .send_event( + false, + wt.root, + xproto::EventMask::SUBSTRUCTURE_NOTIFY + | xproto::EventMask::SUBSTRUCTURE_REDIRECT, + client_msg.serialize(), + ) + .expect_then_ignore_error("Failed to send `ClientMessage` event."); + return; + } + + if xev.message_type == atoms[XdndEnter] as c_ulong { + let source_window = xev.data.get_long(0) as xproto::Window; + let flags = xev.data.get_long(1); + let version = flags >> 24; + self.dnd.version = Some(version); + let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1; + if !has_more_types { + let type_list = vec![ + xev.data.get_long(2) as xproto::Atom, + xev.data.get_long(3) as xproto::Atom, + xev.data.get_long(4) as xproto::Atom, + ]; + self.dnd.type_list = Some(type_list); + } else if let Ok(more_types) = unsafe { self.dnd.get_type_list(source_window) } { + self.dnd.type_list = Some(more_types); + } + return; + } + + if xev.message_type == atoms[XdndPosition] as c_ulong { + // This event occurs every time the mouse moves while a file's being dragged + // over our window. We emit HoveredFile in response; while the macOS backend + // does that upon a drag entering, XDND doesn't have access to the actual drop + // data until this event. For parity with other platforms, we only emit + // `HoveredFile` the first time, though if winit's API is later extended to + // supply position updates with `HoveredFile` or another event, implementing + // that here would be trivial. + + let source_window = xev.data.get_long(0) as xproto::Window; + + // Equivalent to `(x << shift) | y` + // where `shift = mem::size_of::() * 8` + // Note that coordinates are in "desktop space", not "window space" + // (in X11 parlance, they're root window coordinates) + // let packed_coordinates = xev.data.get_long(2); + // let shift = mem::size_of::() * 8; + // let x = packed_coordinates >> shift; + // let y = packed_coordinates & !(x << shift); + + // By our own state flow, `version` should never be `None` at this point. + let version = self.dnd.version.unwrap_or(5); + + // Action is specified in versions 2 and up, though we don't need it anyway. + // let action = xev.data.get_long(4); + + let accepted = if let Some(ref type_list) = self.dnd.type_list { + type_list.contains(&atoms[TextUriList]) + } else { + false + }; + + if !accepted { + unsafe { + self.dnd + .send_status(window, source_window, DndState::Rejected) + .expect("Failed to send `XdndStatus` message."); + } + self.dnd.reset(); + return; + } + + self.dnd.source_window = Some(source_window); + if self.dnd.result.is_none() { + let time = if version >= 1 { + xev.data.get_long(3) as xproto::Timestamp + } else { + // In version 0, time isn't specified + x11rb::CURRENT_TIME + }; + + // Log this timestamp. + wt.xconn.set_timestamp(time); + + // This results in the `SelectionNotify` event below + unsafe { + self.dnd.convert_selection(window, time); + } + } + + unsafe { + self.dnd + .send_status(window, source_window, DndState::Accepted) + .expect("Failed to send `XdndStatus` message."); + } + return; + } + + if xev.message_type == atoms[XdndDrop] as c_ulong { + let (source_window, state) = if let Some(source_window) = self.dnd.source_window { + if let Some(Ok(ref path_list)) = self.dnd.result { + for path in path_list { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::DroppedFile(path.clone()), + }; + callback(&self.target, event); + } + } + (source_window, DndState::Accepted) + } else { + // `source_window` won't be part of our DND state if we already rejected the drop in + // our `XdndPosition` handler. + let source_window = xev.data.get_long(0) as xproto::Window; + (source_window, DndState::Rejected) + }; + + unsafe { + self.dnd + .send_finished(window, source_window, state) + .expect("Failed to send `XdndFinished` message."); + } + + self.dnd.reset(); + return; + } + + if xev.message_type == atoms[XdndLeave] as c_ulong { + self.dnd.reset(); + let event = Event::WindowEvent { window_id, event: WindowEvent::HoveredFileCancelled }; + callback(&self.target, event); + } + } + + fn selection_notify(&mut self, xev: &XSelectionEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.xconn.atoms(); + + let window = xev.requestor as xproto::Window; + let window_id = mkwid(window); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if xev.property != atoms[XdndSelection] as c_ulong { + return; + } + + // This is where we receive data from drag and drop + self.dnd.result = None; + if let Ok(mut data) = unsafe { self.dnd.read_data(window) } { + let parse_result = self.dnd.parse_data(&mut data); + if let Ok(ref path_list) = parse_result { + for path in path_list { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::HoveredFile(path.clone()), + }; + callback(&self.target, event); + } + } + self.dnd.result = Some(parse_result); + } + } + + fn configure_notify(&self, xev: &XConfigureEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + let xwindow = xev.window as xproto::Window; + let window_id = mkwid(xwindow); + + let window = match self.with_window(xwindow, Arc::clone) { + Some(window) => window, + None => return, + }; + + // So apparently... + // `XSendEvent` (synthetic `ConfigureNotify`) -> position relative to root + // `XConfigureNotify` (real `ConfigureNotify`) -> position relative to parent + // https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.5 + // We don't want to send `Moved` when this is false, since then every `Resized` + // (whether the window moved or not) is accompanied by an extraneous `Moved` event + // that has a position relative to the parent window. + let is_synthetic = xev.send_event == xlib::True; + + // These are both in physical space. + let new_inner_size = (xev.width as u32, xev.height as u32); + let new_inner_position = (xev.x, xev.y); + + let (mut resized, moved) = { + let mut shared_state_lock = window.shared_state_lock(); + + let resized = util::maybe_change(&mut shared_state_lock.size, new_inner_size); + let moved = if is_synthetic { + util::maybe_change(&mut shared_state_lock.inner_position, new_inner_position) + } else { + // Detect when frame extents change. + // Since this isn't synthetic, as per the notes above, this position is relative to + // the parent window. + let rel_parent = new_inner_position; + if util::maybe_change(&mut shared_state_lock.inner_position_rel_parent, rel_parent) + { + // This ensures we process the next `Moved`. + shared_state_lock.inner_position = None; + // Extra insurance against stale frame extents. + shared_state_lock.frame_extents = None; + } + false + }; + (resized, moved) + }; + + let position = window.shared_state_lock().position; + + let new_outer_position = if let (Some(position), false) = (position, moved) { + position + } else { + let mut shared_state_lock = window.shared_state_lock(); + + // We need to convert client area position to window position. + let frame_extents = + shared_state_lock.frame_extents.as_ref().cloned().unwrap_or_else(|| { + let frame_extents = wt.xconn.get_frame_extents_heuristic(xwindow, wt.root); + shared_state_lock.frame_extents = Some(frame_extents.clone()); + frame_extents + }); + let outer = + frame_extents.inner_pos_to_outer(new_inner_position.0, new_inner_position.1); + shared_state_lock.position = Some(outer); + + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); + + if moved { + callback(&self.target, Event::WindowEvent { + window_id, + event: WindowEvent::Moved(outer.into()), + }); + } + outer + }; + + if is_synthetic { + let mut shared_state_lock = window.shared_state_lock(); + // If we don't use the existing adjusted value when available, then the user can screw + // up the resizing by dragging across monitors *without* dropping the + // window. + let (width, height) = + shared_state_lock.dpi_adjusted.unwrap_or((xev.width as u32, xev.height as u32)); + + let last_scale_factor = shared_state_lock.last_monitor.scale_factor; + let new_scale_factor = { + let window_rect = util::AaRect::new(new_outer_position, new_inner_size); + let monitor = wt + .xconn + .get_monitor_for_window(Some(window_rect)) + .expect("Failed to find monitor for window"); + + if monitor.is_dummy() { + // Avoid updating monitor using a dummy monitor handle + last_scale_factor + } else { + shared_state_lock.last_monitor = monitor.clone(); + monitor.scale_factor + } + }; + if last_scale_factor != new_scale_factor { + let (new_width, new_height) = window.adjust_for_dpi( + last_scale_factor, + new_scale_factor, + width, + height, + &shared_state_lock, + ); + + let old_inner_size = PhysicalSize::new(width, height); + let new_inner_size = PhysicalSize::new(new_width, new_height); + + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); + + let inner_size = Arc::new(Mutex::new(new_inner_size)); + callback(&self.target, Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor: new_scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&inner_size)), + }, + }); + + let new_inner_size = *inner_size.lock().unwrap(); + drop(inner_size); + + if new_inner_size != old_inner_size { + window.request_inner_size_physical(new_inner_size.width, new_inner_size.height); + window.shared_state_lock().dpi_adjusted = Some(new_inner_size.into()); + // if the DPI factor changed, force a resize event to ensure the logical + // size is computed with the right DPI factor + resized = true; + } + } + } + + // NOTE: Ensure that the lock is dropped before handling the resized and + // sending the event back to user. + let hittest = { + let mut shared_state_lock = window.shared_state_lock(); + let hittest = shared_state_lock.cursor_hittest; + + // This is a hack to ensure that the DPI adjusted resize is actually + // applied on all WMs. KWin doesn't need this, but Xfwm does. The hack + // should not be run on other WMs, since tiling WMs constrain the window + // size, making the resize fail. This would cause an endless stream of + // XResizeWindow requests, making Xorg, the winit client, and the WM + // consume 100% of CPU. + if let Some(adjusted_size) = shared_state_lock.dpi_adjusted { + if new_inner_size == adjusted_size || !util::wm_name_is_one_of(&["Xfwm4"]) { + // When this finally happens, the event will not be synthetic. + shared_state_lock.dpi_adjusted = None; + } else { + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); + window.request_inner_size_physical(adjusted_size.0, adjusted_size.1); + } + } + + hittest + }; + + // Reload hittest. + if hittest.unwrap_or(false) { + let _ = window.set_cursor_hittest(true); + } + + if resized { + callback(&self.target, Event::WindowEvent { + window_id, + event: WindowEvent::Resized(new_inner_size.into()), + }); + } + } + + /// This is generally a reliable way to detect when the window manager's been + /// replaced, though this event is only fired by reparenting window managers + /// (which is almost all of them). Failing to correctly update WM info doesn't + /// really have much impact, since on the WMs affected (xmonad, dwm, etc.) the only + /// effect is that we waste some time trying to query unsupported properties. + fn reparent_notify(&self, xev: &XReparentEvent) { + let wt = Self::window_target(&self.target); + + wt.xconn.update_cached_wm_info(wt.root); + + self.with_window(xev.window as xproto::Window, |window| { + window.invalidate_cached_frame_extents(); + }); + } + + fn map_notify(&self, xev: &XMapEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + // NOTE: Re-issue the focus state when mapping the window. + // + // The purpose of it is to deliver initial focused state of the newly created + // window, given that we can't rely on `CreateNotify`, due to it being not + // sent. + let focus = self.with_window(window, |window| window.has_focus()).unwrap_or_default(); + let event = Event::WindowEvent { window_id, event: WindowEvent::Focused(focus) }; + + callback(&self.target, event); + } + + fn destroy_notify(&self, xev: &XDestroyWindowEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + // In the event that the window's been destroyed without being dropped first, we + // cleanup again here. + wt.windows.borrow_mut().remove(&WindowId(window as _)); + + // Since all XIM stuff needs to happen from the same thread, we destroy the input + // context here instead of when dropping the window. + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut() + .remove_context(window as XWindow) + .expect("Failed to destroy input context"); + } + + callback(&self.target, Event::WindowEvent { window_id, event: WindowEvent::Destroyed }); + } + + fn property_notify(&mut self, xev: &XPropertyEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.x_connection().atoms(); + let atom = xev.atom as xproto::Atom; + + if atom == xproto::Atom::from(xproto::AtomEnum::RESOURCE_MANAGER) + || atom == atoms[_XSETTINGS_SETTINGS] + { + self.process_dpi_change(&mut callback); + } + } + + fn visibility_notify(&self, xev: &XVisibilityEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let xwindow = xev.window as xproto::Window; + + let event = Event::WindowEvent { + window_id: mkwid(xwindow), + event: WindowEvent::Occluded(xev.state == xlib::VisibilityFullyObscured), + }; + callback(&self.target, event); + + self.with_window(xwindow, |window| { + window.visibility_notify(); + }); + } + + fn expose(&self, xev: &XExposeEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + // Multiple Expose events may be received for subareas of a window. + // We issue `RedrawRequested` only for the last event of such a series. + if xev.count == 0 { + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + let event = Event::WindowEvent { window_id, event: WindowEvent::RedrawRequested }; + + callback(&self.target, event); + } + } + + fn xinput_key_input( + &mut self, + xev: &mut XKeyEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let window = match self.active_window { + Some(window) => window, + None => return, + }; + + let window_id = mkwid(window); + let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD); + + let keycode = xev.keycode as _; + + // Update state to track key repeats and determine whether this key was a repeat. + // + // Note, when a key is held before focusing on this window the first + // (non-synthetic) event will not be flagged as a repeat (also note that the + // synthetic press event that is generated before this when the window gains focus + // will also not be flagged as a repeat). + // + // Only keys that can repeat should change the held_key_press state since a + // continuously held repeatable key may continue repeating after the press of a + // non-repeatable key. + let key_repeats = + self.xkb_context.keymap_mut().map(|k| k.key_repeats(keycode)).unwrap_or(false); + let repeat = if key_repeats { + let is_latest_held = self.held_key_press == Some(keycode); + + if state == ElementState::Pressed { + self.held_key_press = Some(keycode); + is_latest_held + } else { + // Check that the released key is the latest repeatable key that has been + // pressed, since repeats will continue for the latest key press if a + // different previously pressed key is released. + if is_latest_held { + self.held_key_press = None; + } + false + } + } else { + false + }; + + // NOTE: When the modifier was captured by the XFilterEvents the modifiers for the modifier + // itself are out of sync due to XkbState being delivered before XKeyEvent, since it's + // being replayed by the XIM, thus we should replay ourselves. + let replay = if let Some(position) = + self.xfiltered_modifiers.iter().rev().position(|&s| s == xev.keycode as u8) + { + // We don't have to replay modifiers pressed before the current event if some events + // were not forwarded to us, since their state is irrelevant. + self.xfiltered_modifiers.resize(self.xfiltered_modifiers.len() - 1 - position, 0); + true + } else { + false + }; + + // Always update the modifiers when we're not replaying. + if !replay { + self.update_mods_from_core_event(window_id, xev.state as u16, &mut callback); + } + + if keycode != 0 && !self.is_composing { + // Don't alter the modifiers state from replaying. + if replay { + self.send_synthic_modifier_from_core(window_id, xev.state as u16, &mut callback); + } + + if let Some(mut key_processor) = self.xkb_context.key_context() { + let event = key_processor.process_key_event(keycode, state, repeat); + let event = Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { device_id, event, is_synthetic: false }, + }; + callback(&self.target, event); + } + + // Restore the client's modifiers state after replay. + if replay { + self.send_modifiers(window_id, self.modifiers.get(), true, &mut callback); + } + + return; + } + + let wt = Self::window_target(&self.target); + + if let Some(ic) = + wt.ime.as_ref().and_then(|ime| ime.borrow().get_context(window as XWindow)) + { + let written = wt.xconn.lookup_utf8(ic, xev); + if !written.is_empty() { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }; + callback(&self.target, event); + + let event = + Event::WindowEvent { window_id, event: WindowEvent::Ime(Ime::Commit(written)) }; + + self.is_composing = false; + callback(&self.target, event); + } + } + } + + fn send_synthic_modifier_from_core( + &mut self, + window_id: crate::window::WindowId, + state: u16, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let keymap = match self.xkb_context.keymap_mut() { + Some(keymap) => keymap, + None => return, + }; + + let wt = Self::window_target(&self.target); + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + + // Use synthetic state since we're replaying the modifier. The user modifier state + // will be restored later. + let mut xkb_state = match XkbState::new_x11(xcb, keymap) { + Some(xkb_state) => xkb_state, + None => return, + }; + + let mask = self.xkb_mod_mask_from_core(state); + xkb_state.update_modifiers(mask, 0, 0, 0, 0, Self::core_keyboard_group(state)); + let mods: ModifiersState = xkb_state.modifiers().into(); + + let event = + Event::WindowEvent { window_id, event: WindowEvent::ModifiersChanged(mods.into()) }; + + callback(&self.target, event); + } + + fn xinput2_button_input( + &self, + event: &XIDeviceEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window_id = mkwid(event.event as xproto::Window); + let device_id = mkdid(event.deviceid as xinput::DeviceId); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + // Deliver multi-touch events instead of emulated mouse events. + if (event.flags & xinput2::XIPointerEmulated) != 0 { + return; + } + + let event = match event.detail as u32 { + xlib::Button1 => { + WindowEvent::MouseInput { device_id, state, button: MouseButton::Left } + }, + xlib::Button2 => { + WindowEvent::MouseInput { device_id, state, button: MouseButton::Middle } + }, + + xlib::Button3 => { + WindowEvent::MouseInput { device_id, state, button: MouseButton::Right } + }, + + // Suppress emulated scroll wheel clicks, since we handle the real motion events for + // those. In practice, even clicky scroll wheels appear to be reported by + // evdev (and XInput2 in turn) as axis motion, so we don't otherwise + // special-case these button presses. + 4..=7 => WindowEvent::MouseWheel { + device_id, + delta: match event.detail { + 4 => MouseScrollDelta::LineDelta(0.0, 1.0), + 5 => MouseScrollDelta::LineDelta(0.0, -1.0), + 6 => MouseScrollDelta::LineDelta(1.0, 0.0), + 7 => MouseScrollDelta::LineDelta(-1.0, 0.0), + _ => unreachable!(), + }, + phase: TouchPhase::Moved, + }, + 8 => WindowEvent::MouseInput { device_id, state, button: MouseButton::Back }, + + 9 => WindowEvent::MouseInput { device_id, state, button: MouseButton::Forward }, + x => WindowEvent::MouseInput { device_id, state, button: MouseButton::Other(x as u16) }, + }; + + let event = Event::WindowEvent { window_id, event }; + callback(&self.target, event); + } + + fn xinput2_mouse_motion(&self, event: &XIDeviceEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + let device_id = mkdid(event.deviceid as xinput::DeviceId); + let window = event.event as xproto::Window; + let window_id = mkwid(window); + let new_cursor_pos = (event.event_x, event.event_y); + + let cursor_moved = self.with_window(window, |window| { + let mut shared_state_lock = window.shared_state_lock(); + util::maybe_change(&mut shared_state_lock.cursor_pos, new_cursor_pos) + }); + + if cursor_moved == Some(true) { + let position = PhysicalPosition::new(event.event_x, event.event_y); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { device_id, position }, + }; + callback(&self.target, event); + } else if cursor_moved.is_none() { + return; + } + + // More gymnastics, for self.devices + let mask = unsafe { + slice::from_raw_parts(event.valuators.mask, event.valuators.mask_len as usize) + }; + let mut devices = self.devices.borrow_mut(); + let physical_device = match devices.get_mut(&DeviceId(event.sourceid as xinput::DeviceId)) { + Some(device) => device, + None => return, + }; + + let mut events = Vec::new(); + let mut value = event.valuators.values; + for i in 0..event.valuators.mask_len * 8 { + if !xinput2::XIMaskIsSet(mask, i) { + continue; + } + + let x = unsafe { *value }; + + let event = if let Some(&mut (_, ref mut info)) = + physical_device.scroll_axes.iter_mut().find(|&&mut (axis, _)| axis == i as _) + { + let delta = (x - info.position) / info.increment; + info.position = x; + // X11 vertical scroll coordinates are opposite to winit's + let delta = match info.orientation { + ScrollOrientation::Horizontal => { + MouseScrollDelta::LineDelta(-delta as f32, 0.0) + }, + ScrollOrientation::Vertical => MouseScrollDelta::LineDelta(0.0, -delta as f32), + }; + + WindowEvent::MouseWheel { device_id, delta, phase: TouchPhase::Moved } + } else { + WindowEvent::AxisMotion { device_id, axis: i as u32, value: unsafe { *value } } + }; + + events.push(Event::WindowEvent { window_id, event }); + + value = unsafe { value.offset(1) }; + } + + for event in events { + callback(&self.target, event); + } + } + + fn xinput2_mouse_enter(&self, event: &XIEnterEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + let window = event.event as xproto::Window; + let window_id = mkwid(window); + let device_id = mkdid(event.deviceid as xinput::DeviceId); + + if let Some(all_info) = DeviceInfo::get(&wt.xconn, super::ALL_DEVICES.into()) { + let mut devices = self.devices.borrow_mut(); + for device_info in all_info.iter() { + // The second expression is need for resetting to work correctly on i3, and + // presumably some other WMs. On those, `XI_Enter` doesn't include the physical + // device ID, so both `sourceid` and `deviceid` are the virtual device. + if device_info.deviceid == event.sourceid + || device_info.attachment == event.sourceid + { + let device_id = DeviceId(device_info.deviceid as _); + if let Some(device) = devices.get_mut(&device_id) { + device.reset_scroll_position(device_info); + } + } + } + } + + if self.window_exists(window) { + let position = PhysicalPosition::new(event.event_x, event.event_y); + + let event = + Event::WindowEvent { window_id, event: WindowEvent::CursorEntered { device_id } }; + callback(&self.target, event); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { device_id, position }, + }; + callback(&self.target, event); + } + } + + fn xinput2_mouse_left(&self, event: &XILeaveEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window = event.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + // Leave, FocusIn, and FocusOut can be received by a window that's already + // been destroyed, which the user presumably doesn't want to deal with. + if self.window_exists(window) { + let event = Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::CursorLeft { + device_id: mkdid(event.deviceid as xinput::DeviceId), + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_focused(&mut self, xev: &XIFocusInEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window = xev.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut().focus(xev.event).expect("Failed to focus input context"); + } + + if self.active_window == Some(window) { + return; + } + + self.active_window = Some(window); + + wt.update_listen_device_events(true); + + let window_id = mkwid(window); + let position = PhysicalPosition::new(xev.event_x, xev.event_y); + + if let Some(window) = self.with_window(window, Arc::clone) { + window.shared_state_lock().has_focus = true; + } + + let event = Event::WindowEvent { window_id, event: WindowEvent::Focused(true) }; + callback(&self.target, event); + + // Issue key press events for all pressed keys + Self::handle_pressed_keys( + &self.target, + window_id, + ElementState::Pressed, + &mut self.xkb_context, + &mut callback, + ); + + self.update_mods_from_query(window_id, &mut callback); + + // The deviceid for this event is for a keyboard instead of a pointer, + // so we have to do a little extra work. + let pointer_id = self + .devices + .borrow() + .get(&DeviceId(xev.deviceid as xinput::DeviceId)) + .map(|device| device.attachment) + .unwrap_or(2); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { device_id: mkdid(pointer_id as _), position }, + }; + callback(&self.target, event); + } + + fn xinput2_unfocused(&mut self, xev: &XIFocusOutEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window = xev.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if !self.window_exists(window) { + return; + } + + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut().unfocus(xev.event).expect("Failed to unfocus input context"); + } + + if self.active_window.take() == Some(window) { + let window_id = mkwid(window); + + wt.update_listen_device_events(false); + + // Clear the modifiers when unfocusing the window. + if let Some(xkb_state) = self.xkb_context.state_mut() { + xkb_state.update_modifiers(0, 0, 0, 0, 0, 0); + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), true, &mut callback); + } + + // Issue key release events for all pressed keys + Self::handle_pressed_keys( + &self.target, + window_id, + ElementState::Released, + &mut self.xkb_context, + &mut callback, + ); + + // Clear this so detecting key repeats is consistently handled when the + // window regains focus. + self.held_key_press = None; + + if let Some(window) = self.with_window(window, Arc::clone) { + window.shared_state_lock().has_focus = false; + } + + let event = Event::WindowEvent { window_id, event: WindowEvent::Focused(false) }; + callback(&self.target, event) + } + } + + fn xinput2_touch( + &mut self, + xev: &XIDeviceEvent, + phase: TouchPhase, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let window = xev.event as xproto::Window; + if self.window_exists(window) { + let window_id = mkwid(window); + let id = xev.detail as u64; + let location = PhysicalPosition::new(xev.event_x, xev.event_y); + + // Mouse cursor position changes when touch events are received. + // Only the first concurrently active touch ID moves the mouse cursor. + if is_first_touch(&mut self.first_touch, &mut self.num_touch, id, phase) { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { + device_id: mkdid(util::VIRTUAL_CORE_POINTER), + position: location.cast(), + }, + }; + callback(&self.target, event); + } + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Touch(Touch { + device_id: mkdid(xev.deviceid as xinput::DeviceId), + phase, + location, + force: None, // TODO + id, + }), + }; + callback(&self.target, event) + } + } + + fn xinput2_raw_button_input( + &self, + xev: &XIRawEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if xev.flags & xinput2::XIPointerEmulated == 0 { + let event = Event::DeviceEvent { + device_id: mkdid(xev.deviceid as xinput::DeviceId), + event: DeviceEvent::Button { state, button: xev.detail as u32 }, + }; + callback(&self.target, event); + } + } + + fn xinput2_raw_mouse_motion(&self, xev: &XIRawEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let did = mkdid(xev.deviceid as xinput::DeviceId); + + let mask = + unsafe { slice::from_raw_parts(xev.valuators.mask, xev.valuators.mask_len as usize) }; + let mut value = xev.raw_values; + let mut mouse_delta = util::Delta::default(); + let mut scroll_delta = util::Delta::default(); + for i in 0..xev.valuators.mask_len * 8 { + if !xinput2::XIMaskIsSet(mask, i) { + continue; + } + let x = unsafe { value.read_unaligned() }; + + // We assume that every XInput2 device with analog axes is a pointing device emitting + // relative coordinates. + match i { + 0 => mouse_delta.set_x(x), + 1 => mouse_delta.set_y(x), + 2 => scroll_delta.set_x(x as f32), + 3 => scroll_delta.set_y(x as f32), + _ => {}, + } + + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::Motion { axis: i as u32, value: x }, + }; + callback(&self.target, event); + + value = unsafe { value.offset(1) }; + } + + if let Some(mouse_delta) = mouse_delta.consume() { + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::MouseMotion { delta: mouse_delta }, + }; + callback(&self.target, event); + } + + if let Some(scroll_delta) = scroll_delta.consume() { + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::MouseWheel { + delta: MouseScrollDelta::LineDelta(scroll_delta.0, scroll_delta.1), + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_raw_key_input( + &mut self, + xev: &XIRawEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let device_id = mkdid(xev.sourceid as xinput::DeviceId); + let keycode = xev.detail as u32; + if keycode < KEYCODE_OFFSET as u32 { + return; + } + let physical_key = xkb::raw_keycode_to_physicalkey(keycode); + + callback(&self.target, Event::DeviceEvent { + device_id, + event: DeviceEvent::Key(RawKeyEvent { physical_key, state }), + }); + } + + fn xinput2_hierarchy_changed(&mut self, xev: &XIHierarchyEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + let infos = unsafe { slice::from_raw_parts(xev.info, xev.num_info as usize) }; + for info in infos { + if 0 != info.flags & (xinput2::XISlaveAdded | xinput2::XIMasterAdded) { + self.init_device(info.deviceid as xinput::DeviceId); + callback(&self.target, Event::DeviceEvent { + device_id: mkdid(info.deviceid as xinput::DeviceId), + event: DeviceEvent::Added, + }); + } else if 0 != info.flags & (xinput2::XISlaveRemoved | xinput2::XIMasterRemoved) { + callback(&self.target, Event::DeviceEvent { + device_id: mkdid(info.deviceid as xinput::DeviceId), + event: DeviceEvent::Removed, + }); + let mut devices = self.devices.borrow_mut(); + devices.remove(&DeviceId(info.deviceid as xinput::DeviceId)); + } + } + } + + fn xkb_event(&mut self, xev: &XkbAnyEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + match xev.xkb_type { + xlib::XkbNewKeyboardNotify => { + let xev = unsafe { &*(xev as *const _ as *const xlib::XkbNewKeyboardNotifyEvent) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let keycodes_changed_flag = 0x1; + let geometry_changed_flag = 0x1 << 1; + + let keycodes_changed = util::has_flag(xev.changed, keycodes_changed_flag); + let geometry_changed = util::has_flag(xev.changed, geometry_changed_flag); + + if xev.device == self.xkb_context.core_keyboard_id + && (keycodes_changed || geometry_changed) + { + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + self.xkb_context.set_keymap_from_x11(xcb); + self.xmodmap.reload_from_x_connection(&wt.xconn); + + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + if let Some(state) = self.xkb_context.state_mut() { + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } + } + }, + xlib::XkbMapNotify => { + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + self.xkb_context.set_keymap_from_x11(xcb); + self.xmodmap.reload_from_x_connection(&wt.xconn); + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + if let Some(state) = self.xkb_context.state_mut() { + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } + }, + xlib::XkbStateNotify => { + let xev = unsafe { &*(xev as *const _ as *const xlib::XkbStateNotifyEvent) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if let Some(state) = self.xkb_context.state_mut() { + state.update_modifiers( + xev.base_mods, + xev.latched_mods, + xev.locked_mods, + xev.base_group as u32, + xev.latched_group as u32, + xev.locked_group as u32, + ); + + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } + }, + _ => {}, + } + } + + pub fn update_mods_from_xinput2_event( + &mut self, + mods: &XIModifierState, + group: &XIModifierState, + force: bool, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + if let Some(state) = self.xkb_context.state_mut() { + state.update_modifiers( + mods.base as u32, + mods.latched as u32, + mods.locked as u32, + group.base as u32, + group.latched as u32, + group.locked as u32, + ); + + // NOTE: we use active window since generally sub windows don't have keyboard input, + // and winit assumes that unfocused window doesn't have modifiers. + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + let mods = state.modifiers(); + self.send_modifiers(window_id, mods.into(), force, &mut callback); + } + } + + fn update_mods_from_query( + &mut self, + window_id: crate::window::WindowId, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + let xkb_state = match self.xkb_context.state_mut() { + Some(xkb_state) => xkb_state, + None => return, + }; + + unsafe { + let mut state: XkbStateRec = std::mem::zeroed(); + if (wt.xconn.xlib.XkbGetState)(wt.xconn.display, XkbId::USE_CORE_KBD.into(), &mut state) + == xlib::True + { + xkb_state.update_modifiers( + state.base_mods as u32, + state.latched_mods as u32, + state.locked_mods as u32, + state.base_group as u32, + state.latched_group as u32, + state.locked_group as u32, + ); + } + } + + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), true, &mut callback) + } + + pub fn update_mods_from_core_event( + &mut self, + window_id: crate::window::WindowId, + state: u16, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let xkb_mask = self.xkb_mod_mask_from_core(state); + let xkb_state = match self.xkb_context.state_mut() { + Some(xkb_state) => xkb_state, + None => return, + }; + + // NOTE: this is inspired by Qt impl. + let mut depressed = xkb_state.depressed_modifiers() & xkb_mask; + let latched = xkb_state.latched_modifiers() & xkb_mask; + let locked = xkb_state.locked_modifiers() & xkb_mask; + // Set modifiers in depressed if they don't appear in any of the final masks. + depressed |= !(depressed | latched | locked) & xkb_mask; + + xkb_state.update_modifiers( + depressed, + latched, + locked, + 0, + 0, + Self::core_keyboard_group(state), + ); + + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), false, &mut callback); + } + + // Bits 13 and 14 report the state keyboard group. + pub fn core_keyboard_group(state: u16) -> u32 { + ((state >> 13) & 3) as u32 + } + + pub fn xkb_mod_mask_from_core(&mut self, state: u16) -> xkb_mod_mask_t { + let mods_indices = match self.xkb_context.keymap_mut() { + Some(keymap) => keymap.mods_indices(), + None => return 0, + }; + + // Build the XKB modifiers from the regular state. + let mut depressed = 0u32; + if let Some(shift) = mods_indices.shift.filter(|_| ModMask::SHIFT.intersects(state)) { + depressed |= 1 << shift; + } + if let Some(caps) = mods_indices.caps.filter(|_| ModMask::LOCK.intersects(state)) { + depressed |= 1 << caps; + } + if let Some(ctrl) = mods_indices.ctrl.filter(|_| ModMask::CONTROL.intersects(state)) { + depressed |= 1 << ctrl; + } + if let Some(alt) = mods_indices.alt.filter(|_| ModMask::M1.intersects(state)) { + depressed |= 1 << alt; + } + if let Some(num) = mods_indices.num.filter(|_| ModMask::M2.intersects(state)) { + depressed |= 1 << num; + } + if let Some(mod3) = mods_indices.mod3.filter(|_| ModMask::M3.intersects(state)) { + depressed |= 1 << mod3; + } + if let Some(logo) = mods_indices.logo.filter(|_| ModMask::M4.intersects(state)) { + depressed |= 1 << logo; + } + if let Some(mod5) = mods_indices.mod5.filter(|_| ModMask::M5.intersects(state)) { + depressed |= 1 << mod5; + } + + depressed + } + + /// Send modifiers for the active window. + /// + /// The event won't be sent when the `modifiers` match the previously `sent` modifiers value, + /// unless `force` is passed. The `force` should be passed when the active window changes. + fn send_modifiers)>( + &self, + window_id: crate::window::WindowId, + modifiers: ModifiersState, + force: bool, + callback: &mut F, + ) { + // NOTE: Always update the modifiers to account for case when they've changed + // and forced was `true`. + if self.modifiers.replace(modifiers) != modifiers || force { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::ModifiersChanged(self.modifiers.get().into()), + }; + callback(&self.target, event); + } + } + + fn handle_pressed_keys( + target: &RootAEL, + window_id: crate::window::WindowId, + state: ElementState, + xkb_context: &mut Context, + callback: &mut F, + ) where + F: FnMut(&RootAEL, Event), + { + let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD); + + // Update modifiers state and emit key events based on which keys are currently pressed. + let window_target = Self::window_target(target); + let xcb = window_target.xconn.xcb_connection().get_raw_xcb_connection(); + + let keymap = match xkb_context.keymap_mut() { + Some(keymap) => keymap, + None => return, + }; + + // Send the keys using the synthetic state to not alter the main state. + let mut xkb_state = match XkbState::new_x11(xcb, keymap) { + Some(xkb_state) => xkb_state, + None => return, + }; + let mut key_processor = match xkb_context.key_context_with_state(&mut xkb_state) { + Some(key_processor) => key_processor, + None => return, + }; + + for keycode in + window_target.xconn.query_keymap().into_iter().filter(|k| *k >= KEYCODE_OFFSET) + { + let event = key_processor.process_key_event(keycode as u32, state, false); + let event = Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { device_id, event, is_synthetic: true }, + }; + callback(target, event); + } + } + + fn process_dpi_change(&self, callback: &mut F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + wt.xconn.reload_database().expect("failed to reload Xft database"); + + // In the future, it would be quite easy to emit monitor hotplug events. + let prev_list = { + let prev_list = wt.xconn.invalidate_cached_monitor_list(); + match prev_list { + Some(prev_list) => prev_list, + None => return, + } + }; + + let new_list = wt.xconn.available_monitors().expect("Failed to get monitor list"); + for new_monitor in new_list { + // Previous list may be empty, in case of disconnecting and + // reconnecting the only one monitor. We still need to emit events in + // this case. + let maybe_prev_scale_factor = prev_list + .iter() + .find(|prev_monitor| prev_monitor.name == new_monitor.name) + .map(|prev_monitor| prev_monitor.scale_factor); + if Some(new_monitor.scale_factor) != maybe_prev_scale_factor { + for window in wt.windows.borrow().iter().filter_map(|(_, w)| w.upgrade()) { + window.refresh_dpi_for_monitor(&new_monitor, maybe_prev_scale_factor, |event| { + callback(&self.target, event); + }) + } + } + } + } + + fn window_exists(&self, window_id: xproto::Window) -> bool { + self.with_window(window_id, |_| ()).is_some() + } +} + +fn is_first_touch(first: &mut Option, num: &mut u32, id: u64, phase: TouchPhase) -> bool { + match phase { + TouchPhase::Started => { + if *num == 0 { + *first = Some(id); + } + *num += 1; + }, + TouchPhase::Cancelled | TouchPhase::Ended => { + if *first == Some(id) { + *first = None; + } + *num = num.saturating_sub(1); + }, + _ => (), + } + + *first == Some(id) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/ffi.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ffi.rs new file mode 100644 index 0000000..57bd78e --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ffi.rs @@ -0,0 +1,5 @@ +pub use x11_dl::error::OpenError; +pub use x11_dl::xcursor::*; +pub use x11_dl::xinput2::*; +pub use x11_dl::xlib::*; +pub use x11_dl::xlib_xcb::*; diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/callbacks.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/callbacks.rs new file mode 100644 index 0000000..fa0fa5c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/callbacks.rs @@ -0,0 +1,206 @@ +use std::collections::HashMap; +use std::os::raw::c_char; +use std::ptr; +use std::sync::Arc; + +use super::{ffi, XConnection, XError}; + +use super::context::{ImeContext, ImeContextCreationError}; +use super::inner::{close_im, ImeInner}; +use super::input_method::PotentialInputMethods; + +pub(crate) unsafe fn xim_set_callback( + xconn: &Arc, + xim: ffi::XIM, + field: *const c_char, + callback: *mut ffi::XIMCallback, +) -> Result<(), XError> { + // It's advisable to wrap variadic FFI functions in our own functions, as we want to minimize + // access that isn't type-checked. + unsafe { (xconn.xlib.XSetIMValues)(xim, field, callback, ptr::null_mut::<()>()) }; + xconn.check_errors() +} + +// Set a callback for when an input method matching the current locale modifiers becomes +// available. Note that this has nothing to do with what input methods are open or able to be +// opened, and simply uses the modifiers that are set when the callback is set. +// * This is called per locale modifier, not per input method opened with that locale modifier. +// * Trying to set this for multiple locale modifiers causes problems, i.e. one of the rebuilt input +// contexts would always silently fail to use the input method. +pub(crate) unsafe fn set_instantiate_callback( + xconn: &Arc, + client_data: ffi::XPointer, +) -> Result<(), XError> { + unsafe { + (xconn.xlib.XRegisterIMInstantiateCallback)( + xconn.display, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + Some(xim_instantiate_callback), + client_data, + ) + }; + xconn.check_errors() +} + +pub(crate) unsafe fn unset_instantiate_callback( + xconn: &Arc, + client_data: ffi::XPointer, +) -> Result<(), XError> { + unsafe { + (xconn.xlib.XUnregisterIMInstantiateCallback)( + xconn.display, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + Some(xim_instantiate_callback), + client_data, + ) + }; + xconn.check_errors() +} + +pub(crate) unsafe fn set_destroy_callback( + xconn: &Arc, + im: ffi::XIM, + inner: &ImeInner, +) -> Result<(), XError> { + unsafe { + xim_set_callback( + xconn, + im, + ffi::XNDestroyCallback_0.as_ptr() as *const _, + &inner.destroy_callback as *const _ as *mut _, + ) + } +} + +#[derive(Debug)] +#[allow(clippy::enum_variant_names)] +enum ReplaceImError { + // Boxed to prevent large error type + MethodOpenFailed(#[allow(dead_code)] Box), + ContextCreationFailed(#[allow(dead_code)] ImeContextCreationError), + SetDestroyCallbackFailed(#[allow(dead_code)] XError), +} + +// Attempt to replace current IM (which may or may not be presently valid) with a new one. This +// includes replacing all existing input contexts and free'ing resources as necessary. This only +// modifies existing state if all operations succeed. +unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> { + let xconn = unsafe { &(*inner).xconn }; + + let (new_im, is_fallback) = { + let new_im = unsafe { (*inner).potential_input_methods.open_im(xconn, None) }; + let is_fallback = new_im.is_fallback(); + ( + new_im.ok().ok_or_else(|| { + ReplaceImError::MethodOpenFailed(Box::new(unsafe { + (*inner).potential_input_methods.clone() + })) + })?, + is_fallback, + ) + }; + + // It's important to always set a destroy callback, since there's otherwise potential for us + // to try to use or free a resource that's already been destroyed on the server. + { + let result = unsafe { set_destroy_callback(xconn, new_im.im, &*inner) }; + if result.is_err() { + let _ = unsafe { close_im(xconn, new_im.im) }; + } + result + } + .map_err(ReplaceImError::SetDestroyCallbackFailed)?; + + let mut new_contexts = HashMap::new(); + for (window, old_context) in unsafe { (*inner).contexts.iter() } { + let spot = old_context.as_ref().map(|old_context| old_context.ic_spot); + + // Check if the IME was allowed on that context. + let is_allowed = + old_context.as_ref().map(|old_context| old_context.is_allowed()).unwrap_or_default(); + + let new_context = { + let result = unsafe { + ImeContext::new( + xconn, + &new_im, + *window, + spot, + (*inner).event_sender.clone(), + is_allowed, + ) + }; + if result.is_err() { + let _ = unsafe { close_im(xconn, new_im.im) }; + } + result.map_err(ReplaceImError::ContextCreationFailed)? + }; + new_contexts.insert(*window, Some(new_context)); + } + + // If we've made it this far, everything succeeded. + unsafe { + let _ = (*inner).destroy_all_contexts_if_necessary(); + let _ = (*inner).close_im_if_necessary(); + (*inner).im = Some(new_im); + (*inner).contexts = new_contexts; + (*inner).is_destroyed = false; + (*inner).is_fallback = is_fallback; + } + Ok(()) +} + +pub unsafe extern "C" fn xim_instantiate_callback( + _display: *mut ffi::Display, + client_data: ffi::XPointer, + // This field is unsupplied. + _call_data: ffi::XPointer, +) { + let inner: *mut ImeInner = client_data as _; + if !inner.is_null() { + let xconn = unsafe { &(*inner).xconn }; + match unsafe { replace_im(inner) } { + Ok(()) => unsafe { + let _ = unset_instantiate_callback(xconn, client_data); + (*inner).is_fallback = false; + }, + Err(err) => unsafe { + if (*inner).is_destroyed { + // We have no usable input methods! + panic!("Failed to reopen input method: {err:?}"); + } + }, + } + } +} + +// This callback is triggered when the input method is closed on the server end. When this +// happens, XCloseIM/XDestroyIC doesn't need to be called, as the resources have already been +// free'd (attempting to do so causes our connection to freeze). +pub unsafe extern "C" fn xim_destroy_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + // This field is unsupplied. + _call_data: ffi::XPointer, +) { + let inner: *mut ImeInner = client_data as _; + if !inner.is_null() { + unsafe { (*inner).is_destroyed = true }; + let xconn = unsafe { &(*inner).xconn }; + if unsafe { !(*inner).is_fallback } { + let _ = unsafe { set_instantiate_callback(xconn, client_data) }; + // Attempt to open fallback input method. + match unsafe { replace_im(inner) } { + Ok(()) => unsafe { (*inner).is_fallback = true }, + Err(err) => { + // We have no usable input methods! + panic!("Failed to open fallback input method: {err:?}"); + }, + } + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/context.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/context.rs new file mode 100644 index 0000000..2c6c075 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/context.rs @@ -0,0 +1,376 @@ +use std::ffi::CStr; +use std::os::raw::c_short; +use std::sync::Arc; +use std::{mem, ptr}; + +use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct}; + +use super::{ffi, util, XConnection, XError}; +use crate::platform_impl::platform::x11::ime::input_method::{InputMethod, Style, XIMStyle}; +use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender}; + +/// IME creation error. +#[derive(Debug)] +pub enum ImeContextCreationError { + /// Got the error from Xlib. + XError(XError), + + /// Got null pointer from Xlib but without exact reason. + Null, +} + +/// The callback used by XIM preedit functions. +type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer); + +/// Wrapper for creating XIM callbacks. +#[inline] +fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback { + XIMCallback { client_data, callback: Some(callback) } +} + +/// The server started preedit. +extern "C" fn preedit_start_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + _call_data: ffi::XPointer, +) -> i32 { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + + client_data.text.clear(); + client_data.cursor_pos = 0; + client_data + .event_sender + .send((client_data.window, ImeEvent::Start)) + .expect("failed to send preedit start event"); + -1 +} + +/// Done callback is used when the preedit should be hidden. +extern "C" fn preedit_done_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + _call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + + // Drop text buffer and reset cursor position on done. + client_data.text = Vec::new(); + client_data.cursor_pos = 0; + + client_data + .event_sender + .send((client_data.window, ImeEvent::End)) + .expect("failed to send preedit end event"); +} + +fn calc_byte_position(text: &[char], pos: usize) -> usize { + text.iter().take(pos).fold(0, |byte_pos, text| byte_pos + text.len_utf8()) +} + +/// Preedit text information to be drawn inline by the client. +extern "C" fn preedit_draw_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) }; + client_data.cursor_pos = call_data.caret as usize; + + let chg_range = + call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize; + if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() { + tracing::warn!( + "invalid chg range: buffer length={}, but chg_first={} chg_length={}", + client_data.text.len(), + call_data.chg_first, + call_data.chg_length + ); + return; + } + + // NULL indicate text deletion + let mut new_chars = if call_data.text.is_null() { + Vec::new() + } else { + let xim_text = unsafe { &mut *(call_data.text) }; + if xim_text.encoding_is_wchar > 0 { + return; + } + + let new_text = unsafe { xim_text.string.multi_byte }; + + if new_text.is_null() { + return; + } + + let new_text = unsafe { CStr::from_ptr(new_text) }; + + String::from(new_text.to_str().expect("Invalid UTF-8 String from IME")).chars().collect() + }; + let mut old_text_tail = client_data.text.split_off(chg_range.end); + client_data.text.truncate(chg_range.start); + client_data.text.append(&mut new_chars); + client_data.text.append(&mut old_text_tail); + let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos); + + client_data + .event_sender + .send(( + client_data.window, + ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos), + )) + .expect("failed to send preedit update event"); +} + +/// Handling of cursor movements in preedit text. +extern "C" fn preedit_caret_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) }; + + if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition { + client_data.cursor_pos = call_data.position as usize; + let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos); + + client_data + .event_sender + .send(( + client_data.window, + ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos), + )) + .expect("failed to send preedit update event"); + } +} + +/// Struct to simplify callback creation and latter passing into Xlib XIM. +struct PreeditCallbacks { + start_callback: ffi::XIMCallback, + done_callback: ffi::XIMCallback, + draw_callback: ffi::XIMCallback, + caret_callback: ffi::XIMCallback, +} + +impl PreeditCallbacks { + pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks { + let start_callback = create_xim_callback(client_data, unsafe { + mem::transmute::( + preedit_start_callback as *const () as usize, + ) + }); + let done_callback = create_xim_callback(client_data, preedit_done_callback); + let caret_callback = create_xim_callback(client_data, preedit_caret_callback); + let draw_callback = create_xim_callback(client_data, preedit_draw_callback); + + PreeditCallbacks { start_callback, done_callback, caret_callback, draw_callback } + } +} + +struct ImeContextClientData { + window: ffi::Window, + event_sender: ImeEventSender, + text: Vec, + cursor_pos: usize, +} + +// XXX: this struct doesn't destroy its XIC resource when dropped. +// This is intentional, as it doesn't have enough information to know whether or not the context +// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled +// through `ImeInner`. +pub struct ImeContext { + pub(crate) ic: ffi::XIC, + pub(crate) ic_spot: ffi::XPoint, + pub(crate) allowed: bool, + // Since the data is passed shared between X11 XIM callbacks, but couldn't be directly free + // from there we keep the pointer to automatically deallocate it. + _client_data: Box, +} + +impl ImeContext { + pub(crate) unsafe fn new( + xconn: &Arc, + im: &InputMethod, + window: ffi::Window, + ic_spot: Option, + event_sender: ImeEventSender, + allowed: bool, + ) -> Result { + let client_data = Box::into_raw(Box::new(ImeContextClientData { + window, + event_sender, + text: Vec::new(), + cursor_pos: 0, + })); + + let style = if allowed { im.preedit_style } else { im.none_style }; + + let ic = match style as _ { + Style::Preedit(style) => unsafe { + ImeContext::create_preedit_ic( + xconn, + im.im, + style, + window, + client_data as ffi::XPointer, + ) + }, + Style::Nothing(style) => unsafe { + ImeContext::create_nothing_ic(xconn, im.im, style, window) + }, + Style::None(style) => unsafe { + ImeContext::create_none_ic(xconn, im.im, style, window) + }, + } + .ok_or(ImeContextCreationError::Null)?; + + xconn.check_errors().map_err(ImeContextCreationError::XError)?; + + let mut context = ImeContext { + ic, + ic_spot: ffi::XPoint { x: 0, y: 0 }, + allowed, + _client_data: unsafe { Box::from_raw(client_data) }, + }; + + // Set the spot location, if it's present. + if let Some(ic_spot) = ic_spot { + context.set_spot(xconn, ic_spot.x, ic_spot.y) + } + + Ok(context) + } + + unsafe fn create_none_ic( + xconn: &Arc, + im: ffi::XIM, + style: XIMStyle, + window: ffi::Window, + ) -> Option { + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ptr::null_mut::<()>(), + ) + }; + + (!ic.is_null()).then_some(ic) + } + + unsafe fn create_preedit_ic( + xconn: &Arc, + im: ffi::XIM, + style: XIMStyle, + window: ffi::Window, + client_data: ffi::XPointer, + ) -> Option { + let preedit_callbacks = PreeditCallbacks::new(client_data); + let preedit_attr = util::memory::XSmartPointer::new(xconn, unsafe { + (xconn.xlib.XVaCreateNestedList)( + 0, + ffi::XNPreeditStartCallback_0.as_ptr() as *const _, + &(preedit_callbacks.start_callback) as *const _, + ffi::XNPreeditDoneCallback_0.as_ptr() as *const _, + &(preedit_callbacks.done_callback) as *const _, + ffi::XNPreeditCaretCallback_0.as_ptr() as *const _, + &(preedit_callbacks.caret_callback) as *const _, + ffi::XNPreeditDrawCallback_0.as_ptr() as *const _, + &(preedit_callbacks.draw_callback) as *const _, + ptr::null_mut::<()>(), + ) + }) + .expect("XVaCreateNestedList returned NULL"); + + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ffi::XNPreeditAttributes_0.as_ptr() as *const _, + preedit_attr.ptr, + ptr::null_mut::<()>(), + ) + }; + + (!ic.is_null()).then_some(ic) + } + + unsafe fn create_nothing_ic( + xconn: &Arc, + im: ffi::XIM, + style: XIMStyle, + window: ffi::Window, + ) -> Option { + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ptr::null_mut::<()>(), + ) + }; + + (!ic.is_null()).then_some(ic) + } + + pub(crate) fn focus(&self, xconn: &Arc) -> Result<(), XError> { + unsafe { + (xconn.xlib.XSetICFocus)(self.ic); + } + xconn.check_errors() + } + + pub(crate) fn unfocus(&self, xconn: &Arc) -> Result<(), XError> { + unsafe { + (xconn.xlib.XUnsetICFocus)(self.ic); + } + xconn.check_errors() + } + + pub fn is_allowed(&self) -> bool { + self.allowed + } + + // Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks + // are being used. Certain IMEs do show selection window, but it's placed in bottom left of the + // window and couldn't be changed. + // + // For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580. + pub(crate) fn set_spot(&mut self, xconn: &Arc, x: c_short, y: c_short) { + if !self.is_allowed() || self.ic_spot.x == x && self.ic_spot.y == y { + return; + } + + self.ic_spot = ffi::XPoint { x, y }; + + unsafe { + let preedit_attr = util::memory::XSmartPointer::new( + xconn, + (xconn.xlib.XVaCreateNestedList)( + 0, + ffi::XNSpotLocation_0.as_ptr(), + &self.ic_spot, + ptr::null_mut::<()>(), + ), + ) + .expect("XVaCreateNestedList returned NULL"); + + (xconn.xlib.XSetICValues)( + self.ic, + ffi::XNPreeditAttributes_0.as_ptr() as *const _, + preedit_attr.ptr, + ptr::null_mut::<()>(), + ); + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/inner.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/inner.rs new file mode 100644 index 0000000..da1ccf4 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/inner.rs @@ -0,0 +1,74 @@ +use std::collections::HashMap; +use std::mem; +use std::sync::Arc; + +use super::{ffi, XConnection, XError}; + +use super::context::ImeContext; +use super::input_method::{InputMethod, PotentialInputMethods}; +use crate::platform_impl::platform::x11::ime::ImeEventSender; + +pub(crate) unsafe fn close_im(xconn: &Arc, im: ffi::XIM) -> Result<(), XError> { + unsafe { (xconn.xlib.XCloseIM)(im) }; + xconn.check_errors() +} + +pub(crate) unsafe fn destroy_ic(xconn: &Arc, ic: ffi::XIC) -> Result<(), XError> { + unsafe { (xconn.xlib.XDestroyIC)(ic) }; + xconn.check_errors() +} + +pub(crate) struct ImeInner { + pub xconn: Arc, + pub im: Option, + pub potential_input_methods: PotentialInputMethods, + pub contexts: HashMap>, + // WARNING: this is initially zeroed! + pub destroy_callback: ffi::XIMCallback, + pub event_sender: ImeEventSender, + // Indicates whether or not the input method was destroyed on the server end + // (i.e. if ibus/fcitx/etc. was terminated/restarted) + pub is_destroyed: bool, + pub is_fallback: bool, +} + +impl ImeInner { + pub(crate) fn new( + xconn: Arc, + potential_input_methods: PotentialInputMethods, + event_sender: ImeEventSender, + ) -> Self { + ImeInner { + xconn, + im: None, + potential_input_methods, + contexts: HashMap::new(), + destroy_callback: unsafe { mem::zeroed() }, + event_sender, + is_destroyed: false, + is_fallback: false, + } + } + + pub unsafe fn close_im_if_necessary(&self) -> Result { + match self.im.as_ref() { + Some(im) if !self.is_destroyed => unsafe { close_im(&self.xconn, im.im).map(|_| true) }, + _ => Ok(false), + } + } + + pub unsafe fn destroy_ic_if_necessary(&self, ic: ffi::XIC) -> Result { + if !self.is_destroyed { + unsafe { destroy_ic(&self.xconn, ic) }.map(|_| true) + } else { + Ok(false) + } + } + + pub unsafe fn destroy_all_contexts_if_necessary(&self) -> Result { + for context in self.contexts.values().flatten() { + unsafe { self.destroy_ic_if_necessary(context.ic)? }; + } + Ok(!self.is_destroyed) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/input_method.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/input_method.rs new file mode 100644 index 0000000..e1f2fbd --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/input_method.rs @@ -0,0 +1,345 @@ +use std::ffi::{CStr, CString, IntoStringError}; +use std::os::raw::{c_char, c_ulong, c_ushort}; +use std::sync::{Arc, Mutex}; +use std::{env, fmt, ptr}; + +use super::super::atoms::*; +use super::{ffi, util, XConnection, XError}; +use x11rb::protocol::xproto; + +static GLOBAL_LOCK: Mutex<()> = Mutex::new(()); + +unsafe fn open_im(xconn: &Arc, locale_modifiers: &CStr) -> Option { + let _lock = GLOBAL_LOCK.lock(); + + // XSetLocaleModifiers returns... + // * The current locale modifiers if it's given a NULL pointer. + // * The new locale modifiers if we succeeded in setting them. + // * NULL if the locale modifiers string is malformed or if the current locale is not supported + // by Xlib. + unsafe { (xconn.xlib.XSetLocaleModifiers)(locale_modifiers.as_ptr()) }; + + let im = unsafe { + (xconn.xlib.XOpenIM)(xconn.display, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()) + }; + + if im.is_null() { + None + } else { + Some(im) + } +} + +#[derive(Debug)] +pub struct InputMethod { + pub im: ffi::XIM, + pub preedit_style: Style, + pub none_style: Style, + _name: String, +} + +impl InputMethod { + fn new(xconn: &Arc, im: ffi::XIM, name: String) -> Option { + let mut styles: *mut XIMStyles = std::ptr::null_mut(); + + // Query the styles supported by the XIM. + unsafe { + if !(xconn.xlib.XGetIMValues)( + im, + ffi::XNQueryInputStyle_0.as_ptr() as *const _, + (&mut styles) as *mut _, + std::ptr::null_mut::<()>(), + ) + .is_null() + { + return None; + } + } + + let mut preedit_style = None; + let mut none_style = None; + + unsafe { + std::slice::from_raw_parts((*styles).supported_styles, (*styles).count_styles as _) + .iter() + .for_each(|style| match *style { + XIM_PREEDIT_STYLE => { + preedit_style = Some(Style::Preedit(*style)); + }, + XIM_NOTHING_STYLE if preedit_style.is_none() => { + preedit_style = Some(Style::Nothing(*style)) + }, + XIM_NONE_STYLE => none_style = Some(Style::None(*style)), + _ => (), + }); + + (xconn.xlib.XFree)(styles.cast()); + }; + + if preedit_style.is_none() && none_style.is_none() { + return None; + } + + let preedit_style = preedit_style.unwrap_or_else(|| none_style.unwrap()); + let none_style = none_style.unwrap_or(preedit_style); + + Some(InputMethod { im, _name: name, preedit_style, none_style }) + } +} + +const XIM_PREEDIT_STYLE: XIMStyle = (ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing) as XIMStyle; +const XIM_NOTHING_STYLE: XIMStyle = (ffi::XIMPreeditNothing | ffi::XIMStatusNothing) as XIMStyle; +const XIM_NONE_STYLE: XIMStyle = (ffi::XIMPreeditNone | ffi::XIMStatusNone) as XIMStyle; + +/// Style of the IME context. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Style { + /// Preedit callbacks. + Preedit(XIMStyle), + + /// Nothing. + Nothing(XIMStyle), + + /// No IME. + None(XIMStyle), +} + +impl Default for Style { + fn default() -> Self { + Style::None(XIM_NONE_STYLE) + } +} + +#[repr(C)] +#[derive(Debug)] +struct XIMStyles { + count_styles: c_ushort, + supported_styles: *const XIMStyle, +} + +pub(crate) type XIMStyle = c_ulong; + +#[derive(Debug)] +pub enum InputMethodResult { + /// Input method used locale modifier from `XMODIFIERS` environment variable. + XModifiers(InputMethod), + /// Input method used internal fallback locale modifier. + Fallback(InputMethod), + /// Input method could not be opened using any locale modifier tried. + Failure, +} + +impl InputMethodResult { + pub fn is_fallback(&self) -> bool { + matches!(self, InputMethodResult::Fallback(_)) + } + + pub fn ok(self) -> Option { + use self::InputMethodResult::*; + match self { + XModifiers(im) | Fallback(im) => Some(im), + Failure => None, + } + } +} + +#[derive(Debug, Clone)] +enum GetXimServersError { + XError(#[allow(dead_code)] XError), + GetPropertyError(#[allow(dead_code)] util::GetPropertyError), + InvalidUtf8(#[allow(dead_code)] IntoStringError), +} + +impl From for GetXimServersError { + fn from(error: util::GetPropertyError) -> Self { + GetXimServersError::GetPropertyError(error) + } +} + +// The root window has a property named XIM_SERVERS, which contains a list of atoms representing +// the available XIM servers. For instance, if you're using ibus, it would contain an atom named +// "@server=ibus". It's possible for this property to contain multiple atoms, though presumably +// rare. Note that we replace "@server=" with "@im=" in order to match the format of locale +// modifiers, since we don't want a user who's looking at logs to ask "am I supposed to set +// XMODIFIERS to `@server=ibus`?!?" +unsafe fn get_xim_servers(xconn: &Arc) -> Result, GetXimServersError> { + let atoms = xconn.atoms(); + let servers_atom = atoms[XIM_SERVERS]; + + let root = unsafe { (xconn.xlib.XDefaultRootWindow)(xconn.display) }; + + let mut atoms: Vec = xconn + .get_property::( + root as xproto::Window, + servers_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ) + .map_err(GetXimServersError::GetPropertyError)? + .into_iter() + .map(|atom| atom as _) + .collect::>(); + + let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len()); + unsafe { + (xconn.xlib.XGetAtomNames)( + xconn.display, + atoms.as_mut_ptr(), + atoms.len() as _, + names.as_mut_ptr() as _, + ) + }; + unsafe { names.set_len(atoms.len()) }; + + let mut formatted_names = Vec::with_capacity(names.len()); + for name in names { + let string = unsafe { CStr::from_ptr(name) } + .to_owned() + .into_string() + .map_err(GetXimServersError::InvalidUtf8)?; + unsafe { (xconn.xlib.XFree)(name as _) }; + formatted_names.push(string.replace("@server=", "@im=")); + } + xconn.check_errors().map_err(GetXimServersError::XError)?; + Ok(formatted_names) +} + +#[derive(Clone)] +struct InputMethodName { + c_string: CString, + string: String, +} + +impl InputMethodName { + pub fn from_string(string: String) -> Self { + let c_string = CString::new(string.clone()) + .expect("String used to construct CString contained null byte"); + InputMethodName { c_string, string } + } + + pub fn from_str(string: &str) -> Self { + let c_string = + CString::new(string).expect("String used to construct CString contained null byte"); + InputMethodName { c_string, string: string.to_owned() } + } +} + +impl fmt::Debug for InputMethodName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.string.fmt(f) + } +} + +#[derive(Debug, Clone)] +struct PotentialInputMethod { + name: InputMethodName, + successful: Option, +} + +impl PotentialInputMethod { + pub fn from_string(string: String) -> Self { + PotentialInputMethod { name: InputMethodName::from_string(string), successful: None } + } + + pub fn from_str(string: &str) -> Self { + PotentialInputMethod { name: InputMethodName::from_str(string), successful: None } + } + + pub fn reset(&mut self) { + self.successful = None; + } + + pub fn open_im(&mut self, xconn: &Arc) -> Option { + let im = unsafe { open_im(xconn, &self.name.c_string) }; + self.successful = Some(im.is_some()); + im.and_then(|im| InputMethod::new(xconn, im, self.name.string.clone())) + } +} + +// By logging this struct, you get a sequential listing of every locale modifier tried, where it +// came from, and if it succeeded. +#[derive(Debug, Clone)] +pub(crate) struct PotentialInputMethods { + // On correctly configured systems, the XMODIFIERS environment variable tells us everything we + // need to know. + xmodifiers: Option, + // We have some standard options at our disposal that should ostensibly always work. For users + // who only need compose sequences, this ensures that the program launches without a hitch + // For users who need more sophisticated IME features, this is more or less a silent failure. + // Logging features should be added in the future to allow both audiences to be effectively + // served. + fallbacks: [PotentialInputMethod; 2], + // For diagnostic purposes, we include the list of XIM servers that the server reports as + // being available. + _xim_servers: Result, GetXimServersError>, +} + +impl PotentialInputMethods { + pub fn new(xconn: &Arc) -> Self { + let xmodifiers = env::var("XMODIFIERS").ok().map(PotentialInputMethod::from_string); + PotentialInputMethods { + // Since passing "" to XSetLocaleModifiers results in it defaulting to the value of + // XMODIFIERS, it's worth noting what happens if XMODIFIERS is also "". If simply + // running the program with `XMODIFIERS="" cargo run`, then assuming XMODIFIERS is + // defined in the profile (or parent environment) then that parent XMODIFIERS is used. + // If that XMODIFIERS value is also "" (i.e. if you ran `export XMODIFIERS=""`), then + // XSetLocaleModifiers uses the default local input method. Note that defining + // XMODIFIERS as "" is different from XMODIFIERS not being defined at all, since in + // that case, we get `None` and end up skipping ahead to the next method. + xmodifiers, + fallbacks: [ + // This is a standard input method that supports compose sequences, which should + // always be available. `@im=none` appears to mean the same thing. + PotentialInputMethod::from_str("@im=local"), + // This explicitly specifies to use the implementation-dependent default, though + // that seems to be equivalent to just using the local input method. + PotentialInputMethod::from_str("@im="), + ], + // The XIM_SERVERS property can have surprising values. For instance, when I exited + // ibus to run fcitx, it retained the value denoting ibus. Even more surprising is + // that the fcitx input method could only be successfully opened using "@im=ibus". + // Presumably due to this quirk, it's actually possible to alternate between ibus and + // fcitx in a running application. + _xim_servers: unsafe { get_xim_servers(xconn) }, + } + } + + // This resets the `successful` field of every potential input method, ensuring we have + // accurate information when this struct is re-used by the destruction/instantiation callbacks. + fn reset(&mut self) { + if let Some(ref mut input_method) = self.xmodifiers { + input_method.reset(); + } + + for input_method in &mut self.fallbacks { + input_method.reset(); + } + } + + pub fn open_im( + &mut self, + xconn: &Arc, + callback: Option<&dyn Fn()>, + ) -> InputMethodResult { + use self::InputMethodResult::*; + + self.reset(); + + if let Some(ref mut input_method) = self.xmodifiers { + let im = input_method.open_im(xconn); + if let Some(im) = im { + return XModifiers(im); + } else if let Some(ref callback) = callback { + callback(); + } + } + + for input_method in &mut self.fallbacks { + let im = input_method.open_im(xconn); + if let Some(im) = im { + return Fallback(im); + } + } + + Failure + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/mod.rs new file mode 100644 index 0000000..0a419c8 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/ime/mod.rs @@ -0,0 +1,233 @@ +// Important: all XIM calls need to happen from the same thread! + +mod callbacks; +mod context; +mod inner; +mod input_method; + +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::Arc; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use self::callbacks::*; +use self::context::ImeContext; +pub use self::context::ImeContextCreationError; +use self::inner::{close_im, ImeInner}; +use self::input_method::PotentialInputMethods; +use super::{ffi, util, XConnection, XError}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ImeEvent { + Enabled, + Start, + Update(String, usize), + End, + Disabled, +} + +pub type ImeReceiver = Receiver; +pub type ImeSender = Sender; +pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>; +pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>; + +/// Request to control XIM handler from the window. +pub enum ImeRequest { + /// Set IME spot position for given `window_id`. + Position(ffi::Window, i16, i16), + + /// Allow IME input for the given `window_id`. + Allow(ffi::Window, bool), +} + +#[derive(Debug)] +pub(crate) enum ImeCreationError { + // Boxed to prevent large error type + OpenFailure(Box), + SetDestroyCallbackFailed(#[allow(dead_code)] XError), +} + +pub(crate) struct Ime { + xconn: Arc, + // The actual meat of this struct is boxed away, since it needs to have a fixed location in + // memory so we can pass a pointer to it around. + inner: Box, +} + +impl Ime { + pub fn new( + xconn: Arc, + event_sender: ImeEventSender, + ) -> Result { + let potential_input_methods = PotentialInputMethods::new(&xconn); + + let (mut inner, client_data) = { + let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods, event_sender)); + let inner_ptr = Box::into_raw(inner); + let client_data = inner_ptr as _; + let destroy_callback = + ffi::XIMCallback { client_data, callback: Some(xim_destroy_callback) }; + inner = unsafe { Box::from_raw(inner_ptr) }; + inner.destroy_callback = destroy_callback; + (inner, client_data) + }; + + let xconn = Arc::clone(&inner.xconn); + + let input_method = inner.potential_input_methods.open_im( + &xconn, + Some(&|| { + let _ = unsafe { set_instantiate_callback(&xconn, client_data) }; + }), + ); + + let is_fallback = input_method.is_fallback(); + if let Some(input_method) = input_method.ok() { + inner.is_fallback = is_fallback; + unsafe { + let result = set_destroy_callback(&xconn, input_method.im, &inner) + .map_err(ImeCreationError::SetDestroyCallbackFailed); + if result.is_err() { + let _ = close_im(&xconn, input_method.im); + } + result?; + } + inner.im = Some(input_method); + Ok(Ime { xconn, inner }) + } else { + Err(ImeCreationError::OpenFailure(Box::new(inner.potential_input_methods))) + } + } + + pub fn is_destroyed(&self) -> bool { + self.inner.is_destroyed + } + + // This pattern is used for various methods here: + // Ok(_) indicates that nothing went wrong internally + // Ok(true) indicates that the action was actually performed + // Ok(false) indicates that the action is not presently applicable + pub fn create_context( + &mut self, + window: ffi::Window, + with_ime: bool, + ) -> Result { + let context = if self.is_destroyed() { + // Create empty entry in map, so that when IME is rebuilt, this window has a context. + None + } else { + let im = self.inner.im.as_ref().unwrap(); + + let context = unsafe { + ImeContext::new( + &self.inner.xconn, + im, + window, + None, + self.inner.event_sender.clone(), + with_ime, + )? + }; + + let event = if context.is_allowed() { ImeEvent::Enabled } else { ImeEvent::Disabled }; + self.inner.event_sender.send((window, event)).expect("Failed to send enabled event"); + + Some(context) + }; + + self.inner.contexts.insert(window, context); + Ok(!self.is_destroyed()) + } + + pub fn get_context(&self, window: ffi::Window) -> Option { + if self.is_destroyed() { + return None; + } + if let Some(Some(context)) = self.inner.contexts.get(&window) { + Some(context.ic) + } else { + None + } + } + + pub fn remove_context(&mut self, window: ffi::Window) -> Result { + if let Some(Some(context)) = self.inner.contexts.remove(&window) { + unsafe { + self.inner.destroy_ic_if_necessary(context.ic)?; + } + Ok(true) + } else { + Ok(false) + } + } + + pub fn focus(&mut self, window: ffi::Window) -> Result { + if self.is_destroyed() { + return Ok(false); + } + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + context.focus(&self.xconn).map(|_| true) + } else { + Ok(false) + } + } + + pub fn unfocus(&mut self, window: ffi::Window) -> Result { + if self.is_destroyed() { + return Ok(false); + } + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + context.unfocus(&self.xconn).map(|_| true) + } else { + Ok(false) + } + } + + pub fn send_xim_spot(&mut self, window: ffi::Window, x: i16, y: i16) { + if self.is_destroyed() { + return; + } + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + context.set_spot(&self.xconn, x as _, y as _); + } + } + + pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) { + if self.is_destroyed() { + return; + } + + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + if allowed == context.is_allowed() { + return; + } + } + + // Remove context for that window. + let _ = self.remove_context(window); + + // Create new context supporting IME input. + let _ = self.create_context(window, allowed); + } + + pub fn is_ime_allowed(&self, window: ffi::Window) -> bool { + if self.is_destroyed() { + false + } else if let Some(Some(context)) = self.inner.contexts.get(&window) { + context.is_allowed() + } else { + false + } + } +} + +impl Drop for Ime { + fn drop(&mut self) { + unsafe { + let _ = self.inner.destroy_all_contexts_if_necessary(); + let _ = self.inner.close_im_if_necessary(); + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/mod.rs new file mode 100644 index 0000000..f29b314 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/mod.rs @@ -0,0 +1,1064 @@ +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::CStr; +use std::marker::PhantomData; +use std::mem::MaybeUninit; +use std::ops::Deref; +use std::os::raw::*; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; +use std::sync::{Arc, Weak}; +use std::time::{Duration, Instant}; +use std::{fmt, ptr, slice, str}; + +use calloop::generic::Generic; +use calloop::ping::Ping; +use calloop::{EventLoop as Loop, Readiness}; +use libc::{setlocale, LC_CTYPE}; +use tracing::warn; + +use x11rb::connection::RequestConnection; +use x11rb::errors::{ConnectError, ConnectionError, IdsExhausted, ReplyError}; +use x11rb::protocol::xinput::{self, ConnectionExt as _}; +use x11rb::protocol::xkb; +use x11rb::protocol::xproto::{self, ConnectionExt as _}; +use x11rb::x11_utils::X11Error as LogicalError; +use x11rb::xcb_ffi::ReplyOrIdError; + +use crate::error::{EventLoopError, OsError as RootOsError}; +use crate::event::{Event, StartCause, WindowEvent}; +use crate::event_loop::{ActiveEventLoop as RootAEL, ControlFlow, DeviceEvents, EventLoopClosed}; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::common::xkb::Context; +use crate::platform_impl::platform::{min_timeout, WindowId}; +use crate::platform_impl::{ + ActiveEventLoop as PlatformActiveEventLoop, OsError, PlatformCustomCursor, +}; +use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource, WindowAttributes}; + +mod activation; +mod atoms; +mod dnd; +mod event_processor; +pub mod ffi; +mod ime; +mod monitor; +mod util; +mod window; +mod xdisplay; +mod xsettings; + +pub use util::CustomCursor; + +use atoms::*; +use dnd::{Dnd, DndState}; +use event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; +use ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender}; +pub(crate) use monitor::{MonitorHandle, VideoModeHandle}; +use window::UnownedWindow; +pub(crate) use xdisplay::{XConnection, XError, XNotSupported}; + +// Xinput constants not defined in x11rb +const ALL_DEVICES: u16 = 0; +const ALL_MASTER_DEVICES: u16 = 1; +const ICONIC_STATE: u32 = 3; + +/// The underlying x11rb connection that we are using. +type X11rbConnection = x11rb::xcb_ffi::XCBConnection; + +type X11Source = Generic>; + +struct WakeSender { + sender: Sender, + waker: Ping, +} + +impl Clone for WakeSender { + fn clone(&self) -> Self { + Self { sender: self.sender.clone(), waker: self.waker.clone() } + } +} + +impl WakeSender { + pub fn send(&self, t: T) -> Result<(), EventLoopClosed> { + let res = self.sender.send(t).map_err(|e| EventLoopClosed(e.0)); + if res.is_ok() { + self.waker.ping(); + } + res + } +} + +struct PeekableReceiver { + recv: Receiver, + first: Option, +} + +impl PeekableReceiver { + pub fn from_recv(recv: Receiver) -> Self { + Self { recv, first: None } + } + + pub fn has_incoming(&mut self) -> bool { + if self.first.is_some() { + return true; + } + + match self.recv.try_recv() { + Ok(v) => { + self.first = Some(v); + true + }, + Err(TryRecvError::Empty) => false, + Err(TryRecvError::Disconnected) => { + warn!("Channel was disconnected when checking incoming"); + false + }, + } + } + + pub fn try_recv(&mut self) -> Result { + if let Some(first) = self.first.take() { + return Ok(first); + } + self.recv.try_recv() + } +} + +pub struct ActiveEventLoop { + xconn: Arc, + wm_delete_window: xproto::Atom, + net_wm_ping: xproto::Atom, + ime_sender: ImeSender, + control_flow: Cell, + exit: Cell>, + root: xproto::Window, + ime: Option>, + windows: RefCell>>, + redraw_sender: WakeSender, + activation_sender: WakeSender, + device_events: Cell, +} + +pub struct EventLoop { + loop_running: bool, + event_loop: Loop<'static, EventLoopState>, + waker: calloop::ping::Ping, + event_processor: EventProcessor, + redraw_receiver: PeekableReceiver, + user_receiver: PeekableReceiver, + activation_receiver: PeekableReceiver, + user_sender: Sender, + + /// The current state of the event loop. + state: EventLoopState, +} + +type ActivationToken = (WindowId, crate::event_loop::AsyncRequestSerial); + +struct EventLoopState { + /// The latest readiness state for the x11 file descriptor + x11_readiness: Readiness, +} + +pub struct EventLoopProxy { + user_sender: WakeSender, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { user_sender: self.user_sender.clone() } + } +} + +impl EventLoop { + pub(crate) fn new(xconn: Arc) -> EventLoop { + let root = xconn.default_root().root; + let atoms = xconn.atoms(); + + let wm_delete_window = atoms[WM_DELETE_WINDOW]; + let net_wm_ping = atoms[_NET_WM_PING]; + + let dnd = Dnd::new(Arc::clone(&xconn)) + .expect("Failed to call XInternAtoms when initializing drag and drop"); + + let (ime_sender, ime_receiver) = mpsc::channel(); + let (ime_event_sender, ime_event_receiver) = mpsc::channel(); + // Input methods will open successfully without setting the locale, but it won't be + // possible to actually commit pre-edit sequences. + unsafe { + // Remember default locale to restore it if target locale is unsupported + // by Xlib + let default_locale = setlocale(LC_CTYPE, ptr::null()); + setlocale(LC_CTYPE, b"\0".as_ptr() as *const _); + + // Check if set locale is supported by Xlib. + // If not, calls to some Xlib functions like `XSetLocaleModifiers` + // will fail. + let locale_supported = (xconn.xlib.XSupportsLocale)() == 1; + if !locale_supported { + let unsupported_locale = setlocale(LC_CTYPE, ptr::null()); + warn!( + "Unsupported locale \"{}\". Restoring default locale \"{}\".", + CStr::from_ptr(unsupported_locale).to_string_lossy(), + CStr::from_ptr(default_locale).to_string_lossy() + ); + // Restore default locale + setlocale(LC_CTYPE, default_locale); + } + } + + let ime = Ime::new(Arc::clone(&xconn), ime_event_sender); + if let Err(ImeCreationError::OpenFailure(state)) = ime.as_ref() { + warn!("Failed to open input method: {state:#?}"); + } else if let Err(err) = ime.as_ref() { + warn!("Failed to set input method destruction callback: {err:?}"); + } + + let ime = ime.ok().map(RefCell::new); + + let randr_event_offset = + xconn.select_xrandr_input(root).expect("Failed to query XRandR extension"); + + let xi2ext = xconn + .xcb_connection() + .extension_information(xinput::X11_EXTENSION_NAME) + .expect("Failed to query XInput extension") + .expect("X server missing XInput extension"); + let xkbext = xconn + .xcb_connection() + .extension_information(xkb::X11_EXTENSION_NAME) + .expect("Failed to query XKB extension") + .expect("X server missing XKB extension"); + + // Check for XInput2 support. + xconn + .xcb_connection() + .xinput_xi_query_version(2, 3) + .expect("Failed to send XInput2 query version request") + .reply() + .expect("Error while checking for XInput2 query version reply"); + + xconn.update_cached_wm_info(root); + + // Create an event loop. + let event_loop = + Loop::::try_new().expect("Failed to initialize the event loop"); + let handle = event_loop.handle(); + + // Create the X11 event dispatcher. + let source = X11Source::new( + // SAFETY: xcb owns the FD and outlives the source. + unsafe { BorrowedFd::borrow_raw(xconn.xcb_connection().as_raw_fd()) }, + calloop::Interest::READ, + calloop::Mode::Level, + ); + handle + .insert_source(source, |readiness, _, state| { + state.x11_readiness = readiness; + Ok(calloop::PostAction::Continue) + }) + .expect("Failed to register the X11 event dispatcher"); + + let (waker, waker_source) = + calloop::ping::make_ping().expect("Failed to create event loop waker"); + event_loop + .handle() + .insert_source(waker_source, move |_, _, _| { + // No extra handling is required, we just need to wake-up. + }) + .expect("Failed to register the event loop waker source"); + + // Create a channel for handling redraw requests. + let (redraw_sender, redraw_channel) = mpsc::channel(); + + // Create a channel for sending activation tokens. + let (activation_token_sender, activation_token_channel) = mpsc::channel(); + + // Create a channel for sending user events. + let (user_sender, user_channel) = mpsc::channel(); + + let xkb_context = + Context::from_x11_xkb(xconn.xcb_connection().get_raw_xcb_connection()).unwrap(); + + let mut xmodmap = util::ModifierKeymap::new(); + xmodmap.reload_from_x_connection(&xconn); + + let window_target = ActiveEventLoop { + ime, + root, + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), + windows: Default::default(), + ime_sender, + xconn, + wm_delete_window, + net_wm_ping, + redraw_sender: WakeSender { + sender: redraw_sender, // not used again so no clone + waker: waker.clone(), + }, + activation_sender: WakeSender { + sender: activation_token_sender, // not used again so no clone + waker: waker.clone(), + }, + device_events: Default::default(), + }; + + // Set initial device event filter. + window_target.update_listen_device_events(true); + + let root_window_target = + RootAEL { p: PlatformActiveEventLoop::X(window_target), _marker: PhantomData }; + + let event_processor = EventProcessor { + target: root_window_target, + dnd, + devices: Default::default(), + randr_event_offset, + ime_receiver, + ime_event_receiver, + xi2ext, + xfiltered_modifiers: VecDeque::with_capacity(MAX_MOD_REPLAY_LEN), + xmodmap, + xkbext, + xkb_context, + num_touch: 0, + held_key_press: None, + first_touch: None, + active_window: None, + modifiers: Default::default(), + is_composing: false, + }; + + // Register for device hotplug events + // (The request buffer is flushed during `init_device`) + let xconn = &EventProcessor::window_target(&event_processor.target).xconn; + + xconn + .select_xinput_events( + root, + ALL_DEVICES, + x11rb::protocol::xinput::XIEventMask::HIERARCHY, + ) + .expect_then_ignore_error("Failed to register for XInput2 device hotplug events"); + + xconn + .select_xkb_events( + 0x100, // Use the "core keyboard device" + xkb::EventType::NEW_KEYBOARD_NOTIFY + | xkb::EventType::MAP_NOTIFY + | xkb::EventType::STATE_NOTIFY, + ) + .unwrap(); + + event_processor.init_device(ALL_DEVICES); + + EventLoop { + loop_running: false, + event_loop, + waker, + event_processor, + redraw_receiver: PeekableReceiver::from_recv(redraw_channel), + activation_receiver: PeekableReceiver::from_recv(activation_token_channel), + user_receiver: PeekableReceiver::from_recv(user_channel), + user_sender, + state: EventLoopState { x11_readiness: Readiness::EMPTY }, + } + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + user_sender: WakeSender { sender: self.user_sender.clone(), waker: self.waker.clone() }, + } + } + + pub(crate) fn window_target(&self) -> &RootAEL { + &self.event_processor.target + } + + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootAEL), + { + let exit = loop { + match self.pump_events(None, &mut event_handler) { + PumpStatus::Exit(0) => { + break Ok(()); + }, + PumpStatus::Exit(code) => { + break Err(EventLoopError::ExitFailure(code)); + }, + _ => { + continue; + }, + } + }; + + // Applications aren't allowed to carry windows between separate + // `run_on_demand` calls but if they have only just dropped their + // windows we need to make sure those last requests are sent to the + // X Server. + let wt = EventProcessor::window_target(&self.event_processor.target); + wt.x_connection().sync_with_server().map_err(|x_err| { + EventLoopError::Os(os_error!(OsError::XError(Arc::new(X11Error::Xlib(x_err))))) + })?; + + exit + } + + pub fn pump_events(&mut self, timeout: Option, mut callback: F) -> PumpStatus + where + F: FnMut(Event, &RootAEL), + { + if !self.loop_running { + self.loop_running = true; + + // run the initial loop iteration + self.single_iteration(&mut callback, StartCause::Init); + } + + // Consider the possibility that the `StartCause::Init` iteration could + // request to Exit. + if !self.exiting() { + self.poll_events_with_timeout(timeout, &mut callback); + } + if let Some(code) = self.exit_code() { + self.loop_running = false; + + callback(Event::LoopExiting, self.window_target()); + + PumpStatus::Exit(code) + } else { + PumpStatus::Continue + } + } + + fn has_pending(&mut self) -> bool { + self.event_processor.poll() + || self.user_receiver.has_incoming() + || self.redraw_receiver.has_incoming() + } + + pub fn poll_events_with_timeout(&mut self, mut timeout: Option, mut callback: F) + where + F: FnMut(Event, &RootAEL), + { + let start = Instant::now(); + + let has_pending = self.has_pending(); + + timeout = if has_pending { + // If we already have work to do then we don't want to block on the next poll. + Some(Duration::ZERO) + } else { + let control_flow_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + Some(wait_deadline.saturating_duration_since(start)) + }, + }; + + min_timeout(control_flow_timeout, timeout) + }; + + self.state.x11_readiness = Readiness::EMPTY; + if let Err(error) = + self.event_loop.dispatch(timeout, &mut self.state).map_err(std::io::Error::from) + { + tracing::error!("Failed to poll for events: {error:?}"); + let exit_code = error.raw_os_error().unwrap_or(1); + self.set_exit_code(exit_code); + return; + } + + // NB: `StartCause::Init` is handled as a special case and doesn't need + // to be considered here + let cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { start, requested_resume: None }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() < deadline { + StartCause::WaitCancelled { start, requested_resume: Some(deadline) } + } else { + StartCause::ResumeTimeReached { start, requested_resume: deadline } + } + }, + }; + + // False positive / spurious wake ups could lead to us spamming + // redundant iterations of the event loop with no new events to + // dispatch. + // + // If there's no readable event source then we just double check if we + // have any pending `_receiver` events and if not we return without + // running a loop iteration. + // If we don't have any pending `_receiver` + if !self.has_pending() + && !matches!(&cause, StartCause::ResumeTimeReached { .. } | StartCause::Poll) + && timeout.is_none() + { + return; + } + + self.single_iteration(&mut callback, cause); + } + + fn single_iteration(&mut self, callback: &mut F, cause: StartCause) + where + F: FnMut(Event, &RootAEL), + { + callback(Event::NewEvents(cause), &self.event_processor.target); + + // NB: For consistency all platforms must emit a 'resumed' event even though X11 + // applications don't themselves have a formal suspend/resume lifecycle. + if cause == StartCause::Init { + callback(Event::Resumed, &self.event_processor.target); + } + + // Process all pending events + self.drain_events(callback); + + // Empty activation tokens. + while let Ok((window_id, serial)) = self.activation_receiver.try_recv() { + let token = self.event_processor.with_window(window_id.0 as xproto::Window, |window| { + window.generate_activation_token() + }); + + match token { + Some(Ok(token)) => { + let event = Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::ActivationTokenDone { + serial, + token: crate::window::ActivationToken::from_raw(token), + }, + }; + callback(event, &self.event_processor.target) + }, + Some(Err(e)) => { + tracing::error!("Failed to get activation token: {}", e); + }, + None => {}, + } + } + + // Empty the user event buffer + { + while let Ok(event) = self.user_receiver.try_recv() { + callback(Event::UserEvent(event), &self.event_processor.target); + } + } + + // Empty the redraw requests + { + let mut windows = HashSet::new(); + + while let Ok(window_id) = self.redraw_receiver.try_recv() { + windows.insert(window_id); + } + + for window_id in windows { + let window_id = crate::window::WindowId(window_id); + callback( + Event::WindowEvent { window_id, event: WindowEvent::RedrawRequested }, + &self.event_processor.target, + ); + } + } + + // This is always the last event we dispatch before poll again + { + callback(Event::AboutToWait, &self.event_processor.target); + } + } + + fn drain_events(&mut self, callback: &mut F) + where + F: FnMut(Event, &RootAEL), + { + let mut xev = MaybeUninit::uninit(); + + while unsafe { self.event_processor.poll_one_event(xev.as_mut_ptr()) } { + let mut xev = unsafe { xev.assume_init() }; + self.event_processor.process_event(&mut xev, |window_target, event| { + if let Event::WindowEvent { + window_id: crate::window::WindowId(wid), + event: WindowEvent::RedrawRequested, + } = event + { + let window_target = EventProcessor::window_target(window_target); + window_target.redraw_sender.send(wid).unwrap(); + } else { + callback(event, window_target); + } + }); + } + } + + fn control_flow(&self) -> ControlFlow { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.control_flow() + } + + fn exiting(&self) -> bool { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.exiting() + } + + fn set_exit_code(&self, code: i32) { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.set_exit_code(code); + } + + fn exit_code(&self) -> Option { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.exit_code() + } +} + +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() + } +} + +impl ActiveEventLoop { + /// Returns the `XConnection` of this events loop. + #[inline] + pub(crate) fn x_connection(&self) -> &Arc { + &self.xconn + } + + pub fn available_monitors(&self) -> impl Iterator { + self.xconn.available_monitors().into_iter().flatten() + } + + pub fn primary_monitor(&self) -> Option { + self.xconn.primary_monitor().ok() + } + + pub(crate) fn create_custom_cursor(&self, cursor: CustomCursorSource) -> RootCustomCursor { + RootCustomCursor { inner: PlatformCustomCursor::X(CustomCursor::new(self, cursor.inner)) } + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.device_events.set(allowed); + } + + /// Update the device event based on window focus. + pub fn update_listen_device_events(&self, focus: bool) { + let device_events = self.device_events.get() == DeviceEvents::Always + || (focus && self.device_events.get() == DeviceEvents::WhenFocused); + + let mut mask = xinput::XIEventMask::from(0u32); + if device_events { + mask = xinput::XIEventMask::RAW_MOTION + | xinput::XIEventMask::RAW_BUTTON_PRESS + | xinput::XIEventMask::RAW_BUTTON_RELEASE + | xinput::XIEventMask::RAW_KEY_PRESS + | xinput::XIEventMask::RAW_KEY_RELEASE; + } + + self.xconn + .select_xinput_events(self.root, ALL_MASTER_DEVICES, mask) + .expect_then_ignore_error("Failed to update device event filter"); + } + + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + let mut display_handle = rwh_05::XlibDisplayHandle::empty(); + display_handle.display = self.xconn.display as *mut _; + display_handle.screen = self.xconn.default_screen_index() as c_int; + display_handle.into() + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + let display_handle = rwh_06::XlibDisplayHandle::new( + // SAFETY: display will never be null + Some( + std::ptr::NonNull::new(self.xconn.display as *mut _) + .expect("X11 display should never be null"), + ), + self.xconn.default_screen_index() as c_int, + ); + Ok(display_handle.into()) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(Some(0)) + } + + pub(crate) fn clear_exit(&self) { + self.exit.set(None) + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get().is_some() + } + + pub(crate) fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub(crate) fn exit_code(&self) -> Option { + self.exit.get() + } +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.user_sender.send(event).map_err(|e| EventLoopClosed(e.0)) + } +} + +struct DeviceInfo<'a> { + xconn: &'a XConnection, + info: *const ffi::XIDeviceInfo, + count: usize, +} + +impl<'a> DeviceInfo<'a> { + fn get(xconn: &'a XConnection, device: c_int) -> Option { + unsafe { + let mut count = 0; + let info = (xconn.xinput2.XIQueryDevice)(xconn.display, device, &mut count); + xconn.check_errors().ok()?; + + if info.is_null() || count == 0 { + None + } else { + Some(DeviceInfo { xconn, info, count: count as usize }) + } + } + } +} + +impl Drop for DeviceInfo<'_> { + fn drop(&mut self) { + assert!(!self.info.is_null()); + unsafe { (self.xconn.xinput2.XIFreeDeviceInfo)(self.info as *mut _) }; + } +} + +impl Deref for DeviceInfo<'_> { + type Target = [ffi::XIDeviceInfo]; + + fn deref(&self) -> &Self::Target { + unsafe { slice::from_raw_parts(self.info, self.count) } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(xinput::DeviceId); + +impl DeviceId { + #[allow(unused)] + pub const fn dummy() -> Self { + DeviceId(0) + } +} + +pub(crate) struct Window(Arc); + +impl Deref for Window { + type Target = UnownedWindow; + + #[inline] + fn deref(&self) -> &UnownedWindow { + &self.0 + } +} + +impl Window { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + attribs: WindowAttributes, + ) -> Result { + let window = Arc::new(UnownedWindow::new(event_loop, attribs)?); + event_loop.windows.borrow_mut().insert(window.id(), Arc::downgrade(&window)); + Ok(Window(window)) + } +} + +impl Drop for Window { + fn drop(&mut self) { + let window = self.deref(); + let xconn = &window.xconn; + + if let Ok(c) = xconn.xcb_connection().destroy_window(window.id().0 as xproto::Window) { + c.ignore_error(); + } + } +} + +/// Generic sum error type for X11 errors. +#[derive(Debug)] +pub enum X11Error { + /// An error from the Xlib library. + Xlib(XError), + + /// An error that occurred while trying to connect to the X server. + Connect(ConnectError), + + /// An error that occurred over the connection medium. + Connection(ConnectionError), + + /// An error that occurred logically on the X11 end. + X11(LogicalError), + + /// The XID range has been exhausted. + XidsExhausted(IdsExhausted), + + /// Got `null` from an Xlib function without a reason. + UnexpectedNull(&'static str), + + /// Got an invalid activation token. + InvalidActivationToken(Vec), + + /// An extension that we rely on is not available. + MissingExtension(&'static str), + + /// Could not find a matching X11 visual for this visualid + NoSuchVisual(xproto::Visualid), + + /// Unable to parse xsettings. + XsettingsParse(xsettings::ParserError), + + /// Failed to get property. + GetProperty(util::GetPropertyError), +} + +impl fmt::Display for X11Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + X11Error::Xlib(e) => write!(f, "Xlib error: {e}"), + X11Error::Connect(e) => write!(f, "X11 connection error: {e}"), + X11Error::Connection(e) => write!(f, "X11 connection error: {e}"), + X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {e}"), + X11Error::GetProperty(e) => write!(f, "Failed to get X property {e}"), + X11Error::X11(e) => write!(f, "X11 error: {e:?}"), + X11Error::UnexpectedNull(s) => write!(f, "Xlib function returned null: {s}"), + X11Error::InvalidActivationToken(s) => write!( + f, + "Invalid activation token: {}", + std::str::from_utf8(s).unwrap_or("") + ), + X11Error::MissingExtension(s) => write!(f, "Missing X11 extension: {s}"), + X11Error::NoSuchVisual(visualid) => { + write!(f, "Could not find a matching X11 visual for ID `{visualid:x}`") + }, + X11Error::XsettingsParse(err) => { + write!(f, "Failed to parse xsettings: {err:?}") + }, + } + } +} + +impl std::error::Error for X11Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + X11Error::Xlib(e) => Some(e), + X11Error::Connect(e) => Some(e), + X11Error::Connection(e) => Some(e), + X11Error::XidsExhausted(e) => Some(e), + _ => None, + } + } +} + +impl From for X11Error { + fn from(e: XError) -> Self { + X11Error::Xlib(e) + } +} + +impl From for X11Error { + fn from(e: ConnectError) -> Self { + X11Error::Connect(e) + } +} + +impl From for X11Error { + fn from(e: ConnectionError) -> Self { + X11Error::Connection(e) + } +} + +impl From for X11Error { + fn from(e: LogicalError) -> Self { + X11Error::X11(e) + } +} + +impl From for X11Error { + fn from(value: ReplyError) -> Self { + match value { + ReplyError::ConnectionError(e) => e.into(), + ReplyError::X11Error(e) => e.into(), + } + } +} + +impl From for X11Error { + fn from(value: ime::ImeContextCreationError) -> Self { + match value { + ime::ImeContextCreationError::XError(e) => e.into(), + ime::ImeContextCreationError::Null => Self::UnexpectedNull("XOpenIM"), + } + } +} + +impl From for X11Error { + fn from(value: ReplyOrIdError) -> Self { + match value { + ReplyOrIdError::ConnectionError(e) => e.into(), + ReplyOrIdError::X11Error(e) => e.into(), + ReplyOrIdError::IdsExhausted => Self::XidsExhausted(IdsExhausted), + } + } +} + +impl From for X11Error { + fn from(value: xsettings::ParserError) -> Self { + Self::XsettingsParse(value) + } +} + +impl From for X11Error { + fn from(value: util::GetPropertyError) -> Self { + Self::GetProperty(value) + } +} + +/// Type alias for a void cookie. +type VoidCookie<'a> = x11rb::cookie::VoidCookie<'a, X11rbConnection>; + +/// Extension trait for `Result`. +trait CookieResultExt { + /// Unwrap the send error and ignore the result. + fn expect_then_ignore_error(self, msg: &str); +} + +impl CookieResultExt for Result, E> { + fn expect_then_ignore_error(self, msg: &str) { + self.expect(msg).ignore_error() + } +} + +fn mkwid(w: xproto::Window) -> crate::window::WindowId { + crate::window::WindowId(crate::platform_impl::platform::WindowId(w as _)) +} +fn mkdid(w: xinput::DeviceId) -> crate::event::DeviceId { + crate::event::DeviceId(crate::platform_impl::DeviceId::X(DeviceId(w))) +} + +#[derive(Debug)] +pub struct Device { + _name: String, + scroll_axes: Vec<(i32, ScrollAxis)>, + // For master devices, this is the paired device (pointer <-> keyboard). + // For slave devices, this is the master. + attachment: c_int, +} + +#[derive(Debug, Copy, Clone)] +struct ScrollAxis { + increment: f64, + orientation: ScrollOrientation, + position: f64, +} + +#[derive(Debug, Copy, Clone)] +enum ScrollOrientation { + Vertical, + Horizontal, +} + +impl Device { + fn new(info: &ffi::XIDeviceInfo) -> Self { + let name = unsafe { CStr::from_ptr(info.name).to_string_lossy() }; + let mut scroll_axes = Vec::new(); + + if Device::physical_device(info) { + // Identify scroll axes + for &class_ptr in Device::classes(info) { + let ty = unsafe { (*class_ptr)._type }; + if ty == ffi::XIScrollClass { + let info = unsafe { &*(class_ptr as *const ffi::XIScrollClassInfo) }; + scroll_axes.push((info.number, ScrollAxis { + increment: info.increment, + orientation: match info.scroll_type { + ffi::XIScrollTypeHorizontal => ScrollOrientation::Horizontal, + ffi::XIScrollTypeVertical => ScrollOrientation::Vertical, + _ => unreachable!(), + }, + position: 0.0, + })); + } + } + } + + let mut device = + Device { _name: name.into_owned(), scroll_axes, attachment: info.attachment }; + device.reset_scroll_position(info); + device + } + + fn reset_scroll_position(&mut self, info: &ffi::XIDeviceInfo) { + if Device::physical_device(info) { + for &class_ptr in Device::classes(info) { + let ty = unsafe { (*class_ptr)._type }; + if ty == ffi::XIValuatorClass { + let info = unsafe { &*(class_ptr as *const ffi::XIValuatorClassInfo) }; + if let Some(&mut (_, ref mut axis)) = + self.scroll_axes.iter_mut().find(|&&mut (axis, _)| axis == info.number) + { + axis.position = info.value; + } + } + } + } + } + + #[inline] + fn physical_device(info: &ffi::XIDeviceInfo) -> bool { + info._use == ffi::XISlaveKeyboard + || info._use == ffi::XISlavePointer + || info._use == ffi::XIFloatingSlave + } + + #[inline] + fn classes(info: &ffi::XIDeviceInfo) -> &[*const ffi::XIAnyClassInfo] { + unsafe { + slice::from_raw_parts( + info.classes as *const *const ffi::XIAnyClassInfo, + info.num_classes as usize, + ) + } + } +} + +/// Convert the raw X11 representation for a 32-bit floating point to a double. +#[inline] +fn xinput_fp1616_to_float(fp: xinput::Fp1616) -> f64 { + (fp as f64) / ((1 << 16) as f64) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/monitor.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/monitor.rs new file mode 100644 index 0000000..1964bc9 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/monitor.rs @@ -0,0 +1,355 @@ +use super::{util, X11Error, XConnection}; +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::platform_impl::VideoModeHandle as PlatformVideoModeHandle; +use x11rb::connection::RequestConnection; +use x11rb::protocol::randr::{self, ConnectionExt as _}; +use x11rb::protocol::xproto; + +// Used for testing. This should always be committed as false. +const DISABLE_MONITOR_LIST_CACHING: bool = false; + +impl XConnection { + pub fn invalidate_cached_monitor_list(&self) -> Option> { + // We update this lazily. + self.monitor_handles.lock().unwrap().take() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VideoModeHandle { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + pub(crate) native_mode: randr::Mode, + pub(crate) monitor: Option, +} + +impl VideoModeHandle { + #[inline] + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + #[inline] + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone().unwrap() + } +} + +#[derive(Debug, Clone)] +pub struct MonitorHandle { + /// The actual id + pub(crate) id: randr::Crtc, + /// The name of the monitor + pub(crate) name: String, + /// The size of the monitor + dimensions: (u32, u32), + /// The position of the monitor in the X screen + position: (i32, i32), + /// If the monitor is the primary one + primary: bool, + /// The refresh rate used by monitor. + refresh_rate_millihertz: Option, + /// The DPI scale factor + pub(crate) scale_factor: f64, + /// Used to determine which windows are on this monitor + pub(crate) rect: util::AaRect, + /// Supported video modes on this monitor + video_modes: Vec, +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +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 { + self.id.cmp(&other.id) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +#[inline] +pub fn mode_refresh_rate_millihertz(mode: &randr::ModeInfo) -> Option { + if mode.dot_clock > 0 && mode.htotal > 0 && mode.vtotal > 0 { + #[allow(clippy::unnecessary_cast)] + Some((mode.dot_clock as u64 * 1000 / (mode.htotal as u64 * mode.vtotal as u64)) as u32) + } else { + None + } +} + +impl MonitorHandle { + fn new( + xconn: &XConnection, + resources: &ScreenResources, + id: randr::Crtc, + crtc: &randr::GetCrtcInfoReply, + primary: bool, + ) -> Option { + let (name, scale_factor, video_modes) = xconn.get_output_info(resources, crtc)?; + let dimensions = (crtc.width as u32, crtc.height as u32); + let position = (crtc.x as i32, crtc.y as i32); + + // Get the refresh rate of the current video mode. + let current_mode = crtc.mode; + let screen_modes = resources.modes(); + let refresh_rate_millihertz = screen_modes + .iter() + .find(|mode| mode.id == current_mode) + .and_then(mode_refresh_rate_millihertz); + + let rect = util::AaRect::new(position, dimensions); + + Some(MonitorHandle { + id, + name, + refresh_rate_millihertz, + scale_factor, + dimensions, + position, + primary, + rect, + video_modes, + }) + } + + pub fn dummy() -> Self { + MonitorHandle { + id: 0, + name: "".into(), + scale_factor: 1.0, + dimensions: (1, 1), + position: (0, 0), + refresh_rate_millihertz: None, + primary: true, + rect: util::AaRect::new((0, 0), (1, 1)), + video_modes: Vec::new(), + } + } + + pub(crate) fn is_dummy(&self) -> bool { + // Zero is an invalid XID value; no real monitor will have it + self.id == 0 + } + + pub fn name(&self) -> Option { + Some(self.name.clone()) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + self.id as _ + } + + pub fn size(&self) -> PhysicalSize { + self.dimensions.into() + } + + pub fn position(&self) -> PhysicalPosition { + self.position.into() + } + + pub fn refresh_rate_millihertz(&self) -> Option { + self.refresh_rate_millihertz + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } + + #[inline] + pub fn video_modes(&self) -> impl Iterator { + let monitor = self.clone(); + self.video_modes.clone().into_iter().map(move |mut x| { + x.monitor = Some(monitor.clone()); + PlatformVideoModeHandle::X(x) + }) + } +} + +impl XConnection { + pub fn get_monitor_for_window( + &self, + window_rect: Option, + ) -> Result { + let monitors = self.available_monitors()?; + + if monitors.is_empty() { + // Return a dummy monitor to avoid panicking + return Ok(MonitorHandle::dummy()); + } + + let default = monitors.first().unwrap(); + + let window_rect = match window_rect { + Some(rect) => rect, + None => return Ok(default.to_owned()), + }; + + let mut largest_overlap = 0; + let mut matched_monitor = default; + for monitor in &monitors { + let overlapping_area = window_rect.get_overlapping_area(&monitor.rect); + if overlapping_area > largest_overlap { + largest_overlap = overlapping_area; + matched_monitor = monitor; + } + } + + Ok(matched_monitor.to_owned()) + } + + fn query_monitor_list(&self) -> Result, X11Error> { + let root = self.default_root(); + let resources = + ScreenResources::from_connection(self.xcb_connection(), root, self.randr_version())?; + + // Pipeline all of the get-crtc requests. + let mut crtc_cookies = Vec::with_capacity(resources.crtcs().len()); + for &crtc in resources.crtcs() { + crtc_cookies + .push(self.xcb_connection().randr_get_crtc_info(crtc, x11rb::CURRENT_TIME)?); + } + + // Do this here so we do all of our requests in one shot. + let primary = self.xcb_connection().randr_get_output_primary(root.root)?.reply()?.output; + + let mut crtc_infos = Vec::with_capacity(crtc_cookies.len()); + for cookie in crtc_cookies { + let reply = cookie.reply()?; + crtc_infos.push(reply); + } + + let mut has_primary = false; + let mut available_monitors = Vec::with_capacity(resources.crtcs().len()); + for (crtc_id, crtc) in resources.crtcs().iter().zip(crtc_infos.iter()) { + if crtc.width == 0 || crtc.height == 0 || crtc.outputs.is_empty() { + continue; + } + + let is_primary = crtc.outputs[0] == primary; + has_primary |= is_primary; + let monitor = MonitorHandle::new(self, &resources, *crtc_id, crtc, is_primary); + available_monitors.extend(monitor); + } + + // If we don't have a primary monitor, just pick one ourselves! + if !has_primary { + if let Some(ref mut fallback) = available_monitors.first_mut() { + // Setting this here will come in handy if we ever add an `is_primary` method. + fallback.primary = true; + } + } + + Ok(available_monitors) + } + + pub fn available_monitors(&self) -> Result, X11Error> { + let mut monitors_lock = self.monitor_handles.lock().unwrap(); + match *monitors_lock { + Some(ref monitors) => Ok(monitors.clone()), + None => { + let monitors = self.query_monitor_list()?; + if !DISABLE_MONITOR_LIST_CACHING { + *monitors_lock = Some(monitors.clone()); + } + Ok(monitors) + }, + } + } + + #[inline] + pub fn primary_monitor(&self) -> Result { + Ok(self + .available_monitors()? + .into_iter() + .find(|monitor| monitor.primary) + .unwrap_or_else(MonitorHandle::dummy)) + } + + pub fn select_xrandr_input(&self, root: xproto::Window) -> Result { + use randr::NotifyMask; + + // Get extension info. + let info = self + .xcb_connection() + .extension_information(randr::X11_EXTENSION_NAME)? + .ok_or(X11Error::MissingExtension(randr::X11_EXTENSION_NAME))?; + + // Select input data. + let event_mask = + NotifyMask::CRTC_CHANGE | NotifyMask::OUTPUT_PROPERTY | NotifyMask::SCREEN_CHANGE; + self.xcb_connection().randr_select_input(root, event_mask)?; + + Ok(info.first_event) + } +} + +pub struct ScreenResources { + /// List of attached modes. + modes: Vec, + + /// List of attached CRTCs. + crtcs: Vec, +} + +impl ScreenResources { + pub(crate) fn modes(&self) -> &[randr::ModeInfo] { + &self.modes + } + + pub(crate) fn crtcs(&self) -> &[randr::Crtc] { + &self.crtcs + } + + pub(crate) fn from_connection( + conn: &impl x11rb::connection::Connection, + root: &x11rb::protocol::xproto::Screen, + (major_version, minor_version): (u32, u32), + ) -> Result { + if (major_version == 1 && minor_version >= 3) || major_version > 1 { + let reply = conn.randr_get_screen_resources_current(root.root)?.reply()?; + Ok(Self::from_get_screen_resources_current_reply(reply)) + } else { + let reply = conn.randr_get_screen_resources(root.root)?.reply()?; + Ok(Self::from_get_screen_resources_reply(reply)) + } + } + + pub(crate) fn from_get_screen_resources_reply(reply: randr::GetScreenResourcesReply) -> Self { + Self { modes: reply.modes, crtcs: reply.crtcs } + } + + pub(crate) fn from_get_screen_resources_current_reply( + reply: randr::GetScreenResourcesCurrentReply, + ) -> Self { + Self { modes: reply.modes, crtcs: reply.crtcs } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/tests/xsettings.dat b/third_party/winit-0.30.13/src/platform_impl/linux/x11/tests/xsettings.dat new file mode 100644 index 0000000..e04eeb1 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/tests/xsettings.dat @@ -0,0 +1 @@ +0x6c,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x22,0x00,0x00,0x00,0x00,0x00,0x0b,0x00,0x58,0x66,0x74,0x2f,0x48,0x69,0x6e,0x74,0x69,0x6e,0x67,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x47,0x74,0x6b,0x2f,0x44,0x69,0x61,0x6c,0x6f,0x67,0x73,0x55,0x73,0x65,0x48,0x65,0x61,0x64,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x0c,0x00,0x47,0x74,0x6b,0x2f,0x46,0x6f,0x6e,0x74,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x0b,0x00,0x00,0x00,0x4e,0x6f,0x74,0x6f,0x20,0x53,0x61,0x6e,0x73,0x20,0x39,0x00,0x01,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x4c,0x63,0x64,0x66,0x69,0x6c,0x74,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0x00,0x00,0x00,0x6c,0x63,0x64,0x64,0x65,0x66,0x61,0x75,0x6c,0x74,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x4b,0x65,0x79,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x48,0x69,0x6e,0x74,0x53,0x74,0x79,0x6c,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0x00,0x00,0x00,0x68,0x69,0x6e,0x74,0x73,0x6c,0x69,0x67,0x68,0x74,0x00,0x00,0x01,0x00,0x11,0x00,0x4e,0x65,0x74,0x2f,0x49,0x63,0x6f,0x6e,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x00,0x00,0x65,0x6c,0x65,0x6d,0x65,0x6e,0x74,0x61,0x72,0x79,0x2d,0x78,0x66,0x63,0x65,0x2d,0x64,0x61,0x72,0x6b,0x00,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x41,0x6e,0x74,0x69,0x61,0x6c,0x69,0x61,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x08,0x00,0x58,0x66,0x74,0x2f,0x52,0x47,0x42,0x41,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x72,0x67,0x62,0x00,0x00,0x00,0x13,0x00,0x4e,0x65,0x74,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x42,0x6c,0x69,0x6e,0x6b,0x54,0x69,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0xb0,0x04,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x54,0x68,0x65,0x6d,0x65,0x53,0x69,0x7a,0x65,0x00,0x00,0x00,0x00,0x00,0x18,0x00,0x00,0x00,0x01,0x00,0x15,0x00,0x4e,0x65,0x74,0x2f,0x46,0x61,0x6c,0x6c,0x62,0x61,0x63,0x6b,0x49,0x63,0x6f,0x6e,0x54,0x68,0x65,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x67,0x6e,0x6f,0x6d,0x65,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x54,0x6f,0x6f,0x6c,0x62,0x61,0x72,0x53,0x74,0x79,0x6c,0x65,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x69,0x63,0x6f,0x6e,0x73,0x00,0x00,0x00,0x01,0x00,0x12,0x00,0x4e,0x65,0x74,0x2f,0x53,0x6f,0x75,0x6e,0x64,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x64,0x65,0x66,0x61,0x75,0x6c,0x74,0x00,0x00,0x00,0x15,0x00,0x4e,0x65,0x74,0x2f,0x45,0x6e,0x61,0x62,0x6c,0x65,0x45,0x76,0x65,0x6e,0x74,0x53,0x6f,0x75,0x6e,0x64,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0x00,0x4e,0x65,0x74,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x42,0x6c,0x69,0x6e,0x6b,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x43,0x6f,0x6c,0x6f,0x72,0x50,0x61,0x6c,0x65,0x74,0x74,0x65,0x00,0x00,0x00,0x00,0x94,0x00,0x00,0x00,0x62,0x6c,0x61,0x63,0x6b,0x3a,0x77,0x68,0x69,0x74,0x65,0x3a,0x67,0x72,0x61,0x79,0x35,0x30,0x3a,0x72,0x65,0x64,0x3a,0x70,0x75,0x72,0x70,0x6c,0x65,0x3a,0x62,0x6c,0x75,0x65,0x3a,0x6c,0x69,0x67,0x68,0x74,0x20,0x62,0x6c,0x75,0x65,0x3a,0x67,0x72,0x65,0x65,0x6e,0x3a,0x79,0x65,0x6c,0x6c,0x6f,0x77,0x3a,0x6f,0x72,0x61,0x6e,0x67,0x65,0x3a,0x6c,0x61,0x76,0x65,0x6e,0x64,0x65,0x72,0x3a,0x62,0x72,0x6f,0x77,0x6e,0x3a,0x67,0x6f,0x6c,0x64,0x65,0x6e,0x72,0x6f,0x64,0x34,0x3a,0x64,0x6f,0x64,0x67,0x65,0x72,0x20,0x62,0x6c,0x75,0x65,0x3a,0x70,0x69,0x6e,0x6b,0x3a,0x6c,0x69,0x67,0x68,0x74,0x20,0x67,0x72,0x65,0x65,0x6e,0x3a,0x67,0x72,0x61,0x79,0x31,0x30,0x3a,0x67,0x72,0x61,0x79,0x33,0x30,0x3a,0x67,0x72,0x61,0x79,0x37,0x35,0x3a,0x67,0x72,0x61,0x79,0x39,0x30,0x00,0x00,0x13,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6f,0x75,0x62,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x54,0x69,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x90,0x01,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x61,0x6e,0x43,0x68,0x61,0x6e,0x67,0x65,0x41,0x63,0x63,0x65,0x6c,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x65,0x6e,0x75,0x42,0x61,0x72,0x41,0x63,0x63,0x65,0x6c,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x46,0x31,0x30,0x00,0x01,0x00,0x0d,0x00,0x4e,0x65,0x74,0x2f,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x47,0x72,0x65,0x79,0x62,0x69,0x72,0x64,0x01,0x00,0x17,0x00,0x47,0x74,0x6b,0x2f,0x54,0x69,0x74,0x6c,0x65,0x62,0x61,0x72,0x4d,0x69,0x64,0x64,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x6c,0x6f,0x77,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x42,0x75,0x74,0x74,0x6f,0x6e,0x49,0x6d,0x61,0x67,0x65,0x73,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x17,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6f,0x75,0x62,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x44,0x69,0x73,0x74,0x61,0x6e,0x63,0x65,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x01,0x00,0x15,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x46,0x6f,0x6e,0x74,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0c,0x00,0x00,0x00,0x4d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x20,0x31,0x30,0x00,0x00,0x07,0x00,0x58,0x66,0x74,0x2f,0x44,0x50,0x49,0x00,0x02,0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x01,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x09,0x00,0x00,0x00,0x44,0x4d,0x5a,0x2d,0x57,0x68,0x69,0x74,0x65,0x00,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x54,0x6f,0x6f,0x6c,0x62,0x61,0x72,0x49,0x63,0x6f,0x6e,0x53,0x69,0x7a,0x65,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6e,0x64,0x44,0x72,0x61,0x67,0x54,0x68,0x72,0x65,0x73,0x68,0x6f,0x6c,0x64,0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x01,0x00,0x14,0x00,0x47,0x74,0x6b,0x2f,0x44,0x65,0x63,0x6f,0x72,0x61,0x74,0x69,0x6f,0x6e,0x4c,0x61,0x79,0x6f,0x75,0x74,0x00,0x00,0x00,0x00,0x1c,0x00,0x00,0x00,0x6d,0x65,0x6e,0x75,0x3a,0x6d,0x69,0x6e,0x69,0x6d,0x69,0x7a,0x65,0x2c,0x6d,0x61,0x78,0x69,0x6d,0x69,0x7a,0x65,0x2c,0x63,0x6c,0x6f,0x73,0x65,0x00,0x00,0x1d,0x00,0x4e,0x65,0x74,0x2f,0x45,0x6e,0x61,0x62,0x6c,0x65,0x49,0x6e,0x70,0x75,0x74,0x46,0x65,0x65,0x64,0x62,0x61,0x63,0x6b,0x53,0x6f,0x75,0x6e,0x64,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x17,0x00,0x47,0x64,0x6b,0x2f,0x57,0x69,0x6e,0x64,0x6f,0x77,0x53,0x63,0x61,0x6c,0x69,0x6e,0x67,0x46,0x61,0x63,0x74,0x6f,0x72,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x0d,0x00,0x47,0x74,0x6b,0x2f,0x49,0x63,0x6f,0x6e,0x53,0x69,0x7a,0x65,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x67,0x74,0x6b,0x2d,0x62,0x75,0x74,0x74,0x6f,0x6e,0x3d,0x31,0x36,0x2c,0x31,0x36,0x00,0x00,0x0e,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x65,0x6e,0x75,0x49,0x6d,0x61,0x67,0x65,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00 diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/client_msg.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/client_msg.rs new file mode 100644 index 0000000..cf55177 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/client_msg.rs @@ -0,0 +1,31 @@ +use super::*; +use x11rb::x11_utils::Serialize; + +impl XConnection { + pub fn send_client_msg( + &self, + window: xproto::Window, // The window this is "about"; not necessarily this window + target_window: xproto::Window, // The window we're sending to + message_type: xproto::Atom, + event_mask: Option, + data: impl Into, + ) -> Result, X11Error> { + let event = xproto::ClientMessageEvent { + response_type: xproto::CLIENT_MESSAGE_EVENT, + window, + format: 32, + data: data.into(), + sequence: 0, + type_: message_type, + }; + + self.xcb_connection() + .send_event( + false, + target_window, + event_mask.unwrap_or(xproto::EventMask::NO_EVENT), + event.serialize(), + ) + .map_err(Into::into) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/cookie.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/cookie.rs new file mode 100644 index 0000000..ccdfa8c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/cookie.rs @@ -0,0 +1,55 @@ +use std::ffi::c_int; +use std::sync::Arc; + +use x11_dl::xlib::{self, XEvent, XGenericEventCookie}; + +use crate::platform_impl::x11::XConnection; + +/// XEvents of type GenericEvent store their actual data in an XGenericEventCookie data structure. +/// This is a wrapper to extract the cookie from a GenericEvent XEvent and release the cookie data +/// once it has been processed +pub struct GenericEventCookie { + cookie: XGenericEventCookie, + xconn: Arc, +} + +impl GenericEventCookie { + pub fn from_event(xconn: Arc, event: XEvent) -> Option { + unsafe { + let mut cookie: XGenericEventCookie = From::from(event); + if (xconn.xlib.XGetEventData)(xconn.display, &mut cookie) == xlib::True { + Some(GenericEventCookie { cookie, xconn }) + } else { + None + } + } + } + + #[inline] + pub fn extension(&self) -> u8 { + self.cookie.extension as u8 + } + + #[inline] + pub fn evtype(&self) -> c_int { + self.cookie.evtype + } + + /// Borrow inner event data as `&T`. + /// + /// ## SAFETY + /// + /// The caller must ensure that the event has the `T` inside of it. + #[inline] + pub unsafe fn as_event(&self) -> &T { + unsafe { &*(self.cookie.data as *const _) } + } +} + +impl Drop for GenericEventCookie { + fn drop(&mut self) { + unsafe { + (self.xconn.xlib.XFreeEventData)(self.xconn.display, &mut self.cookie); + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/cursor.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/cursor.rs new file mode 100644 index 0000000..169748d --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/cursor.rs @@ -0,0 +1,174 @@ +use std::ffi::CString; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; +use std::{iter, slice}; + +use x11rb::connection::Connection; + +use crate::platform_impl::PlatformCustomCursorSource; +use crate::window::CursorIcon; + +use super::super::ActiveEventLoop; +use super::*; + +impl XConnection { + pub fn set_cursor_icon(&self, window: xproto::Window, cursor: Option) { + let cursor = *self + .cursor_cache + .lock() + .unwrap() + .entry(cursor) + .or_insert_with(|| self.get_cursor(cursor)); + + self.update_cursor(window, cursor).expect("Failed to set cursor"); + } + + pub(crate) fn set_custom_cursor(&self, window: xproto::Window, cursor: &CustomCursor) { + self.update_cursor(window, cursor.inner.cursor).expect("Failed to set cursor"); + } + + fn create_empty_cursor(&self) -> ffi::Cursor { + let data = 0; + let pixmap = unsafe { + let screen = (self.xlib.XDefaultScreen)(self.display); + let window = (self.xlib.XRootWindow)(self.display, screen); + (self.xlib.XCreateBitmapFromData)(self.display, window, &data, 1, 1) + }; + + if pixmap == 0 { + panic!("failed to allocate pixmap for cursor"); + } + + unsafe { + // We don't care about this color, since it only fills bytes + // in the pixmap which are not 0 in the mask. + let mut dummy_color = MaybeUninit::uninit(); + let cursor = (self.xlib.XCreatePixmapCursor)( + self.display, + pixmap, + pixmap, + dummy_color.as_mut_ptr(), + dummy_color.as_mut_ptr(), + 0, + 0, + ); + (self.xlib.XFreePixmap)(self.display, pixmap); + + cursor + } + } + + fn get_cursor(&self, cursor: Option) -> ffi::Cursor { + let cursor = match cursor { + Some(cursor) => cursor, + None => return self.create_empty_cursor(), + }; + + let mut xcursor = 0; + for &name in iter::once(&cursor.name()).chain(cursor.alt_names().iter()) { + let name = CString::new(name).unwrap(); + xcursor = unsafe { + (self.xcursor.XcursorLibraryLoadCursor)( + self.display, + name.as_ptr() as *const c_char, + ) + }; + + if xcursor != 0 { + break; + } + } + + xcursor + } + + fn update_cursor(&self, window: xproto::Window, cursor: ffi::Cursor) -> Result<(), X11Error> { + self.xcb_connection() + .change_window_attributes( + window, + &xproto::ChangeWindowAttributesAux::new().cursor(cursor as xproto::Cursor), + )? + .ignore_error(); + + self.xcb_connection().flush()?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SelectedCursor { + Custom(CustomCursor), + Named(CursorIcon), +} + +#[derive(Debug, Clone)] +pub struct CustomCursor { + inner: Arc, +} + +impl Hash for CustomCursor { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.inner).hash(state); + } +} + +impl PartialEq for CustomCursor { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) + } +} + +impl Eq for CustomCursor {} + +impl CustomCursor { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + cursor: PlatformCustomCursorSource, + ) -> CustomCursor { + unsafe { + let ximage = (event_loop.xconn.xcursor.XcursorImageCreate)( + cursor.0.width as i32, + cursor.0.height as i32, + ); + if ximage.is_null() { + panic!("failed to allocate cursor image"); + } + (*ximage).xhot = cursor.0.hotspot_x as u32; + (*ximage).yhot = cursor.0.hotspot_y as u32; + (*ximage).delay = 0; + + let dst = slice::from_raw_parts_mut((*ximage).pixels, cursor.0.rgba.len() / 4); + for (dst, chunk) in dst.iter_mut().zip(cursor.0.rgba.chunks_exact(4)) { + *dst = (chunk[0] as u32) << 16 + | (chunk[1] as u32) << 8 + | (chunk[2] as u32) + | (chunk[3] as u32) << 24; + } + + let cursor = + (event_loop.xconn.xcursor.XcursorImageLoadCursor)(event_loop.xconn.display, ximage); + (event_loop.xconn.xcursor.XcursorImageDestroy)(ximage); + Self { inner: Arc::new(CustomCursorInner { xconn: event_loop.xconn.clone(), cursor }) } + } + } +} + +#[derive(Debug)] +struct CustomCursorInner { + xconn: Arc, + cursor: ffi::Cursor, +} + +impl Drop for CustomCursorInner { + fn drop(&mut self) { + unsafe { + (self.xconn.xlib.XFreeCursor)(self.xconn.display, self.cursor); + } + } +} + +impl Default for SelectedCursor { + fn default() -> Self { + SelectedCursor::Named(Default::default()) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/geometry.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/geometry.rs new file mode 100644 index 0000000..63ec564 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/geometry.rs @@ -0,0 +1,265 @@ +use std::cmp; + +use super::*; + +// Friendly neighborhood axis-aligned rectangle +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AaRect { + x: i64, + y: i64, + width: i64, + height: i64, +} + +impl AaRect { + pub fn new((x, y): (i32, i32), (width, height): (u32, u32)) -> Self { + let (x, y) = (x as i64, y as i64); + let (width, height) = (width as i64, height as i64); + AaRect { x, y, width, height } + } + + pub fn contains_point(&self, x: i64, y: i64) -> bool { + x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height + } + + pub fn get_overlapping_area(&self, other: &Self) -> i64 { + let x_overlap = cmp::max( + 0, + cmp::min(self.x + self.width, other.x + other.width) - cmp::max(self.x, other.x), + ); + let y_overlap = cmp::max( + 0, + cmp::min(self.y + self.height, other.y + other.height) - cmp::max(self.y, other.y), + ); + x_overlap * y_overlap + } +} + +#[derive(Debug, Clone)] +pub struct FrameExtents { + pub left: u32, + pub right: u32, + pub top: u32, + pub bottom: u32, +} + +impl FrameExtents { + pub fn new(left: u32, right: u32, top: u32, bottom: u32) -> Self { + FrameExtents { left, right, top, bottom } + } + + pub fn from_border(border: u32) -> Self { + Self::new(border, border, border, border) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FrameExtentsHeuristicPath { + Supported, + UnsupportedNested, + UnsupportedBordered, +} + +#[derive(Debug, Clone)] +pub struct FrameExtentsHeuristic { + pub frame_extents: FrameExtents, + pub heuristic_path: FrameExtentsHeuristicPath, +} + +impl FrameExtentsHeuristic { + pub fn inner_pos_to_outer(&self, x: i32, y: i32) -> (i32, i32) { + use self::FrameExtentsHeuristicPath::*; + if self.heuristic_path != UnsupportedBordered { + (x - self.frame_extents.left as i32, y - self.frame_extents.top as i32) + } else { + (x, y) + } + } + + pub fn inner_size_to_outer(&self, width: u32, height: u32) -> (u32, u32) { + ( + width.saturating_add( + self.frame_extents.left.saturating_add(self.frame_extents.right) as _ + ), + height.saturating_add( + self.frame_extents.top.saturating_add(self.frame_extents.bottom) as _ + ), + ) + } +} + +impl XConnection { + // This is adequate for inner_position + pub fn translate_coords( + &self, + window: xproto::Window, + root: xproto::Window, + ) -> Result { + self.xcb_connection().translate_coordinates(window, root, 0, 0)?.reply().map_err(Into::into) + } + + // This is adequate for inner_size + pub fn get_geometry( + &self, + window: xproto::Window, + ) -> Result { + self.xcb_connection().get_geometry(window)?.reply().map_err(Into::into) + } + + fn get_frame_extents(&self, window: xproto::Window) -> Option { + let atoms = self.atoms(); + let extents_atom = atoms[_NET_FRAME_EXTENTS]; + + if !hint_is_supported(extents_atom) { + return None; + } + + // Of the WMs tested, xmonad, i3, dwm, IceWM (1.3.x and earlier), and blackbox don't + // support this. As this is part of EWMH (Extended Window Manager Hints), it's likely to + // be unsupported by many smaller WMs. + let extents: Option> = self + .get_property(window, extents_atom, xproto::Atom::from(xproto::AtomEnum::CARDINAL)) + .ok(); + + extents.and_then(|extents| { + if extents.len() >= 4 { + Some(FrameExtents { + left: extents[0], + right: extents[1], + top: extents[2], + bottom: extents[3], + }) + } else { + None + } + }) + } + + pub fn is_top_level(&self, window: xproto::Window, root: xproto::Window) -> Option { + let atoms = self.atoms(); + let client_list_atom = atoms[_NET_CLIENT_LIST]; + + if !hint_is_supported(client_list_atom) { + return None; + } + + let client_list: Option> = self + .get_property(root, client_list_atom, xproto::Atom::from(xproto::AtomEnum::WINDOW)) + .ok(); + + client_list.map(|client_list| client_list.contains(&(window as xproto::Window))) + } + + fn get_parent_window(&self, window: xproto::Window) -> Result { + let parent = self.xcb_connection().query_tree(window)?.reply()?.parent; + Ok(parent) + } + + fn climb_hierarchy( + &self, + window: xproto::Window, + root: xproto::Window, + ) -> Result { + let mut outer_window = window; + loop { + let candidate = self.get_parent_window(outer_window)?; + if candidate == root { + break; + } + outer_window = candidate; + } + Ok(outer_window) + } + + pub fn get_frame_extents_heuristic( + &self, + window: xproto::Window, + root: xproto::Window, + ) -> FrameExtentsHeuristic { + use self::FrameExtentsHeuristicPath::*; + + // Position relative to root window. + // With rare exceptions, this is the position of a nested window. Cases where the window + // isn't nested are outlined in the comments throughout this function, but in addition to + // that, fullscreen windows often aren't nested. + let (inner_y_rel_root, child) = { + let coords = self + .translate_coords(window, root) + .expect("Failed to translate window coordinates"); + (coords.dst_y, coords.child) + }; + + let (width, height, border) = { + let inner_geometry = + self.get_geometry(window).expect("Failed to get inner window geometry"); + (inner_geometry.width, inner_geometry.height, inner_geometry.border_width) + }; + + // The first condition is only false for un-nested windows, but isn't always false for + // un-nested windows. Mutter/Muffin/Budgie and Marco present a mysterious discrepancy: + // when y is on the range [0, 2] and if the window has been unfocused since being + // undecorated (or was undecorated upon construction), the first condition is true, + // requiring us to rely on the second condition. + let nested = !(window == child || self.is_top_level(child, root) == Some(true)); + + // Hopefully the WM supports EWMH, allowing us to get exact info on the window frames. + if let Some(mut frame_extents) = self.get_frame_extents(window) { + // Mutter/Muffin/Budgie and Marco preserve their decorated frame extents when + // decorations are disabled, but since the window becomes un-nested, it's easy to + // catch. + if !nested { + frame_extents = FrameExtents::new(0, 0, 0, 0); + } + + // The difference between the nested window's position and the outermost window's + // position is equivalent to the frame size. In most scenarios, this is equivalent to + // manually climbing the hierarchy as is done in the case below. Here's a list of + // known discrepancies: + // * Mutter/Muffin/Budgie gives decorated windows a margin of 9px (only 7px on top) in + // addition to a 1px semi-transparent border. The margin can be easily observed by + // using a screenshot tool to get a screenshot of a selected window, and is presumably + // used for drawing drop shadows. Getting window geometry information via + // hierarchy-climbing results in this margin being included in both the position and + // outer size, so a window positioned at (0, 0) would be reported as having a position + // (-10, -8). + // * Compiz has a drop shadow margin just like Mutter/Muffin/Budgie, though it's 10px on + // all sides, and there's no additional border. + // * Enlightenment otherwise gets a y position equivalent to inner_y_rel_root. Without + // decorations, there's no difference. This is presumably related to Enlightenment's + // fairly unique concept of window position; it interprets positions given to + // XMoveWindow as a client area position rather than a position of the overall window. + + FrameExtentsHeuristic { frame_extents, heuristic_path: Supported } + } else if nested { + // If the position value we have is for a nested window used as the client area, we'll + // just climb up the hierarchy and get the geometry of the outermost window we're + // nested in. + let outer_window = + self.climb_hierarchy(window, root).expect("Failed to climb window hierarchy"); + let (outer_y, outer_width, outer_height) = { + let outer_geometry = + self.get_geometry(outer_window).expect("Failed to get outer window geometry"); + (outer_geometry.y, outer_geometry.width, outer_geometry.height) + }; + + // Since we have the geometry of the outermost window and the geometry of the client + // area, we can figure out what's in between. + let diff_x = outer_width.saturating_sub(width) as u32; + let diff_y = outer_height.saturating_sub(height) as u32; + let offset_y = inner_y_rel_root.saturating_sub(outer_y) as u32; + + let left = diff_x / 2; + let right = left; + let top = offset_y; + let bottom = diff_y.saturating_sub(offset_y); + + let frame_extents = FrameExtents::new(left, right, top, bottom); + FrameExtentsHeuristic { frame_extents, heuristic_path: UnsupportedNested } + } else { + // This is the case for xmonad and dwm, AKA the only WMs tested that supplied a + // border value. This is convenient, since we can use it to get an accurate frame. + let frame_extents = FrameExtents::from_border(border.into()); + FrameExtentsHeuristic { frame_extents, heuristic_path: UnsupportedBordered } + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/hint.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/hint.rs new file mode 100644 index 0000000..a31872d --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/hint.rs @@ -0,0 +1,169 @@ +use crate::platform::x11::WindowType; +use std::sync::Arc; + +use super::*; + +#[derive(Debug)] +#[allow(dead_code)] +pub enum StateOperation { + Remove = 0, // _NET_WM_STATE_REMOVE + Add = 1, // _NET_WM_STATE_ADD + Toggle = 2, // _NET_WM_STATE_TOGGLE +} + +impl From for StateOperation { + fn from(op: bool) -> Self { + if op { + StateOperation::Add + } else { + StateOperation::Remove + } + } +} + +impl WindowType { + pub(crate) fn as_atom(&self, xconn: &Arc) -> xproto::Atom { + use self::WindowType::*; + let atom_name = match *self { + Desktop => _NET_WM_WINDOW_TYPE_DESKTOP, + Dock => _NET_WM_WINDOW_TYPE_DOCK, + Toolbar => _NET_WM_WINDOW_TYPE_TOOLBAR, + Menu => _NET_WM_WINDOW_TYPE_MENU, + Utility => _NET_WM_WINDOW_TYPE_UTILITY, + Splash => _NET_WM_WINDOW_TYPE_SPLASH, + Dialog => _NET_WM_WINDOW_TYPE_DIALOG, + DropdownMenu => _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + PopupMenu => _NET_WM_WINDOW_TYPE_POPUP_MENU, + Tooltip => _NET_WM_WINDOW_TYPE_TOOLTIP, + Notification => _NET_WM_WINDOW_TYPE_NOTIFICATION, + Combo => _NET_WM_WINDOW_TYPE_COMBO, + Dnd => _NET_WM_WINDOW_TYPE_DND, + Normal => _NET_WM_WINDOW_TYPE_NORMAL, + }; + + let atoms = xconn.atoms(); + atoms[atom_name] + } +} + +pub struct MotifHints { + hints: MwmHints, +} + +struct MwmHints { + flags: u32, + functions: u32, + decorations: u32, + input_mode: u32, + status: u32, +} + +#[allow(dead_code)] +mod mwm { + // Motif WM hints are obsolete, but still widely supported. + // https://stackoverflow.com/a/1909708 + pub const MWM_HINTS_FUNCTIONS: u32 = 1 << 0; + pub const MWM_HINTS_DECORATIONS: u32 = 1 << 1; + + pub const MWM_FUNC_ALL: u32 = 1 << 0; + pub const MWM_FUNC_RESIZE: u32 = 1 << 1; + pub const MWM_FUNC_MOVE: u32 = 1 << 2; + pub const MWM_FUNC_MINIMIZE: u32 = 1 << 3; + pub const MWM_FUNC_MAXIMIZE: u32 = 1 << 4; + pub const MWM_FUNC_CLOSE: u32 = 1 << 5; +} + +impl MotifHints { + pub fn new() -> MotifHints { + MotifHints { + hints: MwmHints { flags: 0, functions: 0, decorations: 0, input_mode: 0, status: 0 }, + } + } + + pub fn set_decorations(&mut self, decorations: bool) { + self.hints.flags |= mwm::MWM_HINTS_DECORATIONS; + self.hints.decorations = decorations as u32; + } + + pub fn set_maximizable(&mut self, maximizable: bool) { + if maximizable { + self.add_func(mwm::MWM_FUNC_MAXIMIZE); + } else { + self.remove_func(mwm::MWM_FUNC_MAXIMIZE); + } + } + + fn add_func(&mut self, func: u32) { + if self.hints.flags & mwm::MWM_HINTS_FUNCTIONS != 0 { + if self.hints.functions & mwm::MWM_FUNC_ALL != 0 { + self.hints.functions &= !func; + } else { + self.hints.functions |= func; + } + } + } + + fn remove_func(&mut self, func: u32) { + if self.hints.flags & mwm::MWM_HINTS_FUNCTIONS == 0 { + self.hints.flags |= mwm::MWM_HINTS_FUNCTIONS; + self.hints.functions = mwm::MWM_FUNC_ALL; + } + + if self.hints.functions & mwm::MWM_FUNC_ALL != 0 { + self.hints.functions |= func; + } else { + self.hints.functions &= !func; + } + } +} + +impl Default for MotifHints { + fn default() -> Self { + Self::new() + } +} + +impl XConnection { + pub fn get_motif_hints(&self, window: xproto::Window) -> MotifHints { + let atoms = self.atoms(); + let motif_hints = atoms[_MOTIF_WM_HINTS]; + + let mut hints = MotifHints::new(); + + if let Ok(props) = self.get_property::(window, motif_hints, motif_hints) { + hints.hints.flags = props.first().cloned().unwrap_or(0); + hints.hints.functions = props.get(1).cloned().unwrap_or(0); + hints.hints.decorations = props.get(2).cloned().unwrap_or(0); + hints.hints.input_mode = props.get(3).cloned().unwrap_or(0); + hints.hints.status = props.get(4).cloned().unwrap_or(0); + } + + hints + } + + #[allow(clippy::unnecessary_cast)] + pub fn set_motif_hints( + &self, + window: xproto::Window, + hints: &MotifHints, + ) -> Result, X11Error> { + let atoms = self.atoms(); + let motif_hints = atoms[_MOTIF_WM_HINTS]; + + let hints_data: [u32; 5] = [ + hints.hints.flags as u32, + hints.hints.functions as u32, + hints.hints.decorations as u32, + hints.hints.input_mode as u32, + hints.hints.status as u32, + ]; + + self.change_property( + window, + motif_hints, + motif_hints, + xproto::PropMode::REPLACE, + &hints_data, + ) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/icon.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/icon.rs new file mode 100644 index 0000000..07b5fee --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/icon.rs @@ -0,0 +1,36 @@ +#![allow(clippy::assertions_on_constants)] + +use super::*; +use crate::icon::{Pixel, RgbaIcon, PIXEL_SIZE}; + +impl Pixel { + pub fn to_packed_argb(&self) -> Cardinal { + let mut cardinal = 0; + assert!(CARDINAL_SIZE >= PIXEL_SIZE); + let as_bytes = &mut cardinal as *mut _ as *mut u8; + unsafe { + *as_bytes.offset(0) = self.b; + *as_bytes.offset(1) = self.g; + *as_bytes.offset(2) = self.r; + *as_bytes.offset(3) = self.a; + } + cardinal + } +} + +impl RgbaIcon { + pub(crate) fn to_cardinals(&self) -> Vec { + assert_eq!(self.rgba.len() % PIXEL_SIZE, 0); + let pixel_count = self.rgba.len() / PIXEL_SIZE; + assert_eq!(pixel_count, (self.width * self.height) as usize); + let mut data = Vec::with_capacity(pixel_count); + data.push(self.width as Cardinal); + data.push(self.height as Cardinal); + let pixels = self.rgba.as_ptr() as *const Pixel; + for pixel_index in 0..pixel_count { + let pixel = unsafe { &*pixels.add(pixel_index) }; + data.push(pixel.to_packed_argb()); + } + data + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/input.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/input.rs new file mode 100644 index 0000000..37d21d1 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/input.rs @@ -0,0 +1,106 @@ +use std::{slice, str}; +use x11rb::protocol::xinput::{self, ConnectionExt as _}; +use x11rb::protocol::xkb; + +use super::*; + +pub const VIRTUAL_CORE_POINTER: u16 = 2; +pub const VIRTUAL_CORE_KEYBOARD: u16 = 3; + +// A base buffer size of 1kB uses a negligible amount of RAM while preventing us from having to +// re-allocate (and make another round-trip) in the *vast* majority of cases. +// To test if `lookup_utf8` works correctly, set this to 1. +const TEXT_BUFFER_SIZE: usize = 1024; + +impl XConnection { + pub fn select_xinput_events( + &self, + window: xproto::Window, + device_id: u16, + mask: xinput::XIEventMask, + ) -> Result, X11Error> { + self.xcb_connection() + .xinput_xi_select_events(window, &[xinput::EventMask { + deviceid: device_id, + mask: vec![mask], + }]) + .map_err(Into::into) + } + + pub fn select_xkb_events( + &self, + device_id: xkb::DeviceSpec, + mask: xkb::EventType, + ) -> Result { + let mask = u16::from(mask) as _; + let status = + unsafe { (self.xlib.XkbSelectEvents)(self.display, device_id as _, mask, mask) }; + + if status == ffi::True { + self.flush_requests()?; + Ok(true) + } else { + tracing::error!("Could not select XKB events: The XKB extension is not initialized!"); + Ok(false) + } + } + + pub fn query_pointer( + &self, + window: xproto::Window, + device_id: u16, + ) -> Result { + self.xcb_connection() + .xinput_xi_query_pointer(window, device_id)? + .reply() + .map_err(Into::into) + } + + fn lookup_utf8_inner( + &self, + ic: ffi::XIC, + key_event: &mut ffi::XKeyEvent, + buffer: *mut u8, + size: usize, + ) -> (ffi::KeySym, ffi::Status, c_int) { + let mut keysym: ffi::KeySym = 0; + let mut status: ffi::Status = 0; + let count = unsafe { + (self.xlib.Xutf8LookupString)( + ic, + key_event, + buffer as *mut c_char, + size as c_int, + &mut keysym, + &mut status, + ) + }; + (keysym, status, count) + } + + pub fn lookup_utf8(&self, ic: ffi::XIC, key_event: &mut ffi::XKeyEvent) -> String { + // `assume_init` is safe here because the array consists of `MaybeUninit` values, + // which do not require initialization. + let mut buffer: [MaybeUninit; TEXT_BUFFER_SIZE] = + unsafe { MaybeUninit::uninit().assume_init() }; + // If the buffer overflows, we'll make a new one on the heap. + let mut vec; + + let (_, status, count) = + self.lookup_utf8_inner(ic, key_event, buffer.as_mut_ptr() as *mut u8, buffer.len()); + + let bytes = if status == ffi::XBufferOverflow { + vec = Vec::with_capacity(count as usize); + let (_, _, new_count) = + self.lookup_utf8_inner(ic, key_event, vec.as_mut_ptr(), vec.capacity()); + debug_assert_eq!(count, new_count); + + unsafe { vec.set_len(count as usize) }; + &vec[..count as usize] + } else { + unsafe { slice::from_raw_parts(buffer.as_ptr() as *const u8, count as usize) } + }; + + str::from_utf8(bytes).unwrap_or("").to_string() + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/keys.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/keys.rs new file mode 100644 index 0000000..a6ad291 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/keys.rs @@ -0,0 +1,75 @@ +use std::iter::Enumerate; +use std::slice::Iter; + +use super::*; + +pub struct Keymap { + keys: [u8; 32], +} + +pub struct KeymapIter<'a> { + iter: Enumerate>, + index: usize, + item: Option, +} + +impl Keymap { + pub fn iter(&self) -> KeymapIter<'_> { + KeymapIter { iter: self.keys.iter().enumerate(), index: 0, item: None } + } +} + +impl<'a> IntoIterator for &'a Keymap { + type IntoIter = KeymapIter<'a>; + type Item = ffi::KeyCode; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl Iterator for KeymapIter<'_> { + type Item = ffi::KeyCode; + + fn next(&mut self) -> Option { + if self.item.is_none() { + for (index, &item) in self.iter.by_ref() { + if item != 0 { + self.index = index; + self.item = Some(item); + break; + } + } + } + + self.item.take().map(|item| { + debug_assert!(item != 0); + + let bit = first_bit(item); + + if item != bit { + // Remove the first bit; save the rest for further iterations + self.item = Some(item ^ bit); + } + + let shift = bit.trailing_zeros() + (self.index * 8) as u32; + shift as ffi::KeyCode + }) + } +} + +impl XConnection { + pub fn query_keymap(&self) -> Keymap { + let mut keys = [0; 32]; + + unsafe { + (self.xlib.XQueryKeymap)(self.display, keys.as_mut_ptr() as *mut c_char); + } + + Keymap { keys } + } +} + +fn first_bit(b: u8) -> u8 { + 1 << b.trailing_zeros() +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/memory.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/memory.rs new file mode 100644 index 0000000..4e052a7 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/memory.rs @@ -0,0 +1,42 @@ +use std::ops::{Deref, DerefMut}; + +use super::*; + +pub(crate) struct XSmartPointer<'a, T> { + xconn: &'a XConnection, + pub ptr: *mut T, +} + +impl<'a, T> XSmartPointer<'a, T> { + // You're responsible for only passing things to this that should be XFree'd. + // Returns None if ptr is null. + pub fn new(xconn: &'a XConnection, ptr: *mut T) -> Option { + if !ptr.is_null() { + Some(XSmartPointer { xconn, ptr }) + } else { + None + } + } +} + +impl Deref for XSmartPointer<'_, T> { + type Target = T; + + fn deref(&self) -> &T { + unsafe { &*self.ptr } + } +} + +impl DerefMut for XSmartPointer<'_, T> { + fn deref_mut(&mut self) -> &mut T { + unsafe { &mut *self.ptr } + } +} + +impl Drop for XSmartPointer<'_, T> { + fn drop(&mut self) { + unsafe { + (self.xconn.xlib.XFree)(self.ptr as *mut _); + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/mod.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/mod.rs new file mode 100644 index 0000000..55e6c2d --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/mod.rs @@ -0,0 +1,77 @@ +// Welcome to the util module, where we try to keep you from shooting yourself in the foot. +// *results may vary + +use std::mem::{self, MaybeUninit}; +use std::ops::BitAnd; +use std::os::raw::*; + +mod client_msg; +pub mod cookie; +mod cursor; +mod geometry; +mod hint; +mod icon; +mod input; +pub mod keys; +pub(crate) mod memory; +mod mouse; +mod randr; +mod window_property; +mod wm; +mod xmodmap; + +pub use self::cursor::*; +pub use self::geometry::*; +pub use self::hint::*; +pub use self::input::*; +pub use self::mouse::*; +pub use self::window_property::*; +pub use self::wm::*; +pub use self::xmodmap::ModifierKeymap; + +use super::atoms::*; +use super::{ffi, VoidCookie, X11Error, XConnection, XError}; +use x11rb::protocol::xproto::{self, ConnectionExt as _}; + +pub fn maybe_change(field: &mut Option, value: T) -> bool { + let wrapped = Some(value); + if *field != wrapped { + *field = wrapped; + true + } else { + false + } +} + +pub fn has_flag(bitset: T, flag: T) -> bool +where + T: Copy + PartialEq + BitAnd, +{ + bitset & flag == flag +} + +impl XConnection { + // This is important, so pay attention! + // Xlib has an output buffer, and tries to hide the async nature of X from you. + // This buffer contains the requests you make, and is flushed under various circumstances: + // 1. `XPending`, `XNextEvent`, and `XWindowEvent` flush "as needed" + // 2. `XFlush` explicitly flushes + // 3. `XSync` flushes and blocks until all requests are responded to + // 4. Calls that have a return dependent on a response (i.e. `XGetWindowProperty`) sync + // internally. When in doubt, check the X11 source; if a function calls `_XReply`, it flushes + // and waits. + // All util functions that abstract an async function will return a `Flusher`. + pub fn flush_requests(&self) -> Result<(), XError> { + unsafe { (self.xlib.XFlush)(self.display) }; + // println!("XFlush"); + // This isn't necessarily a useful time to check for errors (since our request hasn't + // necessarily been processed yet) + self.check_errors() + } + + pub fn sync_with_server(&self) -> Result<(), XError> { + unsafe { (self.xlib.XSync)(self.display, ffi::False) }; + // println!("XSync"); + self.check_errors() + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/modifiers.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/modifiers.rs new file mode 100644 index 0000000..bb157e4 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/modifiers.rs @@ -0,0 +1,187 @@ +use std::{collections::HashMap, slice}; + +use super::*; + +use crate::event::{ElementState, ModifiersState}; + +// Offsets within XModifierKeymap to each set of keycodes. +// We are only interested in Shift, Control, Alt, and Logo. +// +// There are 8 sets total. The order of keycode sets is: +// Shift, Lock, Control, Mod1 (Alt), Mod2, Mod3, Mod4 (Logo), Mod5 +// +// https://tronche.com/gui/x/xlib/input/XSetModifierMapping.html +const SHIFT_OFFSET: usize = 0; +const CONTROL_OFFSET: usize = 2; +const ALT_OFFSET: usize = 3; +const LOGO_OFFSET: usize = 6; +const NUM_MODS: usize = 8; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Modifier { + Alt, + Ctrl, + Shift, + Logo, +} + +#[derive(Debug, Default)] +pub(crate) struct ModifierKeymap { + // Maps keycodes to modifiers + keys: HashMap, +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct ModifierKeyState { + // Contains currently pressed modifier keys and their corresponding modifiers + keys: HashMap, + state: ModifiersState, +} + +impl ModifierKeymap { + pub fn new() -> ModifierKeymap { + ModifierKeymap::default() + } + + pub fn get_modifier(&self, keycode: ffi::KeyCode) -> Option { + self.keys.get(&keycode).cloned() + } + + pub fn reset_from_x_connection(&mut self, xconn: &XConnection) { + { + let keymap = xconn.xcb_connection().get_modifier_mapping().expect("get_modifier_mapping failed").reply().expect("get_modifier_mapping failed"); + + if keymap.is_null() { + panic!("failed to allocate XModifierKeymap"); + } + + self.reset_from_x_keymap(&*keymap); + + (xconn.xlib.XFreeModifiermap)(keymap); + } + } + + pub fn reset_from_x_keymap(&mut self, keymap: &ffi::XModifierKeymap) { + let keys_per_mod = keymap.max_keypermod as usize; + + let keys = unsafe { + slice::from_raw_parts(keymap.modifiermap as *const _, keys_per_mod * NUM_MODS) + }; + + self.keys.clear(); + + self.read_x_keys(keys, SHIFT_OFFSET, keys_per_mod, Modifier::Shift); + self.read_x_keys(keys, CONTROL_OFFSET, keys_per_mod, Modifier::Ctrl); + self.read_x_keys(keys, ALT_OFFSET, keys_per_mod, Modifier::Alt); + self.read_x_keys(keys, LOGO_OFFSET, keys_per_mod, Modifier::Logo); + } + + fn read_x_keys( + &mut self, + keys: &[ffi::KeyCode], + offset: usize, + keys_per_mod: usize, + modifier: Modifier, + ) { + let start = offset * keys_per_mod; + let end = start + keys_per_mod; + + for &keycode in &keys[start..end] { + if keycode != 0 { + self.keys.insert(keycode, modifier); + } + } + } +} + +impl ModifierKeyState { + pub fn update_keymap(&mut self, mods: &ModifierKeymap) { + self.keys.retain(|k, v| { + if let Some(m) = mods.get_modifier(*k) { + *v = m; + true + } else { + false + } + }); + + self.reset_state(); + } + + pub fn update_state( + &mut self, + state: &ModifiersState, + except: Option, + ) -> Option { + let mut new_state = *state; + + match except { + Some(Modifier::Alt) => new_state.set(ModifiersState::ALT, self.state.alt()), + Some(Modifier::Ctrl) => new_state.set(ModifiersState::CTRL, self.state.ctrl()), + Some(Modifier::Shift) => new_state.set(ModifiersState::SHIFT, self.state.shift()), + Some(Modifier::Logo) => new_state.set(ModifiersState::LOGO, self.state.logo()), + None => (), + } + + if self.state == new_state { + None + } else { + self.keys.retain(|_k, v| get_modifier(&new_state, *v)); + self.state = new_state; + Some(new_state) + } + } + + pub fn modifiers(&self) -> ModifiersState { + self.state + } + + pub fn key_event(&mut self, state: ElementState, keycode: ffi::KeyCode, modifier: Modifier) { + match state { + ElementState::Pressed => self.key_press(keycode, modifier), + ElementState::Released => self.key_release(keycode), + } + } + + pub fn key_press(&mut self, keycode: ffi::KeyCode, modifier: Modifier) { + self.keys.insert(keycode, modifier); + + set_modifier(&mut self.state, modifier, true); + } + + pub fn key_release(&mut self, keycode: ffi::KeyCode) { + if let Some(modifier) = self.keys.remove(&keycode) { + if !self.keys.values().any(|&m| m == modifier) { + set_modifier(&mut self.state, modifier, false); + } + } + } + + fn reset_state(&mut self) { + let mut new_state = ModifiersState::default(); + + for &m in self.keys.values() { + set_modifier(&mut new_state, m, true); + } + + self.state = new_state; + } +} + +fn get_modifier(state: &ModifiersState, modifier: Modifier) -> bool { + match modifier { + Modifier::Alt => state.alt(), + Modifier::Ctrl => state.ctrl(), + Modifier::Shift => state.shift(), + Modifier::Logo => state.logo(), + } +} + +fn set_modifier(state: &mut ModifiersState, modifier: Modifier, value: bool) { + match modifier { + Modifier::Alt => state.set(ModifiersState::ALT, value), + Modifier::Ctrl => state.set(ModifiersState::CTRL, value), + Modifier::Shift => state.set(ModifiersState::SHIFT, value), + Modifier::Logo => state.set(ModifiersState::LOGO, value), + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/mouse.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/mouse.rs new file mode 100644 index 0000000..e66a76c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/mouse.rs @@ -0,0 +1,49 @@ +//! Utilities for handling mouse events. + +/// Recorded mouse delta designed to filter out noise. +pub struct Delta { + x: T, + y: T, +} + +impl Default for Delta { + fn default() -> Self { + Self { x: Default::default(), y: Default::default() } + } +} + +impl Delta { + pub(crate) fn set_x(&mut self, x: T) { + self.x = x; + } + + pub(crate) fn set_y(&mut self, y: T) { + self.y = y; + } +} + +macro_rules! consume { + ($this:expr, $ty:ty) => {{ + let this = $this; + let (x, y) = match (this.x.abs() < <$ty>::EPSILON, this.y.abs() < <$ty>::EPSILON) { + (true, true) => return None, + (false, true) => (this.x, 0.0), + (true, false) => (0.0, this.y), + (false, false) => (this.x, this.y), + }; + + Some((x, y)) + }}; +} + +impl Delta { + pub(crate) fn consume(self) -> Option<(f32, f32)> { + consume!(self, f32) + } +} + +impl Delta { + pub(crate) fn consume(self) -> Option<(f64, f64)> { + consume!(self, f64) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/randr.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/randr.rs new file mode 100644 index 0000000..d10c97e --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/randr.rs @@ -0,0 +1,186 @@ +use std::str::FromStr; +use std::{env, str}; + +use super::*; +use crate::dpi::validate_scale_factor; +use crate::platform_impl::platform::x11::{monitor, VideoModeHandle}; + +use tracing::warn; +use x11rb::protocol::randr::{self, ConnectionExt as _}; + +/// Represents values of `WINIT_HIDPI_FACTOR`. +pub enum EnvVarDPI { + Randr, + Scale(f64), + NotSet, +} + +pub fn calc_dpi_factor( + (width_px, height_px): (u32, u32), + (width_mm, height_mm): (u64, u64), +) -> f64 { + // See http://xpra.org/trac/ticket/728 for more information. + if width_mm == 0 || height_mm == 0 { + warn!("XRandR reported that the display's 0mm in size, which is certifiably insane"); + return 1.0; + } + + let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt(); + // Quantize 1/12 step size + let dpi_factor = ((ppmm * (12.0 * 25.4 / 96.0)).round() / 12.0).max(1.0); + assert!(validate_scale_factor(dpi_factor)); + if dpi_factor <= 20. { + dpi_factor + } else { + 1. + } +} + +impl XConnection { + // Retrieve DPI from Xft.dpi property + pub fn get_xft_dpi(&self) -> Option { + // Try to get it from XSETTINGS first. + if let Some(xsettings_screen) = self.xsettings_screen() { + match self.xsettings_dpi(xsettings_screen) { + Ok(Some(dpi)) => return Some(dpi), + Ok(None) => {}, + Err(err) => { + tracing::warn!("failed to fetch XSettings: {err}"); + }, + } + } + + self.database().get_string("Xft.dpi", "").and_then(|s| f64::from_str(s).ok()) + } + + pub fn get_output_info( + &self, + resources: &monitor::ScreenResources, + crtc: &randr::GetCrtcInfoReply, + ) -> Option<(String, f64, Vec)> { + let output_info = match self + .xcb_connection() + .randr_get_output_info(crtc.outputs[0], x11rb::CURRENT_TIME) + .map_err(X11Error::from) + .and_then(|r| r.reply().map_err(X11Error::from)) + { + Ok(output_info) => output_info, + Err(err) => { + warn!("Failed to get output info: {:?}", err); + return None; + }, + }; + + let bit_depth = self.default_root().root_depth; + let output_modes = &output_info.modes; + let resource_modes = resources.modes(); + + let modes = resource_modes + .iter() + // XRROutputInfo contains an array of mode ids that correspond to + // modes in the array in XRRScreenResources + .filter(|x| output_modes.contains(&x.id)) + .map(|mode| { + VideoModeHandle { + size: (mode.width.into(), mode.height.into()), + refresh_rate_millihertz: monitor::mode_refresh_rate_millihertz(mode) + .unwrap_or(0), + bit_depth: bit_depth as u16, + native_mode: mode.id, + // This is populated in `MonitorHandle::video_modes` as the + // video mode is returned to the user + monitor: None, + } + }) + .collect(); + + let name = match str::from_utf8(&output_info.name) { + Ok(name) => name.to_owned(), + Err(err) => { + warn!("Failed to get output name: {:?}", err); + return None; + }, + }; + // Override DPI if `WINIT_X11_SCALE_FACTOR` variable is set + let deprecated_dpi_override = env::var("WINIT_HIDPI_FACTOR").ok(); + if deprecated_dpi_override.is_some() { + warn!( + "The WINIT_HIDPI_FACTOR environment variable is deprecated; use \ + WINIT_X11_SCALE_FACTOR" + ) + } + let dpi_env = env::var("WINIT_X11_SCALE_FACTOR").ok().map_or_else( + || EnvVarDPI::NotSet, + |var| { + if var.to_lowercase() == "randr" { + EnvVarDPI::Randr + } else if let Ok(dpi) = f64::from_str(&var) { + EnvVarDPI::Scale(dpi) + } else if var.is_empty() { + EnvVarDPI::NotSet + } else { + panic!( + "`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal \ + floats greater than 0, or `randr`. Got `{var}`" + ); + } + }, + ); + + let scale_factor = match dpi_env { + EnvVarDPI::Randr => calc_dpi_factor( + (crtc.width.into(), crtc.height.into()), + (output_info.mm_width as _, output_info.mm_height as _), + ), + EnvVarDPI::Scale(dpi_override) => { + if !validate_scale_factor(dpi_override) { + panic!( + "`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal \ + floats greater than 0, or `randr`. Got `{dpi_override}`", + ); + } + dpi_override + }, + EnvVarDPI::NotSet => { + if let Some(dpi) = self.get_xft_dpi() { + dpi / 96. + } else { + calc_dpi_factor( + (crtc.width.into(), crtc.height.into()), + (output_info.mm_width as _, output_info.mm_height as _), + ) + } + }, + }; + + Some((name, scale_factor, modes)) + } + + pub fn set_crtc_config( + &self, + crtc_id: randr::Crtc, + mode_id: randr::Mode, + ) -> Result<(), X11Error> { + let crtc = + self.xcb_connection().randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)?.reply()?; + + self.xcb_connection() + .randr_set_crtc_config( + crtc_id, + crtc.timestamp, + x11rb::CURRENT_TIME, + crtc.x, + crtc.y, + mode_id, + crtc.rotation, + &crtc.outputs, + )? + .reply() + .map(|_| ()) + .map_err(Into::into) + } + + pub fn get_crtc_mode(&self, crtc_id: randr::Crtc) -> Result { + Ok(self.xcb_connection().randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)?.reply()?.mode) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/window_property.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/window_property.rs new file mode 100644 index 0000000..0f4ca16 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/window_property.rs @@ -0,0 +1,196 @@ +use std::error::Error; +use std::fmt; +use std::sync::Arc; + +use bytemuck::{NoUninit, Pod}; + +use x11rb::connection::Connection; +use x11rb::errors::ReplyError; + +use super::*; + +pub const CARDINAL_SIZE: usize = mem::size_of::(); + +pub type Cardinal = u32; + +#[derive(Debug, Clone)] +pub enum GetPropertyError { + X11rbError(Arc), + TypeMismatch(xproto::Atom), + FormatMismatch(c_int), +} + +impl GetPropertyError { + pub fn is_actual_property_type(&self, t: xproto::Atom) -> bool { + if let GetPropertyError::TypeMismatch(actual_type) = *self { + actual_type == t + } else { + false + } + } +} + +impl> From for GetPropertyError { + fn from(e: T) -> Self { + Self::X11rbError(Arc::new(e.into())) + } +} + +impl fmt::Display for GetPropertyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GetPropertyError::X11rbError(err) => err.fmt(f), + GetPropertyError::TypeMismatch(err) => write!(f, "type mismatch: {err}"), + GetPropertyError::FormatMismatch(err) => write!(f, "format mismatch: {err}"), + } + } +} + +impl Error for GetPropertyError {} + +// Number of 32-bit chunks to retrieve per iteration of get_property's inner loop. +// To test if `get_property` works correctly, set this to 1. +const PROPERTY_BUFFER_SIZE: u32 = 1024; // 4k of RAM ought to be enough for anyone! + +impl XConnection { + pub fn get_property( + &self, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, + ) -> Result, GetPropertyError> { + let mut iter = PropIterator::new(self.xcb_connection(), window, property, property_type); + let mut data = vec![]; + + loop { + if !iter.next_window(&mut data)? { + break; + } + } + + Ok(data) + } + + pub fn change_property<'a, T: NoUninit>( + &'a self, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, + mode: xproto::PropMode, + new_value: &[T], + ) -> Result, X11Error> { + assert!([1usize, 2, 4].contains(&mem::size_of::())); + self.xcb_connection() + .change_property( + mode, + window, + property, + property_type, + (mem::size_of::() * 8) as u8, + new_value.len().try_into().expect("too many items for property"), + bytemuck::cast_slice::(new_value), + ) + .map_err(Into::into) + } +} + +/// An iterator over the "windows" of the property that we are fetching. +struct PropIterator<'a, C: ?Sized, T> { + /// Handle to the connection. + conn: &'a C, + + /// The window that we're fetching the property from. + window: xproto::Window, + + /// The property that we're fetching. + property: xproto::Atom, + + /// The type of the property that we're fetching. + property_type: xproto::Atom, + + /// The offset of the next window, in 32-bit chunks. + offset: u32, + + /// The format of the type. + format: u8, + + /// Keep a reference to `T`. + _phantom: std::marker::PhantomData, +} + +impl<'a, C: Connection + ?Sized, T: Pod> PropIterator<'a, C, T> { + /// Create a new property iterator. + fn new( + conn: &'a C, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, + ) -> Self { + let format = match mem::size_of::() { + 1 => 8, + 2 => 16, + 4 => 32, + _ => unreachable!(), + }; + + Self { + conn, + window, + property, + property_type, + offset: 0, + format, + _phantom: Default::default(), + } + } + + /// Get the next window and append it to `data`. + /// + /// Returns whether there are more windows to fetch. + fn next_window(&mut self, data: &mut Vec) -> Result { + // Send the request and wait for the reply. + let reply = self + .conn + .get_property( + false, + self.window, + self.property, + self.property_type, + self.offset, + PROPERTY_BUFFER_SIZE, + )? + .reply()?; + + // Make sure that the reply is of the correct type. + if reply.type_ != self.property_type { + return Err(GetPropertyError::TypeMismatch(reply.type_)); + } + + // Make sure that the reply is of the correct format. + if reply.format != self.format { + return Err(GetPropertyError::FormatMismatch(reply.format.into())); + } + + // Append the data to the output. + if mem::size_of::() == 1 && mem::align_of::() == 1 { + // We can just do a bytewise append. + data.extend_from_slice(bytemuck::cast_slice(&reply.value)); + } else { + // Rust's borrowing and types system makes this a bit tricky. + // + // We need to make sure that the data is properly aligned. Unfortunately the best + // safe way to do this is to copy the data to another buffer and then append. + // + // TODO(notgull): It may be worth it to use `unsafe` to copy directly from + // `reply.value` to `data`; check if this is faster. Use benchmarks! + let old_len = data.len(); + let added_len = reply.value.len() / mem::size_of::(); + data.resize(old_len + added_len, T::zeroed()); + bytemuck::cast_slice_mut::(&mut data[old_len..]).copy_from_slice(&reply.value); + } + + // Check `bytes_after` to see if there are more windows to fetch. + self.offset += PROPERTY_BUFFER_SIZE; + Ok(reply.bytes_after != 0) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/wm.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/wm.rs new file mode 100644 index 0000000..be70b83 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/wm.rs @@ -0,0 +1,137 @@ +use std::sync::Mutex; + +use super::*; + +// https://specifications.freedesktop.org/wm-spec/latest/ar01s04.html#idm46075117309248 +pub const MOVERESIZE_TOPLEFT: isize = 0; +pub const MOVERESIZE_TOP: isize = 1; +pub const MOVERESIZE_TOPRIGHT: isize = 2; +pub const MOVERESIZE_RIGHT: isize = 3; +pub const MOVERESIZE_BOTTOMRIGHT: isize = 4; +pub const MOVERESIZE_BOTTOM: isize = 5; +pub const MOVERESIZE_BOTTOMLEFT: isize = 6; +pub const MOVERESIZE_LEFT: isize = 7; +pub const MOVERESIZE_MOVE: isize = 8; + +// This info is global to the window manager. +static SUPPORTED_HINTS: Mutex> = Mutex::new(Vec::new()); +static WM_NAME: Mutex> = Mutex::new(None); + +pub fn hint_is_supported(hint: xproto::Atom) -> bool { + (*SUPPORTED_HINTS.lock().unwrap()).contains(&hint) +} + +pub fn wm_name_is_one_of(names: &[&str]) -> bool { + if let Some(ref name) = *WM_NAME.lock().unwrap() { + names.contains(&name.as_str()) + } else { + false + } +} + +impl XConnection { + pub fn update_cached_wm_info(&self, root: xproto::Window) { + *SUPPORTED_HINTS.lock().unwrap() = self.get_supported_hints(root); + *WM_NAME.lock().unwrap() = self.get_wm_name(root); + } + + fn get_supported_hints(&self, root: xproto::Window) -> Vec { + let atoms = self.atoms(); + let supported_atom = atoms[_NET_SUPPORTED]; + self.get_property(root, supported_atom, xproto::Atom::from(xproto::AtomEnum::ATOM)) + .unwrap_or_else(|_| Vec::with_capacity(0)) + } + + #[allow(clippy::useless_conversion)] + fn get_wm_name(&self, root: xproto::Window) -> Option { + let atoms = self.atoms(); + let check_atom = atoms[_NET_SUPPORTING_WM_CHECK]; + let wm_name_atom = atoms[_NET_WM_NAME]; + + // Mutter/Muffin/Budgie doesn't have _NET_SUPPORTING_WM_CHECK in its _NET_SUPPORTED, despite + // it working and being supported. This has been reported upstream, but due to the + // inavailability of time machines, we'll just try to get _NET_SUPPORTING_WM_CHECK + // regardless of whether or not the WM claims to support it. + // + // Blackbox 0.70 also incorrectly reports not supporting this, though that appears to be + // fixed in 0.72. + // if !supported_hints.contains(&check_atom) { + // return None; + // } + + // IceWM (1.3.x and earlier) doesn't report supporting _NET_WM_NAME, but will nonetheless + // provide us with a value for it. Note that the unofficial 1.4 fork of IceWM works fine. + // if !supported_hints.contains(&wm_name_atom) { + // return None; + // } + + // Of the WMs tested, only xmonad and dwm fail to provide a WM name. + + // Querying this property on the root window will give us the ID of a child window created + // by the WM. + let root_window_wm_check = { + let result = self.get_property::( + root, + check_atom, + xproto::Atom::from(xproto::AtomEnum::WINDOW), + ); + + let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned()); + + wm_check? + }; + + // Querying the same property on the child window we were given, we should get this child + // window's ID again. + let child_window_wm_check = { + let result = self.get_property::( + root_window_wm_check.into(), + check_atom, + xproto::Atom::from(xproto::AtomEnum::WINDOW), + ); + + let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned()); + + wm_check? + }; + + // These values should be the same. + if root_window_wm_check != child_window_wm_check { + return None; + } + + // All of that work gives us a window ID that we can get the WM name from. + let wm_name = { + let atoms = self.atoms(); + let utf8_string_atom = atoms[UTF8_STRING]; + + let result = + self.get_property(root_window_wm_check.into(), wm_name_atom, utf8_string_atom); + + // IceWM requires this. IceWM was also the only WM tested that returns a null-terminated + // string. For more fun trivia, IceWM is also unique in including version and uname + // information in this string (this means you'll have to be careful if you want to match + // against it, though). + // The unofficial 1.4 fork of IceWM still includes the extra details, but properly + // returns a UTF8 string that isn't null-terminated. + let no_utf8 = if let Err(ref err) = result { + err.is_actual_property_type(xproto::Atom::from(xproto::AtomEnum::STRING)) + } else { + false + }; + + if no_utf8 { + self.get_property( + root_window_wm_check.into(), + wm_name_atom, + xproto::Atom::from(xproto::AtomEnum::STRING), + ) + } else { + result + } + } + .ok(); + + wm_name.and_then(|wm_name| String::from_utf8(wm_name).ok()) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/xmodmap.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/xmodmap.rs new file mode 100644 index 0000000..565fd72 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/util/xmodmap.rs @@ -0,0 +1,56 @@ +use std::collections::HashSet; +use std::slice; + +use x11_dl::xlib::{KeyCode as XKeyCode, XModifierKeymap}; + +// Offsets within XModifierKeymap to each set of keycodes. +// We are only interested in Shift, Control, Alt, and Logo. +// +// There are 8 sets total. The order of keycode sets is: +// Shift, Lock, Control, Mod1 (Alt), Mod2, Mod3, Mod4 (Logo), Mod5 +// +// https://tronche.com/gui/x/xlib/input/XSetModifierMapping.html +const NUM_MODS: usize = 8; + +/// Track which keys are modifiers, so we can properly replay them when they were filtered. +#[derive(Debug, Default)] +pub struct ModifierKeymap { + // Maps keycodes to modifiers + modifiers: HashSet, +} + +impl ModifierKeymap { + pub fn new() -> ModifierKeymap { + ModifierKeymap::default() + } + + pub fn is_modifier(&self, keycode: XKeyCode) -> bool { + self.modifiers.contains(&keycode) + } + + pub fn reload_from_x_connection(&mut self, xconn: &super::XConnection) { + unsafe { + let keymap = (xconn.xlib.XGetModifierMapping)(xconn.display); + + if keymap.is_null() { + return; + } + + self.reset_from_x_keymap(&*keymap); + + (xconn.xlib.XFreeModifiermap)(keymap); + } + } + + fn reset_from_x_keymap(&mut self, keymap: &XModifierKeymap) { + let keys_per_mod = keymap.max_keypermod as usize; + + let keys = unsafe { + slice::from_raw_parts(keymap.modifiermap as *const _, keys_per_mod * NUM_MODS) + }; + self.modifiers.clear(); + for key in keys { + self.modifiers.insert(*key); + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/window.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/window.rs new file mode 100644 index 0000000..7e15d5f --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/window.rs @@ -0,0 +1,1938 @@ +use std::ffi::CString; +use std::mem::replace; +use std::os::raw::*; +use std::path::Path; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::{cmp, env}; + +use tracing::{debug, info, warn}; +use x11rb::connection::Connection; +use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification}; +use x11rb::protocol::shape::{ConnectionExt as ShapeExt, SK, SO}; +use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle}; +use x11rb::protocol::{randr, xinput}; + +use crate::cursor::{Cursor, CustomCursor as RootCustomCursor}; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::{Event, InnerSizeWriter, WindowEvent}; +use crate::event_loop::AsyncRequestSerial; +use crate::platform::x11::WindowType; +use crate::platform_impl::x11::atoms::*; +use crate::platform_impl::x11::{ + xinput_fp1616_to_float, MonitorHandle as X11MonitorHandle, WakeSender, X11Error, +}; +use crate::platform_impl::{ + Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformCustomCursor, + PlatformIcon, VideoModeHandle as PlatformVideoModeHandle, +}; +use crate::window::{ + CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowLevel, +}; + +use super::util::{self, SelectedCursor}; +use super::{ + ffi, ActiveEventLoop, CookieResultExt, ImeRequest, ImeSender, VoidCookie, WindowId, XConnection, +}; + +#[derive(Debug)] +pub struct SharedState { + pub cursor_pos: Option<(f64, f64)>, + pub size: Option<(u32, u32)>, + pub position: Option<(i32, i32)>, + pub inner_position: Option<(i32, i32)>, + pub inner_position_rel_parent: Option<(i32, i32)>, + pub is_resizable: bool, + pub is_decorated: bool, + pub last_monitor: X11MonitorHandle, + pub dpi_adjusted: Option<(u32, u32)>, + pub(crate) fullscreen: Option, + // Set when application calls `set_fullscreen` when window is not visible + pub(crate) desired_fullscreen: Option>, + // Used to restore position after exiting fullscreen + pub restore_position: Option<(i32, i32)>, + // Used to restore video mode after exiting fullscreen + pub desktop_video_mode: Option<(randr::Crtc, randr::Mode)>, + pub frame_extents: Option, + pub min_inner_size: Option, + pub max_inner_size: Option, + pub resize_increments: Option, + pub base_size: Option, + pub visibility: Visibility, + pub has_focus: bool, + // Use `Option` to not apply hittest logic when it was never requested. + pub cursor_hittest: Option, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Visibility { + No, + Yes, + // Waiting for VisibilityNotify + YesWait, +} + +impl SharedState { + fn new(last_monitor: X11MonitorHandle, window_attributes: &WindowAttributes) -> Mutex { + let visibility = + if window_attributes.visible { Visibility::YesWait } else { Visibility::No }; + + Mutex::new(SharedState { + last_monitor, + visibility, + + is_resizable: window_attributes.resizable, + is_decorated: window_attributes.decorations, + cursor_pos: None, + size: None, + position: None, + inner_position: None, + inner_position_rel_parent: None, + dpi_adjusted: None, + fullscreen: None, + desired_fullscreen: None, + restore_position: None, + desktop_video_mode: None, + frame_extents: None, + min_inner_size: None, + max_inner_size: None, + resize_increments: None, + base_size: None, + has_focus: false, + cursor_hittest: None, + }) + } +} + +unsafe impl Send for UnownedWindow {} +unsafe impl Sync for UnownedWindow {} + +pub struct UnownedWindow { + pub(crate) xconn: Arc, // never changes + xwindow: xproto::Window, // never changes + #[allow(dead_code)] + visual: u32, // never changes + root: xproto::Window, // never changes + #[allow(dead_code)] + screen_id: i32, // never changes + selected_cursor: Mutex, + cursor_grabbed_mode: Mutex, + #[allow(clippy::mutex_atomic)] + cursor_visible: Mutex, + ime_sender: Mutex, + pub shared_state: Mutex, + redraw_sender: WakeSender, + activation_sender: WakeSender, +} + +macro_rules! leap { + ($e:expr) => { + match $e { + Ok(x) => x, + Err(err) => return Err(os_error!(OsError::XError(X11Error::from(err).into()))), + } + }; +} + +impl UnownedWindow { + #[allow(clippy::unnecessary_cast)] + pub(crate) fn new( + event_loop: &ActiveEventLoop, + window_attrs: WindowAttributes, + ) -> Result { + let xconn = &event_loop.xconn; + let atoms = xconn.atoms(); + + let screen_id = match window_attrs.platform_specific.x11.screen_id { + Some(id) => id, + None => xconn.default_screen_index() as c_int, + }; + + let screen = { + let screen_id_usize = usize::try_from(screen_id) + .map_err(|_| os_error!(OsError::Misc("screen id must be non-negative")))?; + xconn.xcb_connection().setup().roots.get(screen_id_usize).ok_or(os_error!( + OsError::Misc("requested screen id not present in server's response") + ))? + }; + + #[cfg(feature = "rwh_06")] + let root = match window_attrs.parent_window.as_ref().map(|handle| handle.0) { + Some(rwh_06::RawWindowHandle::Xlib(handle)) => handle.window as xproto::Window, + Some(rwh_06::RawWindowHandle::Xcb(handle)) => handle.window.get(), + Some(raw) => unreachable!("Invalid raw window handle {raw:?} on X11"), + None => screen.root, + }; + #[cfg(not(feature = "rwh_06"))] + let root = event_loop.root; + + let mut monitors = leap!(xconn.available_monitors()); + let guessed_monitor = if monitors.is_empty() { + X11MonitorHandle::dummy() + } else { + xconn + .query_pointer(root, util::VIRTUAL_CORE_POINTER) + .ok() + .and_then(|pointer_state| { + let (x, y) = (pointer_state.root_x as i64, pointer_state.root_y as i64); + + for i in 0..monitors.len() { + if monitors[i].rect.contains_point(x, y) { + return Some(monitors.swap_remove(i)); + } + } + + None + }) + .unwrap_or_else(|| monitors.swap_remove(0)) + }; + let scale_factor = guessed_monitor.scale_factor(); + + info!("Guessed window scale factor: {}", scale_factor); + + let max_inner_size: Option<(u32, u32)> = + window_attrs.max_inner_size.map(|size| size.to_physical::(scale_factor).into()); + let min_inner_size: Option<(u32, u32)> = + window_attrs.min_inner_size.map(|size| size.to_physical::(scale_factor).into()); + + let position = + window_attrs.position.map(|position| position.to_physical::(scale_factor)); + + let dimensions = { + // x11 only applies constraints when the window is actively resized + // by the user, so we have to manually apply the initial constraints + let mut dimensions: (u32, u32) = window_attrs + .inner_size + .map(|size| size.to_physical::(scale_factor)) + .or_else(|| Some((800, 600).into())) + .map(Into::into) + .unwrap(); + if let Some(max) = max_inner_size { + dimensions.0 = cmp::min(dimensions.0, max.0); + dimensions.1 = cmp::min(dimensions.1, max.1); + } + if let Some(min) = min_inner_size { + dimensions.0 = cmp::max(dimensions.0, min.0); + dimensions.1 = cmp::max(dimensions.1, min.1); + } + debug!("Calculated physical dimensions: {}x{}", dimensions.0, dimensions.1); + dimensions + }; + + // An iterator over the visuals matching screen id combined with their depths. + let mut all_visuals = screen + .allowed_depths + .iter() + .flat_map(|depth| depth.visuals.iter().map(move |visual| (visual, depth.depth))); + + // creating + let (visualtype, depth, require_colormap) = + match window_attrs.platform_specific.x11.visual_id { + Some(vi) => { + // Find this specific visual. + let (visualtype, depth) = + all_visuals.find(|(visual, _)| visual.visual_id == vi).ok_or_else( + || os_error!(OsError::XError(X11Error::NoSuchVisual(vi).into())), + )?; + + (Some(visualtype), depth, true) + }, + None if window_attrs.transparent => { + // Find a suitable visual, true color with 32 bits of depth. + all_visuals + .find_map(|(visual, depth)| { + (depth == 32 && visual.class == xproto::VisualClass::TRUE_COLOR) + .then_some((Some(visual), depth, true)) + }) + .unwrap_or_else(|| { + debug!( + "Could not set transparency, because XMatchVisualInfo returned \ + zero for the required parameters" + ); + (None as _, x11rb::COPY_FROM_PARENT as _, false) + }) + }, + _ => (None, x11rb::COPY_FROM_PARENT as _, false), + }; + let mut visual = visualtype.map_or(x11rb::COPY_FROM_PARENT, |v| v.visual_id); + + let window_attributes = { + use xproto::EventMask; + + let mut aux = xproto::CreateWindowAux::new(); + let event_mask = EventMask::EXPOSURE + | EventMask::STRUCTURE_NOTIFY + | EventMask::VISIBILITY_CHANGE + | EventMask::KEY_PRESS + | EventMask::KEY_RELEASE + | EventMask::KEYMAP_STATE + | EventMask::BUTTON_PRESS + | EventMask::BUTTON_RELEASE + | EventMask::POINTER_MOTION + | EventMask::PROPERTY_CHANGE; + + aux = aux.event_mask(event_mask).border_pixel(0); + + if window_attrs.platform_specific.x11.override_redirect { + aux = aux.override_redirect(true as u32); + } + + // Add a colormap if needed. + let colormap_visual = match window_attrs.platform_specific.x11.visual_id { + Some(vi) => Some(vi), + None if require_colormap => Some(visual), + _ => None, + }; + + if let Some(visual) = colormap_visual { + let colormap = leap!(xconn.xcb_connection().generate_id()); + leap!(xconn.xcb_connection().create_colormap( + xproto::ColormapAlloc::NONE, + colormap, + root, + visual, + )); + aux = aux.colormap(colormap); + } else { + aux = aux.colormap(0); + } + + aux + }; + + // Figure out the window's parent. + let parent = window_attrs.platform_specific.x11.embed_window.unwrap_or(root); + + // finally creating the window + let xwindow = { + let (x, y) = position.map_or((0, 0), Into::into); + let wid = leap!(xconn.xcb_connection().generate_id()); + let result = xconn.xcb_connection().create_window( + depth, + wid, + parent, + x, + y, + dimensions.0.try_into().unwrap(), + dimensions.1.try_into().unwrap(), + 0, + xproto::WindowClass::INPUT_OUTPUT, + visual, + &window_attributes, + ); + leap!(leap!(result).check()); + + wid + }; + + // The COPY_FROM_PARENT is a special value for the visual used to copy + // the visual from the parent window, thus we have to query the visual + // we've got when we built the window above. + if visual == x11rb::COPY_FROM_PARENT { + visual = leap!(leap!(xconn + .xcb_connection() + .get_window_attributes(xwindow as xproto::Window)) + .reply()) + .visual; + } + + #[allow(clippy::mutex_atomic)] + let mut window = UnownedWindow { + xconn: Arc::clone(xconn), + xwindow: xwindow as xproto::Window, + visual, + root, + screen_id, + selected_cursor: Default::default(), + cursor_grabbed_mode: Mutex::new(CursorGrabMode::None), + cursor_visible: Mutex::new(true), + ime_sender: Mutex::new(event_loop.ime_sender.clone()), + shared_state: SharedState::new(guessed_monitor, &window_attrs), + redraw_sender: event_loop.redraw_sender.clone(), + activation_sender: event_loop.activation_sender.clone(), + }; + + // Title must be set before mapping. Some tiling window managers (i.e. i3) use the window + // title to determine placement/etc., so doing this after mapping would cause the WM to + // act on the wrong title state. + leap!(window.set_title_inner(&window_attrs.title)).ignore_error(); + leap!(window.set_decorations_inner(window_attrs.decorations)).ignore_error(); + + if let Some(theme) = window_attrs.preferred_theme { + leap!(window.set_theme_inner(Some(theme))).ignore_error(); + } + + // Embed the window if needed. + if window_attrs.platform_specific.x11.embed_window.is_some() { + window.embed_window()?; + } + + { + // Enable drag and drop (TODO: extend API to make this toggleable) + { + let dnd_aware_atom = atoms[XdndAware]; + let version = &[5u32]; // Latest version; hasn't changed since 2002 + leap!(xconn.change_property( + window.xwindow, + dnd_aware_atom, + u32::from(xproto::AtomEnum::ATOM), + xproto::PropMode::REPLACE, + version, + )) + .ignore_error(); + } + + // WM_CLASS must be set *before* mapping the window, as per ICCCM! + { + let (instance, class) = if let Some(name) = window_attrs.platform_specific.name { + (name.instance, name.general) + } else { + let class = env::args_os() + .next() + .as_ref() + // Default to the name of the binary (via argv[0]) + .and_then(|path| Path::new(path).file_name()) + .and_then(|bin_name| bin_name.to_str()) + .map(|bin_name| bin_name.to_owned()) + .unwrap_or_else(|| window_attrs.title.clone()); + // This environment variable is extraordinarily unlikely to actually be used... + let instance = env::var("RESOURCE_NAME").ok().unwrap_or_else(|| class.clone()); + (instance, class) + }; + + let class = format!("{instance}\0{class}\0"); + leap!(xconn.change_property( + window.xwindow, + xproto::Atom::from(xproto::AtomEnum::WM_CLASS), + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, + class.as_bytes(), + )) + .ignore_error(); + } + + if let Some(flusher) = leap!(window.set_pid()) { + flusher.ignore_error() + } + + leap!(window.set_window_types(window_attrs.platform_specific.x11.x11_window_types)) + .ignore_error(); + + // Set size hints. + let mut min_inner_size = + window_attrs.min_inner_size.map(|size| size.to_physical::(scale_factor)); + let mut max_inner_size = + window_attrs.max_inner_size.map(|size| size.to_physical::(scale_factor)); + + if !window_attrs.resizable { + if util::wm_name_is_one_of(&["Xfwm4"]) { + warn!("To avoid a WM bug, disabling resizing has no effect on Xfwm4"); + } else { + max_inner_size = Some(dimensions.into()); + min_inner_size = Some(dimensions.into()); + } + } + + let shared_state = window.shared_state.get_mut().unwrap(); + shared_state.min_inner_size = min_inner_size.map(Into::into); + shared_state.max_inner_size = max_inner_size.map(Into::into); + shared_state.resize_increments = window_attrs.resize_increments; + shared_state.base_size = window_attrs.platform_specific.x11.base_size; + + let normal_hints = WmSizeHints { + position: position.map(|PhysicalPosition { x, y }| { + (WmSizeHintsSpecification::UserSpecified, x, y) + }), + size: Some(( + WmSizeHintsSpecification::UserSpecified, + cast_dimension_to_hint(dimensions.0), + cast_dimension_to_hint(dimensions.1), + )), + max_size: max_inner_size.map(cast_physical_size_to_hint), + min_size: min_inner_size.map(cast_physical_size_to_hint), + size_increment: window_attrs + .resize_increments + .map(|size| cast_size_to_hint(size, scale_factor)), + base_size: window_attrs + .platform_specific + .x11 + .base_size + .map(|size| cast_size_to_hint(size, scale_factor)), + aspect: None, + win_gravity: None, + }; + leap!(leap!(normal_hints.set( + xconn.xcb_connection(), + window.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )) + .check()); + + // Set window icons + if let Some(icon) = window_attrs.window_icon { + leap!(window.set_icon_inner(icon.inner)).ignore_error(); + } + + // Opt into handling window close + let result = xconn.xcb_connection().change_property( + xproto::PropMode::REPLACE, + window.xwindow, + atoms[WM_PROTOCOLS], + xproto::AtomEnum::ATOM, + 32, + 2, + bytemuck::cast_slice::(&[ + atoms[WM_DELETE_WINDOW], + atoms[_NET_WM_PING], + ]), + ); + leap!(result).ignore_error(); + + // Select XInput2 events + let mask = xinput::XIEventMask::MOTION + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE + | xinput::XIEventMask::FOCUS_IN + | xinput::XIEventMask::FOCUS_OUT + | xinput::XIEventMask::TOUCH_BEGIN + | xinput::XIEventMask::TOUCH_UPDATE + | xinput::XIEventMask::TOUCH_END; + leap!(xconn.select_xinput_events(window.xwindow, super::ALL_MASTER_DEVICES, mask)) + .ignore_error(); + + // Set visibility (map window) + if window_attrs.visible { + leap!(xconn.xcb_connection().map_window(window.xwindow)).ignore_error(); + leap!(xconn.xcb_connection().configure_window( + xwindow, + &xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE) + )) + .ignore_error(); + } + + // Attempt to make keyboard input repeat detectable + unsafe { + let mut supported_ptr = ffi::False; + (xconn.xlib.XkbSetDetectableAutoRepeat)( + xconn.display, + ffi::True, + &mut supported_ptr, + ); + if supported_ptr == ffi::False { + return Err(os_error!(OsError::Misc("`XkbSetDetectableAutoRepeat` failed"))); + } + } + + // Try to create input context for the window. + if let Some(ime) = event_loop.ime.as_ref() { + let result = ime.borrow_mut().create_context(window.xwindow as ffi::Window, false); + leap!(result); + } + + // These properties must be set after mapping + if window_attrs.maximized { + leap!(window.set_maximized_inner(window_attrs.maximized)).ignore_error(); + } + if window_attrs.fullscreen.is_some() { + if let Some(flusher) = + leap!(window + .set_fullscreen_inner(window_attrs.fullscreen.clone().map(Into::into))) + { + flusher.ignore_error() + } + + if let Some(PhysicalPosition { x, y }) = position { + let shared_state = window.shared_state.get_mut().unwrap(); + + shared_state.restore_position = Some((x, y)); + } + } + + leap!(window.set_window_level_inner(window_attrs.window_level)).ignore_error(); + } + + window.set_cursor(window_attrs.cursor); + + // Remove the startup notification if we have one. + if let Some(startup) = window_attrs.platform_specific.activation_token.as_ref() { + leap!(xconn.remove_activation_token(xwindow, &startup.token)); + } + + // We never want to give the user a broken window, since by then, it's too late to handle. + let window = leap!(xconn.sync_with_server().map(|_| window)); + + Ok(window) + } + + /// Embed this window into a parent window. + pub(super) fn embed_window(&self) -> Result<(), RootOsError> { + let atoms = self.xconn.atoms(); + leap!(leap!(self.xconn.change_property( + self.xwindow, + atoms[_XEMBED], + atoms[_XEMBED], + xproto::PropMode::REPLACE, + &[0u32, 1u32], + )) + .check()); + + Ok(()) + } + + pub(super) fn shared_state_lock(&self) -> MutexGuard<'_, SharedState> { + self.shared_state.lock().unwrap() + } + + fn set_pid(&self) -> Result>, X11Error> { + let atoms = self.xconn.atoms(); + let pid_atom = atoms[_NET_WM_PID]; + let client_machine_atom = atoms[WM_CLIENT_MACHINE]; + + // Get the hostname and the PID. + let uname = rustix::system::uname(); + let pid = rustix::process::getpid(); + + self.xconn + .change_property( + self.xwindow, + pid_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, + &[pid.as_raw_nonzero().get() as util::Cardinal], + )? + .ignore_error(); + let flusher = self.xconn.change_property( + self.xwindow, + client_machine_atom, + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, + uname.nodename().to_bytes(), + ); + flusher.map(Some) + } + + fn set_window_types(&self, window_types: Vec) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let hint_atom = atoms[_NET_WM_WINDOW_TYPE]; + let atoms: Vec<_> = window_types.iter().map(|t| t.as_atom(&self.xconn)).collect(); + + self.xconn.change_property( + self.xwindow, + hint_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + xproto::PropMode::REPLACE, + &atoms, + ) + } + + pub fn set_theme_inner(&self, theme: Option) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let hint_atom = atoms[_GTK_THEME_VARIANT]; + let utf8_atom = atoms[UTF8_STRING]; + let variant = match theme { + Some(Theme::Dark) => "dark", + Some(Theme::Light) => "light", + None => "dark", + }; + let variant = CString::new(variant).expect("`_GTK_THEME_VARIANT` contained null byte"); + self.xconn.change_property( + self.xwindow, + hint_atom, + utf8_atom, + xproto::PropMode::REPLACE, + variant.as_bytes(), + ) + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + self.set_theme_inner(theme).expect("Failed to change window theme").ignore_error(); + + self.xconn.flush_requests().expect("Failed to change window theme"); + } + + fn set_netwm( + &self, + operation: util::StateOperation, + properties: (u32, u32, u32, u32), + ) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; + self.xconn.send_client_msg( + self.xwindow, + self.root, + state_atom, + Some(xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY), + [operation as u32, properties.0, properties.1, properties.2, properties.3], + ) + } + + fn set_fullscreen_hint(&self, fullscreen: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let fullscreen_atom = atoms[_NET_WM_STATE_FULLSCREEN]; + let flusher = self.set_netwm(fullscreen.into(), (fullscreen_atom, 0, 0, 0)); + + if fullscreen { + // Ensure that the fullscreen window receives input focus to prevent + // locking up the user's display. + self.xconn + .xcb_connection() + .set_input_focus(xproto::InputFocus::PARENT, self.xwindow, x11rb::CURRENT_TIME)? + .ignore_error(); + } + + flusher + } + + fn set_fullscreen_inner( + &self, + fullscreen: Option, + ) -> Result>, X11Error> { + let mut shared_state_lock = self.shared_state_lock(); + + match shared_state_lock.visibility { + // Setting fullscreen on a window that is not visible will generate an error. + Visibility::No | Visibility::YesWait => { + shared_state_lock.desired_fullscreen = Some(fullscreen); + return Ok(None); + }, + Visibility::Yes => (), + } + + let old_fullscreen = shared_state_lock.fullscreen.clone(); + if old_fullscreen == fullscreen { + return Ok(None); + } + shared_state_lock.fullscreen.clone_from(&fullscreen); + + match (&old_fullscreen, &fullscreen) { + // Store the desktop video mode before entering exclusive + // fullscreen, so we can restore it upon exit, as XRandR does not + // provide a mechanism to set this per app-session or restore this + // to the desktop video mode as macOS and Windows do + (&None, &Some(Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode)))) + | ( + &Some(Fullscreen::Borderless(_)), + &Some(Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode))), + ) => { + let monitor = video_mode.monitor.as_ref().unwrap(); + shared_state_lock.desktop_video_mode = Some(( + monitor.id, + self.xconn.get_crtc_mode(monitor.id).expect("Failed to get desktop video mode"), + )); + }, + // Restore desktop video mode upon exiting exclusive fullscreen + (&Some(Fullscreen::Exclusive(_)), &None) + | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Borderless(_))) => { + let (monitor_id, mode_id) = shared_state_lock.desktop_video_mode.take().unwrap(); + self.xconn + .set_crtc_config(monitor_id, mode_id) + .expect("failed to restore desktop video mode"); + }, + _ => (), + } + + drop(shared_state_lock); + + match fullscreen { + None => { + let flusher = self.set_fullscreen_hint(false); + let mut shared_state_lock = self.shared_state_lock(); + if let Some(position) = shared_state_lock.restore_position.take() { + drop(shared_state_lock); + self.set_position_inner(position.0, position.1) + .expect_then_ignore_error("Failed to restore window position"); + } + flusher.map(Some) + }, + Some(fullscreen) => { + let (video_mode, monitor) = match fullscreen { + Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode)) => { + (Some(video_mode), video_mode.monitor.clone().unwrap()) + }, + Fullscreen::Borderless(Some(PlatformMonitorHandle::X(monitor))) => { + (None, monitor) + }, + Fullscreen::Borderless(None) => { + (None, self.shared_state_lock().last_monitor.clone()) + }, + #[cfg(wayland_platform)] + _ => unreachable!(), + }; + + // Don't set fullscreen on an invalid dummy monitor handle + if monitor.is_dummy() { + return Ok(None); + } + + if let Some(video_mode) = video_mode { + // FIXME: this is actually not correct if we're setting the + // video mode to a resolution higher than the current + // desktop resolution, because XRandR does not automatically + // reposition the monitors to the right and below this + // monitor. + // + // What ends up happening is we will get the fullscreen + // window showing up on those monitors as well, because + // their virtual position now overlaps with the monitor that + // we just made larger.. + // + // It'd be quite a bit of work to handle this correctly (and + // nobody else seems to bother doing this correctly either), + // so we're just leaving this broken. Fixing this would + // involve storing all CRTCs upon entering fullscreen, + // restoring them upon exit, and after entering fullscreen, + // repositioning displays to the right and below this + // display. I think there would still be edge cases that are + // difficult or impossible to handle correctly, e.g. what if + // a new monitor was plugged in while in fullscreen? + // + // I think we might just want to disallow setting the video + // mode higher than the current desktop video mode (I'm sure + // this will make someone unhappy, but it's very unusual for + // games to want to do this anyway). + self.xconn + .set_crtc_config(monitor.id, video_mode.native_mode) + .expect("failed to set video mode"); + } + + let window_position = self.outer_position_physical(); + self.shared_state_lock().restore_position = Some(window_position); + let monitor_origin: (i32, i32) = monitor.position().into(); + self.set_position_inner(monitor_origin.0, monitor_origin.1) + .expect_then_ignore_error("Failed to set window position"); + self.set_fullscreen_hint(true).map(Some) + }, + } + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + let shared_state = self.shared_state_lock(); + + shared_state.desired_fullscreen.clone().unwrap_or_else(|| shared_state.fullscreen.clone()) + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + if let Some(flusher) = + self.set_fullscreen_inner(fullscreen).expect("Failed to change window fullscreen state") + { + flusher.check().expect("Failed to change window fullscreen state"); + self.invalidate_cached_frame_extents(); + } + } + + // Called by EventProcessor when a VisibilityNotify event is received + pub(crate) fn visibility_notify(&self) { + let mut shared_state = self.shared_state_lock(); + + match shared_state.visibility { + Visibility::No => self + .xconn + .xcb_connection() + .unmap_window(self.xwindow) + .expect_then_ignore_error("Failed to unmap window"), + Visibility::Yes => (), + Visibility::YesWait => { + shared_state.visibility = Visibility::Yes; + + if let Some(fullscreen) = shared_state.desired_fullscreen.take() { + drop(shared_state); + self.set_fullscreen(fullscreen); + } + }, + } + } + + pub fn current_monitor(&self) -> Option { + Some(self.shared_state_lock().last_monitor.clone()) + } + + pub fn available_monitors(&self) -> Vec { + self.xconn.available_monitors().expect("Failed to get available monitors") + } + + pub fn primary_monitor(&self) -> Option { + Some(self.xconn.primary_monitor().expect("Failed to get primary monitor")) + } + + #[inline] + pub fn is_minimized(&self) -> Option { + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; + let state = self.xconn.get_property( + self.xwindow, + state_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ); + let hidden_atom = atoms[_NET_WM_STATE_HIDDEN]; + + Some(match state { + Ok(atoms) => { + atoms.iter().any(|atom: &xproto::Atom| *atom as xproto::Atom == hidden_atom) + }, + _ => false, + }) + } + + /// Refresh the API for the given monitor. + #[inline] + pub(super) fn refresh_dpi_for_monitor( + &self, + new_monitor: &X11MonitorHandle, + maybe_prev_scale_factor: Option, + mut callback: impl FnMut(Event), + ) { + // Check if the self is on this monitor + let monitor = self.shared_state_lock().last_monitor.clone(); + if monitor.name == new_monitor.name { + let (width, height) = self.inner_size_physical(); + let (new_width, new_height) = self.adjust_for_dpi( + // If we couldn't determine the previous scale + // factor (e.g., because all monitors were closed + // before), just pick whatever the current monitor + // has set as a baseline. + maybe_prev_scale_factor.unwrap_or(monitor.scale_factor), + new_monitor.scale_factor, + width, + height, + &self.shared_state_lock(), + ); + + let window_id = crate::window::WindowId(self.id()); + let old_inner_size = PhysicalSize::new(width, height); + let inner_size = Arc::new(Mutex::new(PhysicalSize::new(new_width, new_height))); + callback(Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor: new_monitor.scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&inner_size)), + }, + }); + + let new_inner_size = *inner_size.lock().unwrap(); + drop(inner_size); + + if new_inner_size != old_inner_size { + let (new_width, new_height) = new_inner_size.into(); + self.request_inner_size_physical(new_width, new_height); + } + } + } + + fn set_minimized_inner(&self, minimized: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + + if minimized { + let root_window = self.xconn.default_root().root; + + self.xconn.send_client_msg( + self.xwindow, + root_window, + atoms[WM_CHANGE_STATE], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [3u32, 0, 0, 0, 0], + ) + } else { + self.xconn.send_client_msg( + self.xwindow, + self.root, + atoms[_NET_ACTIVE_WINDOW], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [1, x11rb::CURRENT_TIME, 0, 0, 0], + ) + } + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + self.set_minimized_inner(minimized) + .expect_then_ignore_error("Failed to change window minimization"); + + self.xconn.flush_requests().expect("Failed to change window minimization"); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; + let state = self.xconn.get_property( + self.xwindow, + state_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ); + let horz_atom = atoms[_NET_WM_STATE_MAXIMIZED_HORZ]; + let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT]; + match state { + Ok(atoms) => { + let horz_maximized = atoms.contains(&horz_atom); + let vert_maximized = atoms.contains(&vert_atom); + horz_maximized && vert_maximized + }, + _ => false, + } + } + + fn set_maximized_inner(&self, maximized: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let horz_atom = atoms[_NET_WM_STATE_MAXIMIZED_HORZ]; + let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT]; + + self.set_netwm(maximized.into(), (horz_atom, vert_atom, 0, 0)) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + self.set_maximized_inner(maximized) + .expect_then_ignore_error("Failed to change window maximization"); + self.xconn.flush_requests().expect("Failed to change window maximization"); + self.invalidate_cached_frame_extents(); + } + + fn set_title_inner(&self, title: &str) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + + let title = CString::new(title).expect("Window title contained null byte"); + self.xconn + .change_property( + self.xwindow, + xproto::Atom::from(xproto::AtomEnum::WM_NAME), + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, + title.as_bytes(), + )? + .ignore_error(); + self.xconn.change_property( + self.xwindow, + atoms[_NET_WM_NAME], + atoms[UTF8_STRING], + xproto::PropMode::REPLACE, + title.as_bytes(), + ) + } + + #[inline] + pub fn set_title(&self, title: &str) { + self.set_title_inner(title).expect_then_ignore_error("Failed to set window title"); + + self.xconn.flush_requests().expect("Failed to set window title"); + } + + #[inline] + pub fn set_transparent(&self, _transparent: bool) {} + + #[inline] + pub fn set_blur(&self, _blur: bool) {} + + fn set_decorations_inner(&self, decorations: bool) -> Result, X11Error> { + self.shared_state_lock().is_decorated = decorations; + let mut hints = self.xconn.get_motif_hints(self.xwindow); + + hints.set_decorations(decorations); + + self.xconn.set_motif_hints(self.xwindow, &hints) + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + self.set_decorations_inner(decorations) + .expect_then_ignore_error("Failed to set decoration state"); + self.xconn.flush_requests().expect("Failed to set decoration state"); + self.invalidate_cached_frame_extents(); + } + + #[inline] + pub fn is_decorated(&self) -> bool { + self.shared_state_lock().is_decorated + } + + fn set_maximizable_inner(&self, maximizable: bool) -> Result, X11Error> { + let mut hints = self.xconn.get_motif_hints(self.xwindow); + + hints.set_maximizable(maximizable); + + self.xconn.set_motif_hints(self.xwindow, &hints) + } + + fn toggle_atom(&self, atom_name: AtomName, enable: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let atom = atoms[atom_name]; + self.set_netwm(enable.into(), (atom, 0, 0, 0)) + } + + fn set_window_level_inner(&self, level: WindowLevel) -> Result, X11Error> { + self.toggle_atom(_NET_WM_STATE_ABOVE, level == WindowLevel::AlwaysOnTop)?.ignore_error(); + self.toggle_atom(_NET_WM_STATE_BELOW, level == WindowLevel::AlwaysOnBottom) + } + + #[inline] + pub fn set_window_level(&self, level: WindowLevel) { + self.set_window_level_inner(level) + .expect_then_ignore_error("Failed to set window-level state"); + self.xconn.flush_requests().expect("Failed to set window-level state"); + } + + fn set_icon_inner(&self, icon: PlatformIcon) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let icon_atom = atoms[_NET_WM_ICON]; + let data = icon.to_cardinals(); + self.xconn.change_property( + self.xwindow, + icon_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, + data.as_slice(), + ) + } + + fn unset_icon_inner(&self) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let icon_atom = atoms[_NET_WM_ICON]; + let empty_data: [util::Cardinal; 0] = []; + self.xconn.change_property( + self.xwindow, + icon_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, + &empty_data, + ) + } + + #[inline] + pub(crate) fn set_window_icon(&self, icon: Option) { + match icon { + Some(icon) => self.set_icon_inner(icon), + None => self.unset_icon_inner(), + } + .expect_then_ignore_error("Failed to set icons"); + + self.xconn.flush_requests().expect("Failed to set icons"); + } + + #[inline] + pub fn set_visible(&self, visible: bool) { + let mut shared_state = self.shared_state_lock(); + + match (visible, shared_state.visibility) { + (true, Visibility::Yes) | (true, Visibility::YesWait) | (false, Visibility::No) => { + return + }, + _ => (), + } + + if visible { + self.xconn + .xcb_connection() + .map_window(self.xwindow) + .expect_then_ignore_error("Failed to call `xcb_map_window`"); + self.xconn + .xcb_connection() + .configure_window( + self.xwindow, + &xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE), + ) + .expect_then_ignore_error("Failed to call `xcb_configure_window`"); + self.xconn.flush_requests().expect("Failed to call XMapRaised"); + shared_state.visibility = Visibility::YesWait; + } else { + self.xconn + .xcb_connection() + .unmap_window(self.xwindow) + .expect_then_ignore_error("Failed to call `xcb_unmap_window`"); + self.xconn.flush_requests().expect("Failed to call XUnmapWindow"); + shared_state.visibility = Visibility::No; + } + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(self.shared_state_lock().visibility == Visibility::Yes) + } + + fn update_cached_frame_extents(&self) { + let extents = self.xconn.get_frame_extents_heuristic(self.xwindow, self.root); + self.shared_state_lock().frame_extents = Some(extents); + } + + pub(crate) fn invalidate_cached_frame_extents(&self) { + self.shared_state_lock().frame_extents.take(); + } + + pub(crate) fn outer_position_physical(&self) -> (i32, i32) { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + let (x, y) = self.inner_position_physical(); + extents.inner_pos_to_outer(x, y) + } else { + self.update_cached_frame_extents(); + self.outer_position_physical() + } + } + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + let (x, y) = self.inner_position_physical(); + Ok(extents.inner_pos_to_outer(x, y).into()) + } else { + self.update_cached_frame_extents(); + self.outer_position() + } + } + + pub(crate) fn inner_position_physical(&self) -> (i32, i32) { + // This should be okay to unwrap since the only error XTranslateCoordinates can return + // is BadWindow, and if the window handle is bad we have bigger problems. + self.xconn + .translate_coords(self.xwindow, self.root) + .map(|coords| (coords.dst_x.into(), coords.dst_y.into())) + .unwrap() + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + Ok(self.inner_position_physical().into()) + } + + pub(crate) fn set_position_inner( + &self, + mut x: i32, + mut y: i32, + ) -> Result, X11Error> { + // There are a few WMs that set client area position rather than window position, so + // we'll translate for consistency. + if util::wm_name_is_one_of(&["Enlightenment", "FVWM"]) { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + x += cast_dimension_to_hint(extents.frame_extents.left); + y += cast_dimension_to_hint(extents.frame_extents.top); + } else { + self.update_cached_frame_extents(); + return self.set_position_inner(x, y); + } + } + + self.xconn + .xcb_connection() + .configure_window(self.xwindow, &xproto::ConfigureWindowAux::new().x(x).y(y)) + .map_err(Into::into) + } + + pub(crate) fn set_position_physical(&self, x: i32, y: i32) { + self.set_position_inner(x, y).expect_then_ignore_error("Failed to call `XMoveWindow`"); + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + let (x, y) = position.to_physical::(self.scale_factor()).into(); + self.set_position_physical(x, y); + } + + pub(crate) fn inner_size_physical(&self) -> (u32, u32) { + // This should be okay to unwrap since the only error XGetGeometry can return + // is BadWindow, and if the window handle is bad we have bigger problems. + self.xconn + .get_geometry(self.xwindow) + .map(|geo| (geo.width.into(), geo.height.into())) + .unwrap() + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + self.inner_size_physical().into() + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + let (width, height) = self.inner_size_physical(); + extents.inner_size_to_outer(width, height).into() + } else { + self.update_cached_frame_extents(); + self.outer_size() + } + } + + pub(crate) fn request_inner_size_physical(&self, width: u32, height: u32) { + self.xconn + .xcb_connection() + .configure_window( + self.xwindow, + &xproto::ConfigureWindowAux::new().width(width).height(height), + ) + .expect_then_ignore_error("Failed to call `xcb_configure_window`"); + self.xconn.flush_requests().expect("Failed to call XResizeWindow"); + // cursor_hittest needs to be reapplied after each window resize. + if self.shared_state_lock().cursor_hittest.unwrap_or(false) { + let _ = self.set_cursor_hittest(true); + } + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let scale_factor = self.scale_factor(); + let size = size.to_physical::(scale_factor).into(); + if !self.shared_state_lock().is_resizable { + self.update_normal_hints(|normal_hints| { + normal_hints.min_size = Some(size); + normal_hints.max_size = Some(size); + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + self.request_inner_size_physical(size.0 as u32, size.1 as u32); + + None + } + + fn update_normal_hints(&self, callback: F) -> Result<(), X11Error> + where + F: FnOnce(&mut WmSizeHints), + { + let mut normal_hints = WmSizeHints::get( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )? + .reply()? + .unwrap_or_default(); + callback(&mut normal_hints); + normal_hints + .set( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )? + .ignore_error(); + Ok(()) + } + + pub(crate) fn set_min_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { + self.update_normal_hints(|normal_hints| { + normal_hints.min_size = + dimensions.map(|(w, h)| (cast_dimension_to_hint(w), cast_dimension_to_hint(h))) + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + + #[inline] + pub fn set_min_inner_size(&self, dimensions: Option) { + self.shared_state_lock().min_inner_size = dimensions; + let physical_dimensions = + dimensions.map(|dimensions| dimensions.to_physical::(self.scale_factor()).into()); + self.set_min_inner_size_physical(physical_dimensions); + } + + pub(crate) fn set_max_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { + self.update_normal_hints(|normal_hints| { + normal_hints.max_size = + dimensions.map(|(w, h)| (cast_dimension_to_hint(w), cast_dimension_to_hint(h))) + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + + #[inline] + pub fn set_max_inner_size(&self, dimensions: Option) { + self.shared_state_lock().max_inner_size = dimensions; + let physical_dimensions = + dimensions.map(|dimensions| dimensions.to_physical::(self.scale_factor()).into()); + self.set_max_inner_size_physical(physical_dimensions); + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + WmSizeHints::get( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + ) + .ok() + .and_then(|cookie| cookie.reply().ok()) + .flatten() + .and_then(|hints| hints.size_increment) + .map(|(width, height)| (width as u32, height as u32).into()) + } + + #[inline] + pub fn set_resize_increments(&self, increments: Option) { + self.shared_state_lock().resize_increments = increments; + let physical_increments = + increments.map(|increments| cast_size_to_hint(increments, self.scale_factor())); + self.update_normal_hints(|hints| hints.size_increment = physical_increments) + .expect("Failed to call `XSetWMNormalHints`"); + } + + pub(crate) fn adjust_for_dpi( + &self, + old_scale_factor: f64, + new_scale_factor: f64, + width: u32, + height: u32, + shared_state: &SharedState, + ) -> (u32, u32) { + let scale_factor = new_scale_factor / old_scale_factor; + self.update_normal_hints(|normal_hints| { + let dpi_adjuster = |size: Size| -> (i32, i32) { cast_size_to_hint(size, scale_factor) }; + let max_size = shared_state.max_inner_size.map(dpi_adjuster); + let min_size = shared_state.min_inner_size.map(dpi_adjuster); + let resize_increments = shared_state.resize_increments.map(dpi_adjuster); + let base_size = shared_state.base_size.map(dpi_adjuster); + + normal_hints.max_size = max_size; + normal_hints.min_size = min_size; + normal_hints.size_increment = resize_increments; + normal_hints.base_size = base_size; + }) + .expect("Failed to update normal hints"); + + let new_width = (width as f64 * scale_factor).round() as u32; + let new_height = (height as f64 * scale_factor).round() as u32; + + (new_width, new_height) + } + + pub fn set_resizable(&self, resizable: bool) { + if util::wm_name_is_one_of(&["Xfwm4"]) { + // Making the window unresizable on Xfwm prevents further changes to `WM_NORMAL_HINTS` + // from being detected. This makes it impossible for resizing to be + // re-enabled, and also breaks DPI scaling. As such, we choose the lesser of + // two evils and do nothing. + warn!("To avoid a WM bug, disabling resizing has no effect on Xfwm4"); + return; + } + + let (min_size, max_size) = if resizable { + let shared_state_lock = self.shared_state_lock(); + (shared_state_lock.min_inner_size, shared_state_lock.max_inner_size) + } else { + let window_size = Some(Size::from(self.inner_size())); + (window_size, window_size) + }; + self.shared_state_lock().is_resizable = resizable; + + self.set_maximizable_inner(resizable) + .expect_then_ignore_error("Failed to call `XSetWMNormalHints`"); + + let scale_factor = self.scale_factor(); + let min_inner_size = min_size.map(|size| cast_size_to_hint(size, scale_factor)); + let max_inner_size = max_size.map(|size| cast_size_to_hint(size, scale_factor)); + self.update_normal_hints(|normal_hints| { + normal_hints.min_size = min_inner_size; + normal_hints.max_size = max_inner_size; + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.shared_state_lock().is_resizable + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {} + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + WindowButtons::all() + } + + #[allow(dead_code)] + #[inline] + pub fn xlib_display(&self) -> *mut c_void { + self.xconn.display as _ + } + + #[allow(dead_code)] + #[inline] + pub fn xlib_window(&self) -> c_ulong { + self.xwindow as ffi::Window + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + match cursor { + Cursor::Icon(icon) => { + let old_cursor = replace( + &mut *self.selected_cursor.lock().unwrap(), + SelectedCursor::Named(icon), + ); + + #[allow(clippy::mutex_atomic)] + if SelectedCursor::Named(icon) != old_cursor && *self.cursor_visible.lock().unwrap() + { + self.xconn.set_cursor_icon(self.xwindow, Some(icon)); + } + }, + Cursor::Custom(RootCustomCursor { inner: PlatformCustomCursor::X(cursor) }) => { + #[allow(clippy::mutex_atomic)] + if *self.cursor_visible.lock().unwrap() { + self.xconn.set_custom_cursor(self.xwindow, &cursor); + } + + *self.selected_cursor.lock().unwrap() = SelectedCursor::Custom(cursor); + }, + #[cfg(wayland_platform)] + Cursor::Custom(RootCustomCursor { inner: PlatformCustomCursor::Wayland(_) }) => { + tracing::error!("passed a Wayland cursor to X11 backend") + }, + } + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + // We don't support the locked cursor yet, so ignore it early on. + if mode == CursorGrabMode::Locked { + return Err(ExternalError::NotSupported(NotSupportedError::new())); + } + + let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap(); + if mode == *grabbed_lock { + return Ok(()); + } + + // We ungrab before grabbing to prevent passive grabs from causing `AlreadyGrabbed`. + // Therefore, this is common to both codepaths. + self.xconn + .xcb_connection() + .ungrab_pointer(x11rb::CURRENT_TIME) + .expect_then_ignore_error("Failed to call `xcb_ungrab_pointer`"); + *grabbed_lock = CursorGrabMode::None; + + let result = match mode { + CursorGrabMode::None => self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + }), + CursorGrabMode::Confined => { + let result = self + .xconn + .xcb_connection() + .grab_pointer( + true as _, + self.xwindow, + xproto::EventMask::BUTTON_PRESS + | xproto::EventMask::BUTTON_RELEASE + | xproto::EventMask::ENTER_WINDOW + | xproto::EventMask::LEAVE_WINDOW + | xproto::EventMask::POINTER_MOTION + | xproto::EventMask::POINTER_MOTION_HINT + | xproto::EventMask::BUTTON1_MOTION + | xproto::EventMask::BUTTON2_MOTION + | xproto::EventMask::BUTTON3_MOTION + | xproto::EventMask::BUTTON4_MOTION + | xproto::EventMask::BUTTON5_MOTION + | xproto::EventMask::KEYMAP_STATE, + xproto::GrabMode::ASYNC, + xproto::GrabMode::ASYNC, + self.xwindow, + 0u32, + x11rb::CURRENT_TIME, + ) + .expect("Failed to call `grab_pointer`") + .reply() + .expect("Failed to receive reply from `grab_pointer`"); + + match result.status { + xproto::GrabStatus::SUCCESS => Ok(()), + xproto::GrabStatus::ALREADY_GRABBED => { + Err("Cursor could not be confined: already confined by another client") + }, + xproto::GrabStatus::INVALID_TIME => { + Err("Cursor could not be confined: invalid time") + }, + xproto::GrabStatus::NOT_VIEWABLE => { + Err("Cursor could not be confined: confine location not viewable") + }, + xproto::GrabStatus::FROZEN => { + Err("Cursor could not be confined: frozen by another client") + }, + _ => unreachable!(), + } + .map_err(|err| ExternalError::Os(os_error!(OsError::Misc(err)))) + }, + CursorGrabMode::Locked => return Ok(()), + }; + + if result.is_ok() { + *grabbed_lock = mode; + } + + result + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + #[allow(clippy::mutex_atomic)] + let mut visible_lock = self.cursor_visible.lock().unwrap(); + if visible == *visible_lock { + return; + } + let cursor = + if visible { Some((*self.selected_cursor.lock().unwrap()).clone()) } else { None }; + *visible_lock = visible; + drop(visible_lock); + match cursor { + Some(SelectedCursor::Custom(cursor)) => { + self.xconn.set_custom_cursor(self.xwindow, &cursor); + }, + Some(SelectedCursor::Named(cursor)) => { + self.xconn.set_cursor_icon(self.xwindow, Some(cursor)); + }, + None => { + self.xconn.set_cursor_icon(self.xwindow, None); + }, + } + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.shared_state_lock().last_monitor.scale_factor + } + + pub fn set_cursor_position_physical(&self, x: i32, y: i32) -> Result<(), ExternalError> { + { + self.xconn + .xcb_connection() + .warp_pointer(x11rb::NONE, self.xwindow, 0, 0, 0, 0, x as _, y as _) + .map_err(|e| { + ExternalError::Os(os_error!(OsError::XError(X11Error::from(e).into()))) + })?; + self.xconn.flush_requests().map_err(|e| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(e).into()))) + }) + } + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + let (x, y) = position.to_physical::(self.scale_factor()).into(); + self.set_cursor_position_physical(x, y) + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + // Implement cursor hittest for X11 by either setting an empty or full window input shape. + + // In X11, every window has two "shapes": + // * Bounding shape: defines the visible outline of the window. + // * Input shape: defines the region of the window that receives pointer/keyboard events. + // If the input shape is the full window rectangle, the window behaves normally. + // If the input shape is empty, the window is completely click‑through. + // Here, we implement hit test by mapping `hittest = true` to "restore a full input shape" + // and `hittest = false` to "clear the input shape" (empty list of rectangles). + let mut rectangles: Vec = Vec::new(); + if hittest { + let size = self.inner_size(); + rectangles.push(Rectangle { + x: 0, + y: 0, + width: size.width as u16, + height: size.height as u16, + }) + } + + self.xconn + .xcb_connection() + .shape_rectangles( + SO::SET, + SK::INPUT, + ClipOrdering::UNSORTED, + self.xwindow, + 0, + 0, + &rectangles, + ) + .map_err(|_e| ExternalError::Ignored)?; + self.shared_state_lock().cursor_hittest = Some(hittest); + Ok(()) + } + + /// Moves the window while it is being dragged. + pub fn drag_window(&self) -> Result<(), ExternalError> { + self.drag_initiate(util::MOVERESIZE_MOVE) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + /// Resizes the window while it is being dragged. + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + self.drag_initiate(match direction { + ResizeDirection::East => util::MOVERESIZE_RIGHT, + ResizeDirection::North => util::MOVERESIZE_TOP, + ResizeDirection::NorthEast => util::MOVERESIZE_TOPRIGHT, + ResizeDirection::NorthWest => util::MOVERESIZE_TOPLEFT, + ResizeDirection::South => util::MOVERESIZE_BOTTOM, + ResizeDirection::SouthEast => util::MOVERESIZE_BOTTOMRIGHT, + ResizeDirection::SouthWest => util::MOVERESIZE_BOTTOMLEFT, + ResizeDirection::West => util::MOVERESIZE_LEFT, + }) + } + + /// Initiates a drag operation while the left mouse button is pressed. + fn drag_initiate(&self, action: isize) -> Result<(), ExternalError> { + let pointer = self + .xconn + .query_pointer(self.xwindow, util::VIRTUAL_CORE_POINTER) + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err.into()))))?; + + let window = self.inner_position().map_err(ExternalError::NotSupported)?; + + let atoms = self.xconn.atoms(); + let message = atoms[_NET_WM_MOVERESIZE]; + + // we can't use `set_cursor_grab(false)` here because it doesn't run `XUngrabPointer` + // if the cursor isn't currently grabbed + let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap(); + self.xconn + .xcb_connection() + .ungrab_pointer(x11rb::CURRENT_TIME) + .map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::from(err).into()))) + })? + .ignore_error(); + self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + })?; + *grabbed_lock = CursorGrabMode::None; + + // we keep the lock until we are done + self.xconn + .send_client_msg( + self.xwindow, + self.root, + message, + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [ + (window.x + xinput_fp1616_to_float(pointer.win_x) as i32) as u32, + (window.y + xinput_fp1616_to_float(pointer.win_y) as i32) as u32, + action.try_into().unwrap(), + 1, // Button 1 + 1, + ], + ) + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err.into()))))?; + + self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + }) + } + + #[inline] + pub fn set_ime_cursor_area(&self, spot: Position, _size: Size) { + let (x, y) = spot.to_physical::(self.scale_factor()).into(); + let _ = self.ime_sender.lock().unwrap().send(ImeRequest::Position( + self.xwindow as ffi::Window, + x, + y, + )); + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let _ = self + .ime_sender + .lock() + .unwrap() + .send(ImeRequest::Allow(self.xwindow as ffi::Window, allowed)); + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn focus_window(&self) { + let atoms = self.xconn.atoms(); + let state_atom = atoms[WM_STATE]; + let state_type_atom = atoms[CARD32]; + let is_minimized = if let Ok(state) = + self.xconn.get_property::(self.xwindow, state_atom, state_type_atom) + { + state.contains(&super::ICONIC_STATE) + } else { + false + }; + let is_visible = match self.shared_state_lock().visibility { + Visibility::Yes => true, + Visibility::YesWait | Visibility::No => false, + }; + + if is_visible && !is_minimized { + self.xconn + .send_client_msg( + self.xwindow, + self.root, + atoms[_NET_ACTIVE_WINDOW], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [1, x11rb::CURRENT_TIME, 0, 0, 0], + ) + .expect_then_ignore_error("Failed to send client message"); + if let Err(e) = self.xconn.flush_requests() { + tracing::error!( + "`flush` returned an error when focusing the window. Error was: {}", + e + ); + } + } + } + + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let mut wm_hints = + WmHints::get(self.xconn.xcb_connection(), self.xwindow as xproto::Window) + .ok() + .and_then(|cookie| cookie.reply().ok()) + .flatten() + .unwrap_or_default(); + + wm_hints.urgent = request_type.is_some(); + wm_hints + .set(self.xconn.xcb_connection(), self.xwindow as xproto::Window) + .expect_then_ignore_error("Failed to set WM hints"); + } + + #[inline] + pub(crate) fn generate_activation_token(&self) -> Result { + // Get the title from the WM_NAME property. + let atoms = self.xconn.atoms(); + let title = { + let title_bytes = self + .xconn + .get_property(self.xwindow, atoms[_NET_WM_NAME], atoms[UTF8_STRING]) + .expect("Failed to get title"); + + String::from_utf8(title_bytes).expect("Bad title") + }; + + // Get the activation token and then put it in the event queue. + let token = self.xconn.request_activation_token(&title)?; + + Ok(token) + } + + #[inline] + pub fn request_activation_token(&self) -> Result { + let serial = AsyncRequestSerial::get(); + self.activation_sender + .send((self.id(), serial)) + .expect("activation token channel should never be closed"); + Ok(serial) + } + + #[inline] + pub fn id(&self) -> WindowId { + WindowId(self.xwindow as _) + } + + #[inline] + pub fn request_redraw(&self) { + self.redraw_sender.send(WindowId(self.xwindow as _)).unwrap(); + } + + #[inline] + pub fn pre_present_notify(&self) { + // TODO timer + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::XlibHandle::empty(); + window_handle.display = self.xlib_display(); + window_handle.window = self.xlib_window(); + window_handle.visual_id = self.visual as c_ulong; + rwh_04::RawWindowHandle::Xlib(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::XlibWindowHandle::empty(); + window_handle.window = self.xlib_window(); + window_handle.visual_id = self.visual as c_ulong; + window_handle.into() + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + let mut display_handle = rwh_05::XlibDisplayHandle::empty(); + display_handle.display = self.xlib_display(); + display_handle.screen = self.screen_id; + display_handle.into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let mut window_handle = rwh_06::XlibWindowHandle::new(self.xlib_window()); + window_handle.visual_id = self.visual as c_ulong; + Ok(window_handle.into()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::XlibDisplayHandle::new( + Some( + std::ptr::NonNull::new(self.xlib_display()) + .expect("display pointer should never be null"), + ), + self.screen_id, + ) + .into()) + } + + #[inline] + pub fn theme(&self) -> Option { + None + } + + pub fn set_content_protected(&self, _protected: bool) {} + + #[inline] + pub fn has_focus(&self) -> bool { + self.shared_state_lock().has_focus + } + + pub fn title(&self) -> String { + String::new() + } +} + +/// Cast a dimension value into a hinted dimension for `WmSizeHints`, clamping if too large. +fn cast_dimension_to_hint(val: u32) -> i32 { + val.try_into().unwrap_or(i32::MAX) +} + +/// Use the above strategy to cast a physical size into a hinted size. +fn cast_physical_size_to_hint(size: PhysicalSize) -> (i32, i32) { + let PhysicalSize { width, height } = size; + (cast_dimension_to_hint(width), cast_dimension_to_hint(height)) +} + +/// Use the above strategy to cast a size into a hinted size. +fn cast_size_to_hint(size: Size, scale_factor: f64) -> (i32, i32) { + match size { + Size::Physical(size) => cast_physical_size_to_hint(size), + Size::Logical(size) => size.to_physical::(scale_factor).into(), + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/xdisplay.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/xdisplay.rs new file mode 100644 index 0000000..79da0d1 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/xdisplay.rs @@ -0,0 +1,356 @@ +use std::collections::HashMap; +use std::error::Error; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard}; +use std::{fmt, ptr}; + +use crate::window::CursorIcon; + +use super::atoms::Atoms; +use super::ffi; +use super::monitor::MonitorHandle; +use x11rb::connection::Connection; +use x11rb::protocol::randr::ConnectionExt as _; +use x11rb::protocol::xproto::{self, ConnectionExt}; +use x11rb::resource_manager; +use x11rb::xcb_ffi::XCBConnection; + +/// A connection to an X server. +pub struct XConnection { + pub xlib: ffi::Xlib, + pub xcursor: ffi::Xcursor, + + // TODO(notgull): I'd like to remove this, but apparently Xlib and Xinput2 are tied together + // for some reason. + pub xinput2: ffi::XInput2, + + pub display: *mut ffi::Display, + + /// The manager for the XCB connection. + /// + /// The `Option` ensures that we can drop it before we close the `Display`. + xcb: Option, + + /// The atoms used by `winit`. + /// + /// This is a large structure, so I've elected to Box it to make accessing the fields of + /// this struct easier. Feel free to unbox it if you like kicking puppies. + atoms: Box, + + /// The index of the default screen. + default_screen: usize, + + /// The last timestamp received by this connection. + timestamp: AtomicU32, + + /// List of monitor handles. + pub monitor_handles: Mutex>>, + + /// The resource database. + database: RwLock, + + /// RandR version. + randr_version: (u32, u32), + + /// Atom for the XSettings screen. + xsettings_screen: Option, + + pub latest_error: Mutex>, + pub cursor_cache: Mutex, ffi::Cursor>>, +} + +unsafe impl Send for XConnection {} +unsafe impl Sync for XConnection {} + +pub type XErrorHandler = + Option std::os::raw::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 xlib_xcb = ffi::Xlib_xcb::open()?; + let xinput2 = ffi::XInput2::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 + }; + + // Open the x11rb XCB connection. + let xcb = { + // Get a pointer to the underlying XCB connection + let xcb_connection = + unsafe { (xlib_xcb.XGetXCBConnection)(display as *mut ffi::Display) }; + assert!(!xcb_connection.is_null()); + + // Wrap the XCB connection in an x11rb XCB connection + let conn = + unsafe { XCBConnection::from_raw_xcb_connection(xcb_connection.cast(), false) }; + + conn.map_err(|e| XNotSupported::XcbConversionError(Arc::new(WrapConnectError(e))))? + }; + + // Get the default screen. + let default_screen = unsafe { (xlib.XDefaultScreen)(display) } as usize; + + // Load the database. + let database = resource_manager::new_from_default(&xcb) + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?; + + // Load the RandR version. + let randr_version = xcb + .randr_query_version(1, 3) + .expect("failed to request XRandR version") + .reply() + .expect("failed to query XRandR version"); + + let xsettings_screen = Self::new_xsettings_screen(&xcb, default_screen); + if xsettings_screen.is_none() { + tracing::warn!("error setting XSETTINGS; Xft options won't reload automatically") + } + + // Fetch atoms. + let atoms = Atoms::new(&xcb) + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))? + .reply() + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?; + + Ok(XConnection { + xlib, + xcursor, + xinput2, + display, + xcb: Some(xcb), + atoms: Box::new(atoms), + default_screen, + timestamp: AtomicU32::new(0), + latest_error: Mutex::new(None), + monitor_handles: Mutex::new(None), + database: RwLock::new(database), + cursor_cache: Default::default(), + randr_version: (randr_version.major_version, randr_version.minor_version), + xsettings_screen, + }) + } + + fn new_xsettings_screen(xcb: &XCBConnection, default_screen: usize) -> Option { + // Fetch the _XSETTINGS_S[screen number] atom. + let xsettings_screen = xcb + .intern_atom(false, format!("_XSETTINGS_S{default_screen}").as_bytes()) + .ok()? + .reply() + .ok()? + .atom; + + // Get PropertyNotify events from the XSETTINGS window. + // TODO: The XSETTINGS window here can change. In the future, listen for DestroyNotify on + // this window in order to accommodate for a changed window here. + let selector_window = xcb.get_selection_owner(xsettings_screen).ok()?.reply().ok()?.owner; + + xcb.change_window_attributes( + selector_window, + &xproto::ChangeWindowAttributesAux::new() + .event_mask(xproto::EventMask::PROPERTY_CHANGE), + ) + .ok()? + .check() + .ok()?; + + Some(xsettings_screen) + } + + /// 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().unwrap().take(); + if let Some(error) = error { + Err(error) + } else { + Ok(()) + } + } + + #[inline] + pub fn randr_version(&self) -> (u32, u32) { + self.randr_version + } + + /// Get the underlying XCB connection. + #[inline] + pub fn xcb_connection(&self) -> &XCBConnection { + self.xcb.as_ref().expect("xcb_connection somehow called after drop?") + } + + /// Get the list of atoms. + #[inline] + pub fn atoms(&self) -> &Atoms { + &self.atoms + } + + /// Get the index of the default screen. + #[inline] + pub fn default_screen_index(&self) -> usize { + self.default_screen + } + + /// Get the default screen. + #[inline] + pub fn default_root(&self) -> &xproto::Screen { + &self.xcb_connection().setup().roots[self.default_screen] + } + + /// Get the resource database. + #[inline] + pub fn database(&self) -> RwLockReadGuard<'_, resource_manager::Database> { + self.database.read().unwrap_or_else(|e| e.into_inner()) + } + + /// Reload the resource database. + #[inline] + pub fn reload_database(&self) -> Result<(), super::X11Error> { + let database = resource_manager::new_from_default(self.xcb_connection())?; + *self.database.write().unwrap_or_else(|e| e.into_inner()) = database; + Ok(()) + } + + /// Get the latest timestamp. + #[inline] + pub fn timestamp(&self) -> u32 { + self.timestamp.load(Ordering::Relaxed) + } + + /// Set the last witnessed timestamp. + #[inline] + pub fn set_timestamp(&self, timestamp: u32) { + // Store the timestamp in the slot if it's greater than the last one. + let mut last_timestamp = self.timestamp.load(Ordering::Relaxed); + loop { + if (timestamp as i32).wrapping_sub(last_timestamp as i32) <= 0 { + break; + } + + match self.timestamp.compare_exchange( + last_timestamp, + timestamp, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => break, + Err(x) => last_timestamp = x, + } + } + } + + /// Get the atom for Xsettings. + #[inline] + pub fn xsettings_screen(&self) -> Option { + self.xsettings_screen + } +} + +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) { + self.xcb = None; + 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. + + /// We encountered an error while converting the connection to XCB. + XcbConversionError(Arc), +} + +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", + XNotSupported::XcbConversionError(_) => "Failed to convert Xlib connection to XCB", + } + } +} + +impl Error for XNotSupported { + #[inline] + fn source(&self) -> Option<&(dyn Error + 'static)> { + match *self { + XNotSupported::LibraryOpenError(ref err) => Some(err), + XNotSupported::XcbConversionError(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()) + } +} + +/// A newtype wrapper around a `ConnectError` that can't be accessed by downstream libraries. +/// +/// Without this, `x11rb` would become a public dependency. +#[derive(Debug)] +struct WrapConnectError(x11rb::rust_connection::ConnectError); + +impl fmt::Display for WrapConnectError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Error for WrapConnectError { + // We can't implement `source()` here or otherwise risk exposing `x11rb`. +} diff --git a/third_party/winit-0.30.13/src/platform_impl/linux/x11/xsettings.rs b/third_party/winit-0.30.13/src/platform_impl/linux/x11/xsettings.rs new file mode 100644 index 0000000..dd5b074 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/linux/x11/xsettings.rs @@ -0,0 +1,325 @@ +//! Parser for the xsettings data format. +//! +//! Some of this code is referenced from [here]. +//! +//! [here]: https://github.com/derat/xsettingsd + +use std::iter; +use std::num::NonZeroUsize; + +use x11rb::protocol::xproto::{self, ConnectionExt}; + +use super::atoms::*; +use super::XConnection; + +type Result = core::result::Result; + +const DPI_NAME: &[u8] = b"Xft/DPI"; +const DPI_MULTIPLIER: f64 = 1024.0; +const LITTLE_ENDIAN: u8 = b'l'; +const BIG_ENDIAN: u8 = b'B'; + +impl XConnection { + /// Get the DPI from XSettings. + pub(crate) fn xsettings_dpi( + &self, + xsettings_screen: xproto::Atom, + ) -> core::result::Result, super::X11Error> { + let atoms = self.atoms(); + + // Get the current owner of the screen's settings. + let owner = self.xcb_connection().get_selection_owner(xsettings_screen)?.reply()?; + + // Read the _XSETTINGS_SETTINGS property. + let data: Vec = + self.get_property(owner.owner, atoms[_XSETTINGS_SETTINGS], atoms[_XSETTINGS_SETTINGS])?; + + // Parse the property. + let dpi_setting = read_settings(&data)? + .find(|res| res.as_ref().map_or(true, |s| s.name == DPI_NAME)) + .transpose()?; + if let Some(dpi_setting) = dpi_setting { + let base_dpi = match dpi_setting.data { + SettingData::Integer(dpi) => dpi as f64, + SettingData::String(_) => { + return Err(ParserError::BadType(SettingType::String).into()) + }, + SettingData::Color(_) => { + return Err(ParserError::BadType(SettingType::Color).into()) + }, + }; + + Ok(Some(base_dpi / DPI_MULTIPLIER)) + } else { + Ok(None) + } + } +} + +/// Read over the settings in the block of data. +fn read_settings(data: &[u8]) -> Result>> + '_> { + // Create a parser. This automatically parses the first 8 bytes for metadata. + let mut parser = Parser::new(data)?; + + // Read the total number of settings. + let total_settings = parser.i32()?; + + // Iterate over the settings. + let iter = iter::repeat_with(move || Setting::parse(&mut parser)).take(total_settings as usize); + Ok(iter) +} + +/// A setting in the settings list. +struct Setting<'a> { + /// The name of the setting. + name: &'a [u8], + + /// The data contained in the setting. + data: SettingData<'a>, +} + +/// The data contained in a setting. +enum SettingData<'a> { + Integer(i32), + String(#[allow(dead_code)] &'a [u8]), + Color(#[allow(dead_code)] [i16; 4]), +} + +impl<'a> Setting<'a> { + /// Parse a new `SettingData`. + fn parse(parser: &mut Parser<'a>) -> Result { + // Read the type. + let ty: SettingType = parser.i8()?.try_into()?; + + // Read another byte of padding. + parser.advance(1)?; + + // Read the name of the setting. + let name_len = parser.i16()?; + let name = parser.advance(name_len as usize)?; + parser.pad(name.len(), 4)?; + + // Ignore the serial number. + parser.advance(4)?; + + let data = match ty { + SettingType::Integer => { + // Read a 32-bit integer. + SettingData::Integer(parser.i32()?) + }, + + SettingType::String => { + // Read the data. + let data_len = parser.i32()?; + let data = parser.advance(data_len as usize)?; + parser.pad(data.len(), 4)?; + + SettingData::String(data) + }, + + SettingType::Color => { + // Read i16's of color. + let (red, blue, green, alpha) = + (parser.i16()?, parser.i16()?, parser.i16()?, parser.i16()?); + + SettingData::Color([red, blue, green, alpha]) + }, + }; + + Ok(Setting { name, data }) + } +} + +#[derive(Debug)] +pub enum SettingType { + Integer = 0, + String = 1, + Color = 2, +} + +impl TryFrom for SettingType { + type Error = ParserError; + + fn try_from(value: i8) -> Result { + Ok(match value { + 0 => Self::Integer, + 1 => Self::String, + 2 => Self::Color, + x => return Err(ParserError::InvalidType(x)), + }) + } +} + +/// Parser for the incoming byte stream. +struct Parser<'a> { + bytes: &'a [u8], + endianness: Endianness, +} + +impl<'a> Parser<'a> { + /// Create a new parser. + fn new(bytes: &'a [u8]) -> Result { + let (endianness, bytes) = bytes.split_first().ok_or_else(|| ParserError::ran_out(1, 0))?; + let endianness = match *endianness { + BIG_ENDIAN => Endianness::Big, + LITTLE_ENDIAN => Endianness::Little, + _ => Endianness::native(), + }; + + Ok(Self { + // Ignore three bytes of padding and the four-byte serial. + bytes: bytes.get(7..).ok_or_else(|| ParserError::ran_out(7, bytes.len()))?, + endianness, + }) + } + + /// Get a slice of bytes. + fn advance(&mut self, n: usize) -> Result<&'a [u8]> { + if n == 0 { + return Ok(&[]); + } + + if n > self.bytes.len() { + Err(ParserError::ran_out(n, self.bytes.len())) + } else { + let (part, rem) = self.bytes.split_at(n); + self.bytes = rem; + Ok(part) + } + } + + /// Skip some padding. + fn pad(&mut self, size: usize, pad: usize) -> Result<()> { + let advance = (pad - (size % pad)) % pad; + self.advance(advance)?; + Ok(()) + } + + /// Get a single byte. + fn i8(&mut self) -> Result { + self.advance(1).map(|s| s[0] as i8) + } + + /// Get two bytes. + fn i16(&mut self) -> Result { + self.advance(2).map(|s| { + let bytes: &[u8; 2] = s.try_into().unwrap(); + match self.endianness { + Endianness::Big => i16::from_be_bytes(*bytes), + Endianness::Little => i16::from_le_bytes(*bytes), + } + }) + } + + /// Get four bytes. + fn i32(&mut self) -> Result { + self.advance(4).map(|s| { + let bytes: &[u8; 4] = s.try_into().unwrap(); + match self.endianness { + Endianness::Big => i32::from_be_bytes(*bytes), + Endianness::Little => i32::from_le_bytes(*bytes), + } + }) + } +} + +/// Endianness of the incoming data. +enum Endianness { + Little, + Big, +} + +impl Endianness { + #[cfg(target_endian = "little")] + fn native() -> Self { + Endianness::Little + } + + #[cfg(target_endian = "big")] + fn native() -> Self { + Endianness::Big + } +} + +/// Parser errors. +#[allow(dead_code)] +#[derive(Debug)] +pub enum ParserError { + /// Ran out of bytes. + NoMoreBytes { expected: NonZeroUsize, found: usize }, + + /// Invalid type. + InvalidType(i8), + + /// Bad setting type. + BadType(SettingType), +} + +impl ParserError { + fn ran_out(expected: usize, found: usize) -> ParserError { + let expected = NonZeroUsize::new(expected).unwrap(); + Self::NoMoreBytes { expected, found } + } +} + +#[cfg(test)] +/// Tests for the XSETTINGS parser. +mod tests { + use super::*; + + const XSETTINGS: &str = include_str!("tests/xsettings.dat"); + + #[test] + fn empty() { + let err = match read_settings(&[]) { + Ok(_) => panic!(), + Err(err) => err, + }; + match err { + ParserError::NoMoreBytes { expected, found } => { + assert_eq!(expected.get(), 1); + assert_eq!(found, 0); + }, + + _ => panic!(), + } + } + + #[test] + fn parse_xsettings() { + let data = XSETTINGS + .trim() + .split(',') + .map(|tok| { + let val = tok.strip_prefix("0x").unwrap(); + u8::from_str_radix(val, 16).unwrap() + }) + .collect::>(); + + let settings = read_settings(&data).unwrap().collect::>>().unwrap(); + + let dpi = settings.iter().find(|s| s.name == b"Xft/DPI").unwrap(); + assert_int(&dpi.data, 96 * 1024); + let hinting = settings.iter().find(|s| s.name == b"Xft/Hinting").unwrap(); + assert_int(&hinting.data, 1); + + let rgba = settings.iter().find(|s| s.name == b"Xft/RGBA").unwrap(); + assert_string(&rgba.data, "rgb"); + let lcd = settings.iter().find(|s| s.name == b"Xft/Lcdfilter").unwrap(); + assert_string(&lcd.data, "lcddefault"); + } + + fn assert_string(dat: &SettingData<'_>, s: &str) { + match dat { + SettingData::String(left) => assert_eq!(*left, s.as_bytes()), + _ => panic!("invalid data type"), + } + } + + fn assert_int(dat: &SettingData<'_>, i: i32) { + match dat { + SettingData::Integer(left) => assert_eq!(*left, i), + _ => panic!("invalid data type"), + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/app.rs b/third_party/winit-0.30.13/src/platform_impl/macos/app.rs new file mode 100644 index 0000000..4fb95db --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/app.rs @@ -0,0 +1,195 @@ +#![allow(clippy::unnecessary_cast)] +#![allow(unknown_lints)] // New lint below +#![allow(static_mut_refs)] // Uses `MainThreadBound` in new version. + +use std::cell::Cell; +use std::mem; + +use objc2::runtime::{Imp, Sel}; +use objc2::sel; +use objc2_app_kit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType}; +use objc2_foundation::MainThreadMarker; + +use super::app_state::ApplicationDelegate; +use crate::event::{DeviceEvent, ElementState}; + +type SendEvent = extern "C" fn(&NSApplication, Sel, &NSEvent); + +// NOTE: Only used on the main thread. Ideally, we'd use `MainThreadBound`, but that isn't +// constructible from `const` with this `objc2` version. +static mut ORIGINAL: Cell> = Cell::new(None); + +extern "C" fn send_event(app: &NSApplication, sel: Sel, event: &NSEvent) { + let mtm = MainThreadMarker::from(app); + + // Normally, holding Cmd + any key never sends us a `keyUp` event for that key. + // Overriding `sendEvent:` fixes that. (https://stackoverflow.com/a/15294196) + // Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553) + // + // 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 = unsafe { event.r#type() }; + let modifier_flags = unsafe { event.modifierFlags() }; + if event_type == NSEventType::KeyUp + && modifier_flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand) + { + if let Some(key_window) = app.keyWindow() { + key_window.sendEvent(event); + } + return; + } + + // Events are generally scoped to the window level, so the best way + // to get device events is to listen for them on NSApplication. + let delegate = ApplicationDelegate::get(mtm); + maybe_dispatch_device_event(&delegate, event); + + let _ = mtm; + let original = unsafe { ORIGINAL.get().expect("no existing sendEvent: handler set") }; + original(app, sel, event) +} + +/// Override the [`sendEvent:`][NSApplication::sendEvent] method on the given application class. +/// +/// The previous implementation created a subclass of [`NSApplication`], however we would like to +/// give the user full control over their `NSApplication`, so we override the method here using +/// method swizzling instead. +/// +/// This _should_ also allow two versions of Winit to exist in the same application. +/// +/// See the following links for more info on method swizzling: +/// - +/// - +/// - +/// +/// NOTE: This function assumes that the passed in application object is the one returned from +/// [`NSApplication::sharedApplication`], i.e. the one and only global shared application object. +/// For testing though, we allow it to be a different object. +pub(crate) fn override_send_event(global_app: &NSApplication) { + let mtm = MainThreadMarker::from(global_app); + let class = global_app.class(); + + let method = + class.instance_method(sel!(sendEvent:)).expect("NSApplication must have sendEvent: method"); + + // SAFETY: Converting our `sendEvent:` implementation to an IMP. + let overridden = unsafe { mem::transmute::(send_event) }; + + // If we've already overridden the method, don't do anything. + // FIXME(madsmtm): Use `std::ptr::fn_addr_eq` (Rust 1.85) once available in MSRV. + #[allow(unknown_lints, unpredictable_function_pointer_comparisons)] + if overridden == method.implementation() { + return; + } + + // SAFETY: Our implementation has: + // 1. The same signature as `sendEvent:`. + // 2. Does not impose extra safety requirements on callers. + let original = unsafe { method.set_implementation(overridden) }; + + // SAFETY: This is the actual signature of `sendEvent:`. + let original = unsafe { mem::transmute::(original) }; + + // NOTE: If NSApplication was safe to use from multiple threads, then this would potentially be + // a (checked) race-condition, since one could call `sendEvent:` before the original had been + // stored here. + // + // It is only usable from the main thread, however, so we're good! + let _ = mtm; + unsafe { ORIGINAL.set(Some(original)) }; +} + +fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent) { + let event_type = unsafe { event.r#type() }; + #[allow(non_upper_case_globals)] + match event_type { + NSEventType::MouseMoved + | NSEventType::LeftMouseDragged + | NSEventType::OtherMouseDragged + | NSEventType::RightMouseDragged => { + let delta_x = unsafe { event.deltaX() } as f64; + let delta_y = unsafe { event.deltaY() } as f64; + + if delta_x != 0.0 { + delegate.maybe_queue_device_event(DeviceEvent::Motion { axis: 0, value: delta_x }); + } + + if delta_y != 0.0 { + delegate.maybe_queue_device_event(DeviceEvent::Motion { axis: 1, value: delta_y }) + } + + if delta_x != 0.0 || delta_y != 0.0 { + delegate.maybe_queue_device_event(DeviceEvent::MouseMotion { + delta: (delta_x, delta_y), + }); + } + }, + NSEventType::LeftMouseDown | NSEventType::RightMouseDown | NSEventType::OtherMouseDown => { + delegate.maybe_queue_device_event(DeviceEvent::Button { + button: unsafe { event.buttonNumber() } as u32, + state: ElementState::Pressed, + }); + }, + NSEventType::LeftMouseUp | NSEventType::RightMouseUp | NSEventType::OtherMouseUp => { + delegate.maybe_queue_device_event(DeviceEvent::Button { + button: unsafe { event.buttonNumber() } as u32, + state: ElementState::Released, + }); + }, + _ => (), + } +} + +#[cfg(test)] +mod tests { + use objc2::rc::Retained; + use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; + + use super::*; + + #[test] + fn test_override() { + // FIXME(madsmtm): Ensure this always runs (maybe use cargo-nextest or `--test-threads=1`?) + let Some(mtm) = MainThreadMarker::new() else { return }; + + // Create a new application, without making it the shared application. + let app = unsafe { NSApplication::new(mtm) }; + override_send_event(&app); + // Test calling twice works. + override_send_event(&app); + + // FIXME(madsmtm): Can't test this yet, need some way to mock AppState. + // unsafe { + // let event = super::super::event::dummy_event().unwrap(); + // app.sendEvent(&event) + // } + } + + #[test] + fn test_custom_class() { + let Some(_mtm) = MainThreadMarker::new() else { return }; + + declare_class!( + struct TestApplication; + + unsafe impl ClassType for TestApplication { + type Super = NSApplication; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "TestApplication"; + } + + impl DeclaredClass for TestApplication {} + + unsafe impl TestApplication { + #[method(sendEvent:)] + fn send_event(&self, _event: &NSEvent) { + todo!() + } + } + ); + + let app: Retained = unsafe { msg_send_id![TestApplication::class(), new] }; + override_send_event(&app); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/app_state.rs b/third_party/winit-0.30.13/src/platform_impl/macos/app_state.rs new file mode 100644 index 0000000..dd2ccd1 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/app_state.rs @@ -0,0 +1,446 @@ +use std::cell::{Cell, RefCell}; +use std::mem; +use std::rc::Weak; +use std::time::Instant; + +use objc2::rc::Retained; +use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; +use objc2_app_kit::{ + NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, NSRunningApplication, +}; +use objc2_foundation::{MainThreadMarker, NSNotification, NSObject, NSObjectProtocol}; + +use super::event_handler::EventHandler; +use super::event_loop::{notify_windows_of_exit, stop_app_immediately, ActiveEventLoop, PanicInfo}; +use super::observer::{EventLoopWaker, RunLoop}; +use super::{menu, WindowId, DEVICE_ID}; +use crate::event::{DeviceEvent, Event, StartCause, WindowEvent}; +use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow}; +use crate::window::WindowId as RootWindowId; + +#[derive(Debug)] +pub(super) struct AppState { + activation_policy: Option, + default_menu: bool, + activate_ignoring_other_apps: bool, + run_loop: RunLoop, + event_handler: EventHandler, + stop_on_launch: Cell, + stop_before_wait: Cell, + stop_after_wait: Cell, + stop_on_redraw: Cell, + /// Whether `applicationDidFinishLaunching:` has been run or not. + is_launched: Cell, + /// Whether an `EventLoop` is currently running. + is_running: Cell, + /// Whether the user has requested the event loop to exit. + exit: Cell, + control_flow: Cell, + waker: RefCell, + start_time: Cell>, + wait_timeout: Cell>, + pending_redraw: RefCell>, + // NOTE: This is strongly referenced by our `NSWindowDelegate` and our `NSView` subclass, and + // as such should be careful to not add fields that, in turn, strongly reference those. +} + +declare_class!( + #[derive(Debug)] + pub(super) struct ApplicationDelegate; + + unsafe impl ClassType for ApplicationDelegate { + type Super = NSObject; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitApplicationDelegate"; + } + + impl DeclaredClass for ApplicationDelegate { + type Ivars = AppState; + } + + unsafe impl NSObjectProtocol for ApplicationDelegate {} + + unsafe impl NSApplicationDelegate for ApplicationDelegate { + #[method(applicationDidFinishLaunching:)] + fn app_did_finish_launching(&self, notification: &NSNotification) { + self.did_finish_launching(notification) + } + + #[method(applicationWillTerminate:)] + fn app_will_terminate(&self, notification: &NSNotification) { + self.will_terminate(notification) + } + } +); + +impl ApplicationDelegate { + pub(super) fn new( + mtm: MainThreadMarker, + activation_policy: Option, + default_menu: bool, + activate_ignoring_other_apps: bool, + ) -> Retained { + let this = mtm.alloc().set_ivars(AppState { + activation_policy, + default_menu, + activate_ignoring_other_apps, + run_loop: RunLoop::main(mtm), + event_handler: EventHandler::new(), + stop_on_launch: Cell::new(false), + stop_before_wait: Cell::new(false), + stop_after_wait: Cell::new(false), + stop_on_redraw: Cell::new(false), + is_launched: Cell::new(false), + is_running: Cell::new(false), + exit: Cell::new(false), + control_flow: Cell::new(ControlFlow::default()), + waker: RefCell::new(EventLoopWaker::new()), + start_time: Cell::new(None), + wait_timeout: Cell::new(None), + pending_redraw: RefCell::new(vec![]), + }); + unsafe { msg_send_id![super(this), init] } + } + + // NOTE: This will, globally, only be run once, no matter how many + // `EventLoop`s the user creates. + fn did_finish_launching(&self, _notification: &NSNotification) { + trace_scope!("applicationDidFinishLaunching:"); + self.ivars().is_launched.set(true); + + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + // We need to delay setting the activation policy and activating the app + // until `applicationDidFinishLaunching` has been called. Otherwise the + // menu bar is initially unresponsive on macOS 10.15. + // If no activation policy is explicitly provided, do not set it at all + // to allow the package manifest to define behavior via LSUIElement. + if let Some(activation_policy) = self.ivars().activation_policy { + app.setActivationPolicy(activation_policy); + } else { + // If no activation policy is explicitly provided, and the application + // is bundled, do not set the activation policy at all, to allow the + // package manifest to define the behavior via LSUIElement. + // + // See: + // - https://github.com/rust-windowing/winit/issues/261 + // - https://github.com/rust-windowing/winit/issues/3958 + let is_bundled = + unsafe { NSRunningApplication::currentApplication().bundleIdentifier().is_some() }; + if !is_bundled { + app.setActivationPolicy(NSApplicationActivationPolicy::Regular); + } + } + + window_activation_hack(&app); + #[allow(deprecated)] + app.activateIgnoringOtherApps(self.ivars().activate_ignoring_other_apps); + + if self.ivars().default_menu { + // The menubar initialization should be before the `NewEvents` event, to allow + // overriding of the default menu even if it's created + menu::initialize(&app); + } + + self.ivars().waker.borrow_mut().start(); + + self.set_is_running(true); + self.dispatch_init_events(); + + // If the application is being launched via `EventLoop::pump_app_events()` then we'll + // want to stop the app once it is launched (and return to the external loop) + // + // In this case we still want to consider Winit's `EventLoop` to be "running", + // so we call `start_running()` above. + if self.ivars().stop_on_launch.get() { + // NOTE: the original idea had been to only stop the underlying `RunLoop` + // for the app but that didn't work as expected (`-[NSApplication run]` + // effectively ignored the attempt to stop the RunLoop and re-started it). + // + // So we return from `pump_events` by stopping the application. + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + } + + fn will_terminate(&self, _notification: &NSNotification) { + trace_scope!("applicationWillTerminate:"); + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + notify_windows_of_exit(&app); + self.internal_exit(); + } + + pub fn get(mtm: MainThreadMarker) -> Retained { + let app = NSApplication::sharedApplication(mtm); + let delegate = + unsafe { app.delegate() }.expect("a delegate was not configured on the application"); + if delegate.is_kind_of::() { + // SAFETY: Just checked that the delegate is an instance of `ApplicationDelegate` + unsafe { Retained::cast(delegate) } + } else { + panic!("tried to get a delegate that was not the one Winit has registered") + } + } + + /// Place the event handler in the application delegate for the duration + /// of the given closure. + pub fn set_event_handler( + &self, + handler: impl FnMut(Event, &RootActiveEventLoop), + closure: impl FnOnce() -> R, + ) -> R { + self.ivars().event_handler.set(handler, closure) + } + + /// If `pump_events` is called to progress the event loop then we + /// bootstrap the event loop via `-[NSApplication run]` but will use + /// `CFRunLoopRunInMode` for subsequent calls to `pump_events`. + pub fn set_stop_on_launch(&self) { + self.ivars().stop_on_launch.set(true); + } + + pub fn set_stop_before_wait(&self, value: bool) { + self.ivars().stop_before_wait.set(value) + } + + pub fn set_stop_after_wait(&self, value: bool) { + self.ivars().stop_after_wait.set(value) + } + + pub fn set_stop_on_redraw(&self, value: bool) { + self.ivars().stop_on_redraw.set(value) + } + + pub fn set_wait_timeout(&self, value: Option) { + self.ivars().wait_timeout.set(value) + } + + /// Clears the `running` state and resets the `control_flow` state when an `EventLoop` exits. + /// + /// NOTE: that if the `NSApplication` has been launched then that state is preserved, + /// and we won't need to re-launch the app if subsequent EventLoops are run. + pub fn internal_exit(&self) { + self.handle_event(Event::LoopExiting); + + self.set_is_running(false); + self.set_stop_on_redraw(false); + self.set_stop_before_wait(false); + self.set_stop_after_wait(false); + self.set_wait_timeout(None); + } + + pub fn is_launched(&self) -> bool { + self.ivars().is_launched.get() + } + + pub fn set_is_running(&self, value: bool) { + self.ivars().is_running.set(value) + } + + pub fn is_running(&self) -> bool { + self.ivars().is_running.get() + } + + pub fn exit(&self) { + self.ivars().exit.set(true) + } + + pub fn clear_exit(&self) { + self.ivars().exit.set(false) + } + + pub fn exiting(&self) -> bool { + self.ivars().exit.get() + } + + pub fn set_control_flow(&self, value: ControlFlow) { + self.ivars().control_flow.set(value) + } + + pub fn control_flow(&self) -> ControlFlow { + self.ivars().control_flow.get() + } + + pub fn maybe_queue_window_event(&self, window_id: WindowId, event: WindowEvent) { + self.maybe_queue_event(Event::WindowEvent { window_id: RootWindowId(window_id), event }); + } + + pub fn handle_window_event(&self, window_id: WindowId, event: WindowEvent) { + self.handle_event(Event::WindowEvent { window_id: RootWindowId(window_id), event }); + } + + pub fn maybe_queue_device_event(&self, event: DeviceEvent) { + self.maybe_queue_event(Event::DeviceEvent { device_id: DEVICE_ID, event }); + } + + pub fn handle_redraw(&self, window_id: WindowId) { + let mtm = MainThreadMarker::from(self); + // Redraw request might come out of order from the OS. + // -> Don't go back into the event handler when our callstack originates from there + if !self.ivars().event_handler.in_use() { + self.handle_event(Event::WindowEvent { + window_id: RootWindowId(window_id), + event: WindowEvent::RedrawRequested, + }); + + // `pump_events` will request to stop immediately _after_ dispatching RedrawRequested + // events as a way to ensure that `pump_events` can't block an external loop + // indefinitely + if self.ivars().stop_on_redraw.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + } + } + + pub fn queue_redraw(&self, window_id: WindowId) { + let mut pending_redraw = self.ivars().pending_redraw.borrow_mut(); + if !pending_redraw.contains(&window_id) { + pending_redraw.push(window_id); + } + self.ivars().run_loop.wakeup(); + } + + #[track_caller] + fn maybe_queue_event(&self, event: Event) { + // Most programmer actions in AppKit (e.g. change window fullscreen, set focused, etc.) + // result in an event being queued, and applied at a later point. + // + // However, it is not documented which actions do this, and which ones are done immediately, + // so to make sure that we don't encounter re-entrancy issues, we first check if we're + // currently handling another event, and if we are, we queue the event instead. + if !self.ivars().event_handler.in_use() { + self.handle_event(event); + } else { + tracing::debug!(?event, "had to queue event since another is currently being handled"); + let this = self.retain(); + self.ivars().run_loop.queue_closure(move || this.handle_event(event)); + } + } + + #[track_caller] + fn handle_event(&self, event: Event) { + self.ivars().event_handler.handle_event(event, &ActiveEventLoop::new_root(self.retain())) + } + + /// dispatch `NewEvents(Init)` + `Resumed` + pub fn dispatch_init_events(&self) { + self.handle_event(Event::NewEvents(StartCause::Init)); + // NB: For consistency all platforms must emit a 'resumed' event even though macOS + // applications don't themselves have a formal suspend/resume lifecycle. + self.handle_event(Event::Resumed); + } + + // Called by RunLoopObserver after finishing waiting for new events + pub fn wakeup(&self, panic_info: Weak) { + let mtm = MainThreadMarker::from(self); + let panic_info = panic_info + .upgrade() + .expect("The panic info must exist here. This failure indicates a developer error."); + + // Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779 + if panic_info.is_panicking() || !self.ivars().event_handler.ready() || !self.is_running() { + return; + } + + if self.ivars().stop_after_wait.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + + let start = self.ivars().start_time.get().unwrap(); + let cause = match self.control_flow() { + 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) } + } + }, + }; + + self.handle_event(Event::NewEvents(cause)); + } + + // Called by RunLoopObserver before waiting for new events + pub fn cleared(&self, panic_info: Weak) { + let mtm = MainThreadMarker::from(self); + let panic_info = panic_info + .upgrade() + .expect("The panic info must exist here. This failure indicates a developer error."); + + // Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779 + // XXX: how does it make sense that `event_handler.ready()` can ever return `false` here if + // we're about to return to the `CFRunLoop` to poll for new events? + if panic_info.is_panicking() || !self.ivars().event_handler.ready() || !self.is_running() { + return; + } + + self.handle_event(Event::UserEvent(HandlePendingUserEvents)); + + let redraw = mem::take(&mut *self.ivars().pending_redraw.borrow_mut()); + for window_id in redraw { + self.handle_event(Event::WindowEvent { + window_id: RootWindowId(window_id), + event: WindowEvent::RedrawRequested, + }); + } + + self.handle_event(Event::AboutToWait); + + if self.exiting() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + notify_windows_of_exit(&app); + } + + if self.ivars().stop_before_wait.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + self.ivars().start_time.set(Some(Instant::now())); + let wait_timeout = self.ivars().wait_timeout.get(); // configured by pump_events + let app_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Instant::now()), + ControlFlow::WaitUntil(instant) => Some(instant), + }; + self.ivars().waker.borrow_mut().start_at(min_timeout(wait_timeout, app_timeout)); + } +} + +#[derive(Debug)] +pub(crate) struct HandlePendingUserEvents; + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout)))) +} + +/// 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. +fn window_activation_hack(app: &NSApplication) { + // TODO: Proper ordering of the windows + app.windows().into_iter().for_each(|window| { + // Call `makeKeyAndOrderFront` if it was called on the window in `WinitWindow::new` + // This way we preserve the user's desired initial visibility status + // TODO: Also filter on the type/"level" of the window, and maybe other things? + if window.isVisible() { + tracing::trace!("Activating visible window"); + window.makeKeyAndOrderFront(None); + } else { + tracing::trace!("Skipping activating invisible window"); + } + }) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/cursor.rs b/third_party/winit-0.30.13/src/platform_impl/macos/cursor.rs new file mode 100644 index 0000000..9e14e8b --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/cursor.rs @@ -0,0 +1,225 @@ +use std::ffi::c_uchar; +use std::slice; +use std::sync::OnceLock; + +use objc2::rc::Retained; +use objc2::runtime::Sel; +use objc2::{msg_send, msg_send_id, sel, ClassType}; +use objc2_app_kit::{NSBitmapImageRep, NSCursor, NSDeviceRGBColorSpace, NSImage}; +use objc2_foundation::{ + ns_string, NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSSize, + NSString, +}; + +use crate::cursor::{CursorImage, OnlyCursorImageSource}; +use crate::window::CursorIcon; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct CustomCursor(pub(crate) Retained); + +// SAFETY: NSCursor is immutable and thread-safe +// TODO(madsmtm): Put this logic in objc2-app-kit itself +unsafe impl Send for CustomCursor {} +unsafe impl Sync for CustomCursor {} + +impl CustomCursor { + pub(crate) fn new(cursor: OnlyCursorImageSource) -> CustomCursor { + Self(cursor_from_image(&cursor.0)) + } +} + +pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Retained { + let width = cursor.width; + let height = cursor.height; + + let bitmap = unsafe { + NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel( + NSBitmapImageRep::alloc(), + std::ptr::null_mut::<*mut c_uchar>(), + width as isize, + height as isize, + 8, + 4, + true, + false, + NSDeviceRGBColorSpace, + width as isize * 4, + 32, + ).unwrap() + }; + let bitmap_data = unsafe { slice::from_raw_parts_mut(bitmap.bitmapData(), cursor.rgba.len()) }; + bitmap_data.copy_from_slice(&cursor.rgba); + + let image = unsafe { + NSImage::initWithSize(NSImage::alloc(), NSSize::new(width.into(), height.into())) + }; + unsafe { image.addRepresentation(&bitmap) }; + + let hotspot = NSPoint::new(cursor.hotspot_x as f64, cursor.hotspot_y as f64); + + NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot) +} + +pub(crate) fn default_cursor() -> Retained { + NSCursor::arrowCursor() +} + +unsafe fn try_cursor_from_selector(sel: Sel) -> Option> { + let cls = NSCursor::class(); + if msg_send![cls, respondsToSelector: sel] { + let cursor: Retained = unsafe { msg_send_id![cls, performSelector: sel] }; + Some(cursor) + } else { + tracing::warn!("cursor `{sel}` appears to be invalid"); + None + } +} + +macro_rules! def_undocumented_cursor { + {$( + $(#[$($m:meta)*])* + fn $name:ident(); + )*} => {$( + $(#[$($m)*])* + #[allow(non_snake_case)] + fn $name() -> Retained { + unsafe { try_cursor_from_selector(sel!($name)).unwrap_or_else(|| default_cursor()) } + } + )*}; +} + +def_undocumented_cursor!( + // Undocumented cursors: https://stackoverflow.com/a/46635398/5435443 + fn _helpCursor(); + fn _zoomInCursor(); + fn _zoomOutCursor(); + fn _windowResizeNorthEastCursor(); + fn _windowResizeNorthWestCursor(); + fn _windowResizeSouthEastCursor(); + fn _windowResizeSouthWestCursor(); + fn _windowResizeNorthEastSouthWestCursor(); + fn _windowResizeNorthWestSouthEastCursor(); + + // While these two are available, the former just loads a white arrow, + // and the latter loads an ugly deflated beachball! + // pub fn _moveCursor(); + // pub fn _waitCursor(); + + // An even more undocumented cursor... + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=522349 + fn busyButClickableCursor(); +); + +// Note that loading `busybutclickable` with this code won't animate +// the frames; instead you'll just get them all in a column. +unsafe fn load_webkit_cursor(name: &NSString) -> Retained { + // Snatch a cursor from WebKit; They fit the style of the native + // cursors, and will seem completely standard to macOS users. + // + // https://stackoverflow.com/a/21786835/5435443 + let root = ns_string!( + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/\ + HIServices.framework/Versions/A/Resources/cursors" + ); + let cursor_path = root.stringByAppendingPathComponent(name); + + let pdf_path = cursor_path.stringByAppendingPathComponent(ns_string!("cursor.pdf")); + let image = NSImage::initByReferencingFile(NSImage::alloc(), &pdf_path).unwrap(); + + // TODO: Handle PLists better + let info_path = cursor_path.stringByAppendingPathComponent(ns_string!("info.plist")); + let info: Retained> = unsafe { + msg_send_id![ + >::class(), + dictionaryWithContentsOfFile: &*info_path, + ] + }; + let mut x = 0.0; + if let Some(n) = info.get(&*ns_string!("hotx")) { + if n.is_kind_of::() { + let ptr: *const NSObject = n; + let ptr: *const NSNumber = ptr.cast(); + x = unsafe { &*ptr }.as_cgfloat() + } + } + let mut y = 0.0; + if let Some(n) = info.get(&*ns_string!("hotx")) { + if n.is_kind_of::() { + let ptr: *const NSObject = n; + let ptr: *const NSNumber = ptr.cast(); + y = unsafe { &*ptr }.as_cgfloat() + } + } + + let hotspot = NSPoint::new(x, y); + NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot) +} + +fn webkit_move() -> Retained { + unsafe { load_webkit_cursor(ns_string!("move")) } +} + +fn webkit_cell() -> Retained { + unsafe { load_webkit_cursor(ns_string!("cell")) } +} + +pub(crate) fn invisible_cursor() -> Retained { + // 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, + ]; + + fn new_invisible() -> Retained { + // TODO: Consider using `dataWithBytesNoCopy:` + let data = NSData::with_bytes(CURSOR_BYTES); + let image = NSImage::initWithData(NSImage::alloc(), &data).unwrap(); + let hotspot = NSPoint::new(0.0, 0.0); + NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot) + } + + // Cache this for efficiency + static CURSOR: OnceLock = OnceLock::new(); + CURSOR.get_or_init(|| CustomCursor(new_invisible())).0.clone() +} + +pub(crate) fn cursor_from_icon(icon: CursorIcon) -> Retained { + match icon { + CursorIcon::Default => default_cursor(), + CursorIcon::Pointer => NSCursor::pointingHandCursor(), + CursorIcon::Grab => NSCursor::openHandCursor(), + CursorIcon::Grabbing => NSCursor::closedHandCursor(), + CursorIcon::Text => NSCursor::IBeamCursor(), + CursorIcon::VerticalText => NSCursor::IBeamCursorForVerticalLayout(), + CursorIcon::Copy => NSCursor::dragCopyCursor(), + CursorIcon::Alias => NSCursor::dragLinkCursor(), + CursorIcon::NotAllowed | CursorIcon::NoDrop => NSCursor::operationNotAllowedCursor(), + CursorIcon::ContextMenu => NSCursor::contextualMenuCursor(), + CursorIcon::Crosshair => NSCursor::crosshairCursor(), + CursorIcon::EResize => NSCursor::resizeRightCursor(), + CursorIcon::NResize => NSCursor::resizeUpCursor(), + CursorIcon::WResize => NSCursor::resizeLeftCursor(), + CursorIcon::SResize => NSCursor::resizeDownCursor(), + CursorIcon::EwResize | CursorIcon::ColResize => NSCursor::resizeLeftRightCursor(), + CursorIcon::NsResize | CursorIcon::RowResize => NSCursor::resizeUpDownCursor(), + CursorIcon::Help => _helpCursor(), + CursorIcon::ZoomIn => _zoomInCursor(), + CursorIcon::ZoomOut => _zoomOutCursor(), + CursorIcon::NeResize => _windowResizeNorthEastCursor(), + CursorIcon::NwResize => _windowResizeNorthWestCursor(), + CursorIcon::SeResize => _windowResizeSouthEastCursor(), + CursorIcon::SwResize => _windowResizeSouthWestCursor(), + CursorIcon::NeswResize => _windowResizeNorthEastSouthWestCursor(), + CursorIcon::NwseResize => _windowResizeNorthWestSouthEastCursor(), + // This is the wrong semantics for `Wait`, but it's the same as + // what's used in Safari and Chrome. + CursorIcon::Wait | CursorIcon::Progress => busyButClickableCursor(), + CursorIcon::Move | CursorIcon::AllScroll => webkit_move(), + CursorIcon::Cell => webkit_cell(), + _ => default_cursor(), + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/event.rs b/third_party/winit-0.30.13/src/platform_impl/macos/event.rs new file mode 100644 index 0000000..9b7f35b --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/event.rs @@ -0,0 +1,616 @@ +use std::ffi::c_void; + +use core_foundation::base::CFRelease; +use core_foundation::data::{CFDataGetBytePtr, CFDataRef}; +use objc2::rc::Retained; +use objc2_app_kit::{NSEvent, NSEventModifierFlags, NSEventSubtype, NSEventType}; +use objc2_foundation::{run_on_main, NSPoint}; +use smol_str::SmolStr; + +use crate::event::{ElementState, KeyEvent, Modifiers}; +use crate::keyboard::{ + Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NamedKey, NativeKey, NativeKeyCode, + PhysicalKey, +}; +use crate::platform_impl::platform::ffi; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option, + pub key_without_modifiers: Key, +} + +/// Ignores ALL modifiers. +pub fn get_modifierless_char(scancode: u16) -> Key { + let mut string = [0; 16]; + let input_source; + let layout; + unsafe { + input_source = ffi::TISCopyCurrentKeyboardLayoutInputSource(); + if input_source.is_null() { + tracing::error!("`TISCopyCurrentKeyboardLayoutInputSource` returned null ptr"); + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + let layout_data = + ffi::TISGetInputSourceProperty(input_source, ffi::kTISPropertyUnicodeKeyLayoutData); + if layout_data.is_null() { + CFRelease(input_source as *mut c_void); + tracing::error!("`TISGetInputSourceProperty` returned null ptr"); + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + layout = CFDataGetBytePtr(layout_data as CFDataRef) as *const ffi::UCKeyboardLayout; + } + let keyboard_type = run_on_main(|_mtm| 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 { + tracing::error!("`UCKeyTranslate` returned with the non-zero value: {}", translate_result); + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + if result_len == 0 { + // This is fine - not all keys have text representation. + // For instance, users that have mapped the `Fn` key to toggle + // keyboard layouts will hit this code path. + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + let chars = String::from_utf16_lossy(&string[0..result_len as usize]); + Key::Character(SmolStr::new(chars)) +} + +// Ignores all modifiers except for SHIFT (yes, even ALT is ignored). +fn get_logical_key_char(ns_event: &NSEvent, modifierless_chars: &str) -> Key { + let string = unsafe { ns_event.charactersIgnoringModifiers() } + .map(|s| s.to_string()) + .unwrap_or_default(); + if string.is_empty() { + // Probably a dead key + let first_char = modifierless_chars.chars().next(); + return Key::Dead(first_char); + } + Key::Character(SmolStr::new(string)) +} + +/// Create `KeyEvent` for the given `NSEvent`. +/// +/// This function shouldn't be called when the IME input is in process. +pub(crate) fn create_key_event(ns_event: &NSEvent, is_press: bool, is_repeat: bool) -> KeyEvent { + use ElementState::{Pressed, Released}; + let state = if is_press { Pressed } else { Released }; + + let scancode = unsafe { ns_event.keyCode() }; + let mut physical_key = scancode_to_physicalkey(scancode as u32); + + // NOTE: The logical key should heed both SHIFT and ALT if possible. + // For instance: + // * Pressing the A key: logical key should be "a" + // * Pressing SHIFT A: logical key should be "A" + // * Pressing CTRL SHIFT A: logical key should also be "A" + // This is not easy to tease out of `NSEvent`, but we do our best. + + let characters = unsafe { ns_event.characters() }.map(|s| s.to_string()).unwrap_or_default(); + let text_with_all_modifiers = if characters.is_empty() { + None + } else { + if matches!(physical_key, PhysicalKey::Unidentified(_)) { + // The key may be one of the funky function keys + physical_key = extra_function_key_to_code(scancode, &characters); + } + Some(SmolStr::new(characters)) + }; + + let key_from_code = code_to_key(physical_key, scancode); + let (logical_key, key_without_modifiers) = if matches!(key_from_code, Key::Unidentified(_)) { + // `get_modifierless_char/key_without_modifiers` ignores ALL modifiers. + let key_without_modifiers = get_modifierless_char(scancode); + + let modifiers = unsafe { ns_event.modifierFlags() }; + let has_ctrl = modifiers.contains(NSEventModifierFlags::NSEventModifierFlagControl); + let has_cmd = modifiers.contains(NSEventModifierFlags::NSEventModifierFlagCommand); + + let logical_key = match text_with_all_modifiers.as_ref() { + // Only checking for ctrl and cmd here, not checking for alt because we DO want to + // include its effect in the key. For example if -on the German layout- one + // presses alt+8, the logical key should be "{" + // Also not checking if this is a release event because then this issue would + // still affect the key release. + Some(text) if !has_ctrl && !has_cmd => { + // Character heeding both SHIFT and ALT. + Key::Character(text.clone()) + }, + + _ => match key_without_modifiers.as_ref() { + // Character heeding just SHIFT, ignoring ALT. + Key::Character(ch) => get_logical_key_char(ns_event, ch), + + // Character ignoring ALL modifiers. + _ => key_without_modifiers.clone(), + }, + }; + + (logical_key, key_without_modifiers) + } else { + (key_from_code.clone(), key_from_code) + }; + + let text = if is_press { logical_key.to_text().map(SmolStr::new) } else { None }; + + let location = code_to_location(physical_key); + + KeyEvent { + location, + logical_key, + physical_key, + repeat: is_repeat, + state, + text, + platform_specific: KeyEventExtra { text_with_all_modifiers, key_without_modifiers }, + } +} + +pub fn code_to_key(key: PhysicalKey, scancode: u16) -> Key { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => return Key::Unidentified(code.into()), + }; + + Key::Named(match code { + KeyCode::Enter => NamedKey::Enter, + KeyCode::Tab => NamedKey::Tab, + KeyCode::Space => NamedKey::Space, + KeyCode::Backspace => NamedKey::Backspace, + KeyCode::Escape => NamedKey::Escape, + KeyCode::SuperRight => NamedKey::Super, + KeyCode::SuperLeft => NamedKey::Super, + KeyCode::ShiftLeft => NamedKey::Shift, + KeyCode::AltLeft => NamedKey::Alt, + KeyCode::ControlLeft => NamedKey::Control, + KeyCode::ShiftRight => NamedKey::Shift, + KeyCode::AltRight => NamedKey::Alt, + KeyCode::ControlRight => NamedKey::Control, + + KeyCode::NumLock => NamedKey::NumLock, + KeyCode::AudioVolumeUp => NamedKey::AudioVolumeUp, + KeyCode::AudioVolumeDown => NamedKey::AudioVolumeDown, + + // Other numpad keys all generate text on macOS (if I understand correctly) + KeyCode::NumpadEnter => NamedKey::Enter, + + KeyCode::F1 => NamedKey::F1, + KeyCode::F2 => NamedKey::F2, + KeyCode::F3 => NamedKey::F3, + KeyCode::F4 => NamedKey::F4, + KeyCode::F5 => NamedKey::F5, + KeyCode::F6 => NamedKey::F6, + KeyCode::F7 => NamedKey::F7, + KeyCode::F8 => NamedKey::F8, + KeyCode::F9 => NamedKey::F9, + KeyCode::F10 => NamedKey::F10, + KeyCode::F11 => NamedKey::F11, + KeyCode::F12 => NamedKey::F12, + KeyCode::F13 => NamedKey::F13, + KeyCode::F14 => NamedKey::F14, + KeyCode::F15 => NamedKey::F15, + KeyCode::F16 => NamedKey::F16, + KeyCode::F17 => NamedKey::F17, + KeyCode::F18 => NamedKey::F18, + KeyCode::F19 => NamedKey::F19, + KeyCode::F20 => NamedKey::F20, + + KeyCode::Insert => NamedKey::Insert, + KeyCode::Home => NamedKey::Home, + KeyCode::PageUp => NamedKey::PageUp, + KeyCode::Delete => NamedKey::Delete, + KeyCode::End => NamedKey::End, + KeyCode::PageDown => NamedKey::PageDown, + KeyCode::ArrowLeft => NamedKey::ArrowLeft, + KeyCode::ArrowRight => NamedKey::ArrowRight, + KeyCode::ArrowDown => NamedKey::ArrowDown, + KeyCode::ArrowUp => NamedKey::ArrowUp, + _ => return Key::Unidentified(NativeKey::MacOS(scancode)), + }) +} + +pub fn code_to_location(key: PhysicalKey) -> KeyLocation { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(_) => return KeyLocation::Standard, + }; + + 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) -> PhysicalKey { + if let Some(ch) = string.encode_utf16().next() { + match ch { + 0xf718 => PhysicalKey::Code(KeyCode::F21), + 0xf719 => PhysicalKey::Code(KeyCode::F22), + 0xf71a => PhysicalKey::Code(KeyCode::F23), + 0xf71b => PhysicalKey::Code(KeyCode::F24), + _ => PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode)), + } + } else { + PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode)) + } +} + +// The values are from the https://github.com/apple-oss-distributions/IOHIDFamily/blob/19666c840a6d896468416ff0007040a10b7b46b8/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h#L258-L259 +const NX_DEVICELCTLKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000001); +const NX_DEVICELSHIFTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000002); +const NX_DEVICERSHIFTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000004); +const NX_DEVICELCMDKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000008); +const NX_DEVICERCMDKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000010); +const NX_DEVICELALTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000020); +const NX_DEVICERALTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000040); +const NX_DEVICERCTLKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00002000); + +pub(super) fn lalt_pressed(event: &NSEvent) -> bool { + unsafe { event.modifierFlags() }.contains(NX_DEVICELALTKEYMASK) +} + +pub(super) fn ralt_pressed(event: &NSEvent) -> bool { + unsafe { event.modifierFlags() }.contains(NX_DEVICERALTKEYMASK) +} + +pub(super) fn event_mods(event: &NSEvent) -> Modifiers { + let flags = unsafe { event.modifierFlags() }; + let mut state = ModifiersState::empty(); + let mut pressed_mods = ModifiersKeys::empty(); + + state + .set(ModifiersState::SHIFT, flags.contains(NSEventModifierFlags::NSEventModifierFlagShift)); + pressed_mods.set(ModifiersKeys::LSHIFT, flags.contains(NX_DEVICELSHIFTKEYMASK)); + pressed_mods.set(ModifiersKeys::RSHIFT, flags.contains(NX_DEVICERSHIFTKEYMASK)); + + state.set( + ModifiersState::CONTROL, + flags.contains(NSEventModifierFlags::NSEventModifierFlagControl), + ); + pressed_mods.set(ModifiersKeys::LCONTROL, flags.contains(NX_DEVICELCTLKEYMASK)); + pressed_mods.set(ModifiersKeys::RCONTROL, flags.contains(NX_DEVICERCTLKEYMASK)); + + state.set(ModifiersState::ALT, flags.contains(NSEventModifierFlags::NSEventModifierFlagOption)); + pressed_mods.set(ModifiersKeys::LALT, flags.contains(NX_DEVICELALTKEYMASK)); + pressed_mods.set(ModifiersKeys::RALT, flags.contains(NX_DEVICERALTKEYMASK)); + + state.set( + ModifiersState::SUPER, + flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand), + ); + pressed_mods.set(ModifiersKeys::LSUPER, flags.contains(NX_DEVICELCMDKEYMASK)); + pressed_mods.set(ModifiersKeys::RSUPER, flags.contains(NX_DEVICERCMDKEYMASK)); + + Modifiers { state, pressed_mods } +} + +pub(super) fn dummy_event() -> Option> { + unsafe { + NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2( + NSEventType::ApplicationDefined, + NSPoint::new(0.0, 0.0), + NSEventModifierFlags(0), + 0.0, + 0, + None, + NSEventSubtype::WindowExposed.0, + 0, + 0, + ) + } +} + +pub(crate) fn physicalkey_to_scancode(physical_key: PhysicalKey) -> Option { + let code = match physical_key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(_) => return None, + }; + + 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(crate) fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey { + PhysicalKey::Code(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, + 0x4a => 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, + _ => return PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode as u16)), + }) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/event_handler.rs b/third_party/winit-0.30.13/src/platform_impl/macos/event_handler.rs new file mode 100644 index 0000000..5c353c1 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/event_handler.rs @@ -0,0 +1,139 @@ +use std::cell::RefCell; +use std::{fmt, mem}; + +use super::app_state::HandlePendingUserEvents; +use crate::event::Event; +use crate::event_loop::ActiveEventLoop as RootActiveEventLoop; + +struct EventHandlerData { + #[allow(clippy::type_complexity)] + handler: Box, &RootActiveEventLoop) + 'static>, +} + +impl fmt::Debug for EventHandlerData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EventHandlerData").finish_non_exhaustive() + } +} + +#[derive(Debug)] +pub(crate) struct EventHandler { + /// This can be in the following states: + /// - Not registered by the event loop (None). + /// - Present (Some(handler)). + /// - Currently executing the handler / in use (RefCell borrowed). + inner: RefCell>, +} + +impl EventHandler { + pub(crate) const fn new() -> Self { + Self { inner: RefCell::new(None) } + } + + /// Set the event loop handler for the duration of the given closure. + /// + /// This is similar to using the `scoped-tls` or `scoped-tls-hkt` crates + /// to store the handler in a thread local, such that it can be accessed + /// from within the closure. + pub(crate) fn set<'handler, R>( + &self, + handler: impl FnMut(Event, &RootActiveEventLoop) + 'handler, + closure: impl FnOnce() -> R, + ) -> R { + // SAFETY: We extend the lifetime of the handler here so that we can + // store it in `EventHandler`'s `RefCell`. + // + // This is sound, since we make sure to unset the handler again at the + // end of this function, and as such the lifetime isn't actually + // extended beyond `'handler`. + let handler = unsafe { + mem::transmute::< + Box, &RootActiveEventLoop) + 'handler>, + Box, &RootActiveEventLoop) + 'static>, + >(Box::new(handler)) + }; + + match self.inner.try_borrow_mut().as_deref_mut() { + Ok(Some(_)) => { + unreachable!("tried to set handler while another was already set"); + }, + Ok(data @ None) => { + *data = Some(EventHandlerData { handler }); + }, + Err(_) => { + unreachable!("tried to set handler that is currently in use"); + }, + } + + struct ClearOnDrop<'a>(&'a EventHandler); + + impl Drop for ClearOnDrop<'_> { + fn drop(&mut self) { + match self.0.inner.try_borrow_mut().as_deref_mut() { + Ok(data @ Some(_)) => { + *data = None; + }, + Ok(None) => { + tracing::error!("tried to clear handler, but no handler was set"); + }, + Err(_) => { + // Note: This is not expected to ever happen, this + // module generally controls the `RefCell`, and + // prevents it from ever being borrowed outside of it. + // + // But if it _does_ happen, it is a serious error, and + // we must abort the process, it'd be unsound if we + // weren't able to unset the handler. + eprintln!("tried to clear handler that is currently in use"); + std::process::abort(); + }, + } + } + } + + let _clear_on_drop = ClearOnDrop(self); + + // Note: The RefCell should not be borrowed while executing the + // closure, that'd defeat the whole point. + closure() + + // `_clear_on_drop` will be dropped here, or when unwinding, ensuring + // soundness. + } + + pub(crate) fn in_use(&self) -> bool { + self.inner.try_borrow().is_err() + } + + pub(crate) fn ready(&self) -> bool { + matches!(self.inner.try_borrow().as_deref(), Ok(Some(_))) + } + + pub(crate) fn handle_event( + &self, + event: Event, + event_loop: &RootActiveEventLoop, + ) { + match self.inner.try_borrow_mut().as_deref_mut() { + Ok(Some(EventHandlerData { handler })) => { + // It is important that we keep the reference borrowed here, + // so that `in_use` can properly detect that the handler is + // still in use. + // + // If the handler unwinds, the `RefMut` will ensure that the + // handler is no longer borrowed. + (handler)(event, event_loop); + }, + Ok(None) => { + // `NSApplication`, our app delegate and this handler are all + // global state and so it's not impossible that we could get + // an event after the application has exited the `EventLoop`. + tracing::error!("tried to run event handler, but no handler was set"); + }, + Err(_) => { + // Prevent re-entrancy. + panic!("tried to handle event while another event is currently being handled"); + }, + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/event_loop.rs b/third_party/winit-0.30.13/src/platform_impl/macos/event_loop.rs new file mode 100644 index 0000000..5c093d5 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/event_loop.rs @@ -0,0 +1,523 @@ +use std::any::Any; +use std::cell::Cell; +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::os::raw::c_void; +use std::panic::{catch_unwind, resume_unwind, RefUnwindSafe, UnwindSafe}; +use std::ptr; +use std::rc::{Rc, Weak}; +use std::sync::mpsc; +use std::time::{Duration, Instant}; + +use core_foundation::base::{CFIndex, CFRelease}; +use core_foundation::runloop::{ + kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopSourceContext, + CFRunLoopSourceCreate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, +}; +use objc2::rc::{autoreleasepool, Retained}; +use objc2::runtime::ProtocolObject; +use objc2::sel; +use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSWindow}; +use objc2_foundation::{MainThreadMarker, NSObjectProtocol}; + +use super::app::override_send_event; +use super::app_state::{ApplicationDelegate, HandlePendingUserEvents}; +use super::event::dummy_event; +use super::monitor::{self, MonitorHandle}; +use super::observer::setup_control_flow_observers; +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::{ + ActiveEventLoop as RootWindowTarget, ControlFlow, DeviceEvents, EventLoopClosed, +}; +use crate::platform::macos::ActivationPolicy; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::platform::cursor::CustomCursor; +use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource, 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 current 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(Debug)] +pub struct ActiveEventLoop { + delegate: Retained, + pub(super) mtm: MainThreadMarker, +} + +impl ActiveEventLoop { + pub(super) fn new_root(delegate: Retained) -> RootWindowTarget { + let mtm = MainThreadMarker::from(&*delegate); + let p = Self { delegate, mtm }; + RootWindowTarget { p, _marker: PhantomData } + } + + pub(super) fn app_delegate(&self) -> &ApplicationDelegate { + &self.delegate + } + + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + RootCustomCursor { inner: CustomCursor::new(source.inner) } + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(monitor) + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty()) + } + + #[inline] + pub fn system_theme(&self) -> Option { + let app = NSApplication::sharedApplication(self.mtm); + + if app.respondsToSelector(sel!(effectiveAppearance)) { + Some(super::window_delegate::appearance_to_theme(&app.effectiveAppearance())) + } else { + Some(Theme::Light) + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new())) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.delegate.set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.delegate.control_flow() + } + + pub(crate) fn exit(&self) { + self.delegate.exit() + } + + pub(crate) fn clear_exit(&self) { + self.delegate.clear_exit() + } + + pub(crate) fn exiting(&self) -> bool { + self.delegate.exiting() + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } + + pub(crate) fn hide_application(&self) { + NSApplication::sharedApplication(self.mtm).hide(None) + } + + pub(crate) fn hide_other_applications(&self) { + NSApplication::sharedApplication(self.mtm).hideOtherApplications(None) + } + + pub(crate) fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + NSWindow::setAllowsAutomaticWindowTabbing(enabled, self.mtm) + } + + pub(crate) fn allows_automatic_window_tabbing(&self) -> bool { + NSWindow::allowsAutomaticWindowTabbing(self.mtm) + } +} + +fn map_user_event( + mut handler: impl FnMut(Event, &RootWindowTarget), + receiver: Rc>, +) -> impl FnMut(Event, &RootWindowTarget) { + move |event, window_target| match event.map_nonuser_event() { + Ok(event) => (handler)(event, window_target), + Err(_) => { + for event in receiver.try_iter() { + (handler)(Event::UserEvent(event), window_target); + } + }, + } +} + +pub struct EventLoop { + /// Store a reference to the application for convenience. + /// + /// We intentionally don't store `WinitApplication` since we want to have + /// the possibility of swapping that out at some point. + app: Retained, + /// The application delegate that we've registered. + /// + /// The delegate is only weakly referenced by NSApplication, so we must + /// keep it around here as well. + delegate: Retained, + + // Event sender and receiver, used for EventLoopProxy. + sender: mpsc::Sender, + receiver: Rc>, + + window_target: RootWindowTarget, + panic_info: Rc, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) activation_policy: Option, + pub(crate) default_menu: bool, + pub(crate) activate_ignoring_other_apps: bool, +} + +impl Default for PlatformSpecificEventLoopAttributes { + fn default() -> Self { + Self { activation_policy: None, default_menu: true, activate_ignoring_other_apps: true } + } +} + +impl EventLoop { + pub(crate) fn new( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { + let mtm = MainThreadMarker::new() + .expect("on macOS, `EventLoop` must be created on the main thread!"); + + // Initialize the application (if it has not already been). + let app = NSApplication::sharedApplication(mtm); + + let activation_policy = match attributes.activation_policy { + None => None, + Some(ActivationPolicy::Regular) => Some(NSApplicationActivationPolicy::Regular), + Some(ActivationPolicy::Accessory) => Some(NSApplicationActivationPolicy::Accessory), + Some(ActivationPolicy::Prohibited) => Some(NSApplicationActivationPolicy::Prohibited), + }; + let delegate = ApplicationDelegate::new( + mtm, + activation_policy, + attributes.default_menu, + attributes.activate_ignoring_other_apps, + ); + + autoreleasepool(|_| { + app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + }); + + // Override `sendEvent:` on the application to forward to our application state. + override_send_event(&app); + + let panic_info: Rc = Default::default(); + setup_control_flow_observers(mtm, Rc::downgrade(&panic_info)); + + let (sender, receiver) = mpsc::channel(); + Ok(EventLoop { + app, + delegate: delegate.clone(), + sender, + receiver: Rc::new(receiver), + window_target: RootWindowTarget { + p: ActiveEventLoop { delegate, mtm }, + _marker: PhantomData, + }, + panic_info, + }) + } + + pub fn window_target(&self) -> &RootWindowTarget { + &self.window_target + } + + pub fn run(mut self, handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootWindowTarget), + { + self.run_on_demand(handler) + } + + // NB: we don't base this on `pump_events` because for `MacOs` we can't support + // `pump_events` elegantly (we just ask to run the loop for a "short" amount of + // time and so a layered implementation would end up using a lot of CPU due to + // redundant wake ups. + pub fn run_on_demand(&mut self, handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootWindowTarget), + { + let handler = map_user_event(handler, self.receiver.clone()); + + self.delegate.set_event_handler(handler, || { + autoreleasepool(|_| { + // clear / normalize pump_events state + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(false); + self.delegate.set_stop_after_wait(false); + self.delegate.set_stop_on_redraw(false); + + if self.delegate.is_launched() { + debug_assert!(!self.delegate.is_running()); + self.delegate.set_is_running(true); + self.delegate.dispatch_init_events(); + } + + // SAFETY: We do not run the application re-entrantly + unsafe { self.app.run() }; + + // While the app is running it's possible that we catch a panic + // to avoid unwinding across an objective-c ffi boundary, which + // will lead to us stopping the `NSApplication` and saving the + // `PanicInfo` so that we can resume the unwind at a controlled, + // safe point in time. + if let Some(panic) = self.panic_info.take() { + resume_unwind(panic); + } + + self.delegate.internal_exit() + }) + }); + + Ok(()) + } + + pub fn pump_events(&mut self, timeout: Option, handler: F) -> PumpStatus + where + F: FnMut(Event, &RootWindowTarget), + { + let handler = map_user_event(handler, self.receiver.clone()); + + self.delegate.set_event_handler(handler, || { + autoreleasepool(|_| { + // As a special case, if the application hasn't been launched yet then we at least + // run the loop until it has fully launched. + if !self.delegate.is_launched() { + debug_assert!(!self.delegate.is_running()); + + self.delegate.set_stop_on_launch(); + // SAFETY: We do not run the application re-entrantly + unsafe { self.app.run() }; + + // Note: we dispatch `NewEvents(Init)` + `Resumed` events after the application + // has launched + } else if !self.delegate.is_running() { + // Even though the application may have been launched, it's possible we aren't + // running if the `EventLoop` was run before and has since + // exited. This indicates that we just starting to re-run + // the same `EventLoop` again. + self.delegate.set_is_running(true); + self.delegate.dispatch_init_events(); + } else { + // Only run for as long as the given `Duration` allows so we don't block the + // external loop. + match timeout { + Some(Duration::ZERO) => { + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(true); + }, + Some(duration) => { + self.delegate.set_stop_before_wait(false); + let timeout = Instant::now() + duration; + self.delegate.set_wait_timeout(Some(timeout)); + self.delegate.set_stop_after_wait(true); + }, + None => { + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(false); + self.delegate.set_stop_after_wait(true); + }, + } + self.delegate.set_stop_on_redraw(true); + // SAFETY: We do not run the application re-entrantly + unsafe { self.app.run() }; + } + + // While the app is running it's possible that we catch a panic + // to avoid unwinding across an objective-c ffi boundary, which + // will lead to us stopping the application and saving the + // `PanicInfo` so that we can resume the unwind at a controlled, + // safe point in time. + if let Some(panic) = self.panic_info.take() { + resume_unwind(panic); + } + + if self.delegate.exiting() { + self.delegate.internal_exit(); + PumpStatus::Exit(0) + } else { + PumpStatus::Continue + } + }) + }) + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.sender.clone()) + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::AppKitDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::AppKitDisplayHandle::new().into()) + } +} + +pub(super) fn stop_app_immediately(app: &NSApplication) { + autoreleasepool(|_| { + app.stop(None); + // To stop event loop immediately, we need to post some event here. + // See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752 + app.postEvent_atStart(&dummy_event().unwrap(), true); + }); +} + +/// Tell all windows to close. +/// +/// This will synchronously trigger `WindowEvent::Destroyed` within +/// `windowWillClose:`, giving the application one last chance to handle +/// those events. It doesn't matter if the user also ends up closing the +/// windows in `Window`'s `Drop` impl, once a window has been closed once, it +/// stays closed. +/// +/// This ensures that no windows linger on after the event loop has exited, +/// see . +pub(super) fn notify_windows_of_exit(app: &NSApplication) { + for window in app.windows() { + window.close(); + } +} + +/// Catches panics that happen inside `f` and when a panic +/// happens, stops the `sharedApplication` +#[inline] +pub fn stop_app_on_panic R + UnwindSafe, R>( + mtm: MainThreadMarker, + 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); + } + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + None + }, + } +} + +pub struct EventLoopProxy { + sender: mpsc::Sender, + source: CFRunLoopSourceRef, +} + +unsafe impl Send for EventLoopProxy {} +unsafe impl Sync for EventLoopProxy {} + +impl Drop for EventLoopProxy { + fn drop(&mut self) { + unsafe { + CFRelease(self.source as _); + } + } +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy::new(self.sender.clone()) + } +} + +impl EventLoopProxy { + fn new(sender: mpsc::Sender) -> Self { + unsafe { + // just wake up the eventloop + extern "C" fn event_loop_proxy_handler(_: *const 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 { + version: 0, + info: ptr::null_mut(), + retain: None, + release: None, + copyDescription: None, + equal: None, + hash: None, + schedule: None, + cancel: None, + perform: 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(|mpsc::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/third_party/winit-0.30.13/src/platform_impl/macos/ffi.rs b/third_party/winit-0.30.13/src/platform_impl/macos/ffi.rs new file mode 100644 index 0000000..bdb0c30 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/ffi.rs @@ -0,0 +1,254 @@ +// TODO: Upstream these + +#![allow(dead_code, non_snake_case, non_upper_case_globals)] + +use std::ffi::c_void; + +use core_foundation::array::CFArrayRef; +use core_foundation::dictionary::CFDictionaryRef; +use core_foundation::string::CFStringRef; +use core_foundation::uuid::CFUUIDRef; +use core_graphics::base::CGError; +use core_graphics::display::{CGDirectDisplayID, CGDisplayConfigRef}; +// IRIS PATCH: NSInteger / AnyObject were only used by the removed private +// SkyLight blur declarations (see the extern block below). + +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 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; + + pub fn CGDisplayGetDisplayIDFromUUID(uuid: CFUUIDRef) -> CGDirectDisplayID; +} + +#[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 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); + + // IRIS PATCH (App Store, guideline 2.5.1): the private SkyLight blur APIs + // `CGSMainConnectionID` / `CGSSetWindowBackgroundBlurRadius` were declared + // here and called from WindowDelegate::set_blur. The Mac App Store rejects + // those symbols, so the declarations are removed and set_blur is a no-op. + // See rules/macos/appstore-private-api.md. +} + +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::*; +#[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 = std::os::raw::c_ushort; +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, + ) -> *mut c_void; + + 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; +} + +// CGWindowLevel.h +// +// Note: There are two different things at play in this header: +// `CGWindowLevel` and `CGWindowLevelKey`. +// +// It seems like there was a push towards using "key" values instead of the +// raw window level values, and then you were supposed to use +// `CGWindowLevelForKey` to get the actual level. +// +// But the values that `NSWindowLevel` has are compiled in, and as such has +// to remain ABI compatible, so they're safe for us to hardcode as well. +#[allow(dead_code, non_upper_case_globals)] +mod window_level { + const kCGNumReservedWindowLevels: i32 = 16; + const kCGNumReservedBaseWindowLevels: i32 = 5; + + pub const kCGBaseWindowLevel: i32 = i32::MIN; + pub const kCGMinimumWindowLevel: i32 = kCGBaseWindowLevel + kCGNumReservedBaseWindowLevels; + pub const kCGMaximumWindowLevel: i32 = i32::MAX - kCGNumReservedWindowLevels; + + pub const kCGDesktopWindowLevel: i32 = kCGMinimumWindowLevel + 20; + pub const kCGDesktopIconWindowLevel: i32 = kCGDesktopWindowLevel + 20; + pub const kCGBackstopMenuLevel: i32 = -20; + pub const kCGNormalWindowLevel: i32 = 0; + pub const kCGFloatingWindowLevel: i32 = 3; + pub const kCGTornOffMenuWindowLevel: i32 = 3; + pub const kCGModalPanelWindowLevel: i32 = 8; + pub const kCGUtilityWindowLevel: i32 = 19; + pub const kCGDockWindowLevel: i32 = 20; + pub const kCGMainMenuWindowLevel: i32 = 24; + pub const kCGStatusWindowLevel: i32 = 25; + pub const kCGPopUpMenuWindowLevel: i32 = 101; + pub const kCGOverlayWindowLevel: i32 = 102; + pub const kCGHelpWindowLevel: i32 = 200; + pub const kCGDraggingWindowLevel: i32 = 500; + pub const kCGScreenSaverWindowLevel: i32 = 1000; + pub const kCGAssistiveTechHighWindowLevel: i32 = 1500; + pub const kCGCursorWindowLevel: i32 = kCGMaximumWindowLevel - 1; +} + +pub use window_level::*; diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/menu.rs b/third_party/winit-0.30.13/src/platform_impl/macos/menu.rs new file mode 100644 index 0000000..cdebea0 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/menu.rs @@ -0,0 +1,107 @@ +use objc2::rc::Retained; +use objc2::runtime::Sel; +use objc2::sel; +use objc2_app_kit::{NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem}; +use objc2_foundation::{ns_string, MainThreadMarker, NSProcessInfo, NSString}; + +struct KeyEquivalent<'a> { + key: &'a NSString, + masks: Option, +} + +pub fn initialize(app: &NSApplication) { + let mtm = MainThreadMarker::from(app); + let menubar = NSMenu::new(mtm); + let app_menu_item = NSMenuItem::new(mtm); + menubar.addItem(&app_menu_item); + + let app_menu = NSMenu::new(mtm); + let process_name = NSProcessInfo::processInfo().processName(); + + // About menu item + let about_item_title = ns_string!("About ").stringByAppendingString(&process_name); + let about_item = + menu_item(mtm, &about_item_title, Some(sel!(orderFrontStandardAboutPanel:)), None); + + // Services menu item + let services_menu = NSMenu::new(mtm); + let services_item = menu_item(mtm, ns_string!("Services"), None, None); + services_item.setSubmenu(Some(&services_menu)); + + // Separator menu item + let sep_first = NSMenuItem::separatorItem(mtm); + + // Hide application menu item + let hide_item_title = ns_string!("Hide ").stringByAppendingString(&process_name); + let hide_item = menu_item( + mtm, + &hide_item_title, + Some(sel!(hide:)), + Some(KeyEquivalent { key: ns_string!("h"), masks: None }), + ); + + // Hide other applications menu item + let hide_others_item_title = ns_string!("Hide Others"); + let hide_others_item = menu_item( + mtm, + hide_others_item_title, + Some(sel!(hideOtherApplications:)), + Some(KeyEquivalent { + key: ns_string!("h"), + masks: Some( + NSEventModifierFlags::NSEventModifierFlagOption + | NSEventModifierFlags::NSEventModifierFlagCommand, + ), + }), + ); + + // Show applications menu item + let show_all_item_title = ns_string!("Show All"); + let show_all_item = + menu_item(mtm, show_all_item_title, Some(sel!(unhideAllApplications:)), None); + + // Separator menu item + let sep = NSMenuItem::separatorItem(mtm); + + // Quit application menu item + let quit_item_title = ns_string!("Quit ").stringByAppendingString(&process_name); + let quit_item = menu_item( + mtm, + &quit_item_title, + Some(sel!(terminate:)), + Some(KeyEquivalent { key: ns_string!("q"), masks: None }), + ); + + app_menu.addItem(&about_item); + app_menu.addItem(&sep_first); + app_menu.addItem(&services_item); + app_menu.addItem(&hide_item); + app_menu.addItem(&hide_others_item); + app_menu.addItem(&show_all_item); + app_menu.addItem(&sep); + app_menu.addItem(&quit_item); + app_menu_item.setSubmenu(Some(&app_menu)); + + unsafe { app.setServicesMenu(Some(&services_menu)) }; + app.setMainMenu(Some(&menubar)); +} + +fn menu_item( + mtm: MainThreadMarker, + title: &NSString, + selector: Option, + key_equivalent: Option>, +) -> Retained { + let (key, masks) = match key_equivalent { + Some(ke) => (ke.key, ke.masks), + None => (ns_string!(""), None), + }; + let item = unsafe { + NSMenuItem::initWithTitle_action_keyEquivalent(mtm.alloc(), title, selector, key) + }; + if let Some(masks) = masks { + item.setKeyEquivalentModifierMask(masks) + } + + item +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/mod.rs b/third_party/winit-0.30.13/src/platform_impl/macos/mod.rs new file mode 100644 index 0000000..1b427a8 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/mod.rs @@ -0,0 +1,61 @@ +#[macro_use] +mod util; + +mod app; +mod app_state; +mod cursor; +mod event; +mod event_handler; +mod event_loop; +mod ffi; +mod menu; +mod monitor; +mod observer; +mod view; +mod window; +mod window_delegate; + +use std::fmt; + +pub(crate) use self::event::{physicalkey_to_scancode, scancode_to_physicalkey, KeyEventExtra}; +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub(crate) use self::monitor::{MonitorHandle, VideoModeHandle}; +pub(crate) use self::window::WindowId; +pub(crate) use self::window_delegate::PlatformSpecificWindowAttributes; +use crate::event::DeviceId as RootDeviceId; + +pub(crate) use self::cursor::CustomCursor as PlatformCustomCursor; +pub(crate) use self::window::Window; +pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSource; +pub(crate) use crate::icon::NoIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId; + +impl DeviceId { + pub const 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); + +#[derive(Debug)] +pub enum OsError { + CGError(core_graphics::base::CGError), + CreationError(&'static str), +} + +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/third_party/winit-0.30.13/src/platform_impl/macos/monitor.rs b/third_party/winit-0.30.13/src/platform_impl/macos/monitor.rs new file mode 100644 index 0000000..44e6316 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/monitor.rs @@ -0,0 +1,409 @@ +#![allow(clippy::unnecessary_cast)] + +use std::collections::VecDeque; +use std::fmt; + +use core_foundation::array::{CFArrayGetCount, CFArrayGetValueAtIndex}; +use core_foundation::base::{CFRelease, TCFType}; +use core_foundation::string::CFString; +use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUID}; +use core_graphics::display::{ + CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayCopyDisplayMode, +}; +use objc2::rc::Retained; +use objc2::runtime::AnyObject; +use objc2_app_kit::NSScreen; +use objc2_foundation::{ns_string, run_on_main, MainThreadMarker, NSNumber, NSPoint, NSRect}; +use tracing::warn; + +use super::ffi; +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; + +#[derive(Clone)] +pub struct VideoModeHandle { + size: PhysicalSize, + bit_depth: u16, + refresh_rate_millihertz: u32, + pub(crate) monitor: MonitorHandle, + pub(crate) native_mode: NativeDisplayMode, +} + +impl PartialEq for VideoModeHandle { + fn eq(&self, other: &Self) -> bool { + self.size == other.size + && self.bit_depth == other.bit_depth + && self.refresh_rate_millihertz == other.refresh_rate_millihertz + && self.monitor == other.monitor + } +} + +impl Eq for VideoModeHandle {} + +impl std::hash::Hash for VideoModeHandle { + fn hash(&self, state: &mut H) { + self.size.hash(state); + self.bit_depth.hash(state); + self.refresh_rate_millihertz.hash(state); + self.monitor.hash(state); + } +} + +impl std::fmt::Debug for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VideoModeHandle") + .field("size", &self.size) + .field("bit_depth", &self.bit_depth) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz) + .field("monitor", &self.monitor) + .finish() + } +} + +pub struct NativeDisplayMode(pub ffi::CGDisplayModeRef); + +unsafe impl Send for NativeDisplayMode {} +unsafe impl Sync 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 VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + self.size + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} + +/// `CGDirectDisplayID` is documented as: +/// > a framebuffer, a color correction (gamma) table, and possibly an attached monitor. +/// +/// That is, it doesn't actually represent the monitor itself. Instead, we use the UUID of the +/// monitor, as retrieved from `CGDisplayCreateUUIDFromDisplayID` (this makes the monitor ID stable, +/// even across reboots and video mode changes). +/// +/// NOTE: I'd be perfectly valid to store `[u8; 16]` in here instead, we only store `CFUUID` to +/// avoid having to re-create it when we want to fetch the display ID. +#[derive(Clone)] +pub struct MonitorHandle(CFUUID); + +// SAFETY: CFUUID is immutable. +// FIXME(madsmtm): Upstream this into `objc2-core-foundation`. +unsafe impl Send for MonitorHandle {} +unsafe impl Sync for MonitorHandle {} + +type MonitorUuid = [u8; 16]; + +impl MonitorHandle { + /// Internal comparisons of [`MonitorHandle`]s are done first requesting a UUID for the handle. + fn uuid(&self) -> MonitorUuid { + let uuid = unsafe { CFUUIDGetUUIDBytes(self.0.as_concrete_TypeRef()) }; + MonitorUuid::from([ + uuid.byte0, + uuid.byte1, + uuid.byte2, + uuid.byte3, + uuid.byte4, + uuid.byte5, + uuid.byte6, + uuid.byte7, + uuid.byte8, + uuid.byte9, + uuid.byte10, + uuid.byte11, + uuid.byte12, + uuid.byte13, + uuid.byte14, + uuid.byte15, + ]) + } + + fn display_id(&self) -> CGDirectDisplayID { + unsafe { ffi::CGDisplayGetDisplayIDFromUUID(self.0.as_concrete_TypeRef()) } + } + + #[track_caller] + pub(crate) fn new(display_id: CGDirectDisplayID) -> Option { + // kCGNullDirectDisplay + if display_id == 0 { + // `CGDisplayCreateUUIDFromDisplayID` checks kCGNullDirectDisplay internally. + warn!("constructing monitor from invalid display ID 0; falling back to main monitor"); + } + let ptr = unsafe { ffi::CGDisplayCreateUUIDFromDisplayID(display_id) }; + if ptr.is_null() { + return None; + } + Some(Self(unsafe { CFUUID::wrap_under_create_rule(ptr) })) + } +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.uuid() == other.uuid() + } +} + +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 { + self.uuid().cmp(&other.uuid()) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.uuid().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 { + // Display ID just fetched from `CGGetActiveDisplayList`, should be fine to unwrap. + monitors.push_back(MonitorHandle::new(display).expect("invalid display ID")); + } + monitors + } else { + VecDeque::with_capacity(0) + } +} + +pub fn primary_monitor() -> MonitorHandle { + // Display ID just fetched from `CGMainDisplayID`, should be fine to unwrap. + MonitorHandle::new(CGDisplay::main().id).expect("invalid display ID") +} + +impl fmt::Debug for MonitorHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MonitorHandle") + .field("name", &self.name()) + .field("native_identifier", &self.native_identifier()) + .field("size", &self.size()) + .field("position", &self.position()) + .field("scale_factor", &self.scale_factor()) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz()) + .finish_non_exhaustive() + } +} + +impl MonitorHandle { + // TODO: Be smarter about this: + // + pub fn name(&self) -> Option { + let screen_num = CGDisplay::new(self.display_id()).model_number(); + Some(format!("Monitor #{screen_num}")) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + self.display_id() + } + + pub fn size(&self) -> PhysicalSize { + let display = CGDisplay::new(self.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 { + // This is already in screen coordinates. If we were using `NSScreen`, + // then a conversion would've been needed: + // flip_window_screen_coordinates(self.ns_screen(mtm)?.frame()) + let bounds = unsafe { CGDisplayBounds(self.native_identifier()) }; + let position = LogicalPosition::new(bounds.origin.x, bounds.origin.y); + position.to_physical(self.scale_factor()) + } + + pub fn scale_factor(&self) -> f64 { + run_on_main(|mtm| { + match self.ns_screen(mtm) { + Some(screen) => screen.backingScaleFactor() as f64, + None => 1.0, // default to 1.0 when we can't find the screen + } + }) + } + + pub fn refresh_rate_millihertz(&self) -> Option { + unsafe { + let current_display_mode = + NativeDisplayMode(CGDisplayCopyDisplayMode(self.display_id()) as _); + let refresh_rate = ffi::CGDisplayModeGetRefreshRate(current_display_mode.0); + if refresh_rate > 0.0 { + return Some((refresh_rate * 1000.0).round() as u32); + } + + let mut display_link = std::ptr::null_mut(); + if ffi::CVDisplayLinkCreateWithCGDisplay(self.display_id(), &mut display_link) + != ffi::kCVReturnSuccess + { + return None; + } + let time = ffi::CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link); + ffi::CVDisplayLinkRelease(display_link); + + // This value is indefinite if an invalid display link was specified + if time.flags & ffi::kCVTimeIsIndefinite != 0 { + return None; + } + + (time.time_scale as i64).checked_div(time.time_value).map(|v| (v * 1000) as u32) + } + } + + pub fn video_modes(&self) -> impl Iterator { + let refresh_rate_millihertz = self.refresh_rate_millihertz().unwrap_or(0); + let monitor = self.clone(); + + unsafe { + let modes = { + let array = ffi::CGDisplayCopyAllDisplayModes(self.display_id(), std::ptr::null()); + if array.is_null() { + // Occasionally, certain CalDigit Thunderbolt Hubs report a spurious monitor + // during sleep/wake/cycling monitors. It tends to have null + // or 1 video mode only. See . + warn!(monitor = ?self, "failed to get a list of display modes"); + Vec::new() + } else { + 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_hertz = ffi::CGDisplayModeGetRefreshRate(mode); + + // CGDisplayModeGetRefreshRate returns 0.0 for any display that + // isn't a CRT + let refresh_rate_millihertz = if cg_refresh_rate_hertz > 0.0 { + (cg_refresh_rate_hertz * 1000.0).round() as u32 + } else { + refresh_rate_millihertz + }; + + 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!() + }; + + VideoModeHandle { + size: PhysicalSize::new( + ffi::CGDisplayModeGetPixelWidth(mode) as u32, + ffi::CGDisplayModeGetPixelHeight(mode) as u32, + ), + refresh_rate_millihertz, + bit_depth, + monitor: monitor.clone(), + native_mode: NativeDisplayMode(mode), + } + }) + } + } + + pub(crate) fn ns_screen(&self, mtm: MainThreadMarker) -> Option> { + let uuid = self.uuid(); + NSScreen::screens(mtm).into_iter().find(|screen| { + let other_native_id = get_display_id(screen); + if let Some(other) = MonitorHandle::new(other_native_id) { + uuid == other.uuid() + } else { + // Display ID was just fetched from live NSScreen, but can still result in `None` + // with certain Thunderbolt docked monitors. + warn!(other_native_id, "comparing against screen with invalid display ID"); + false + } + }) + } +} + +pub(crate) fn get_display_id(screen: &NSScreen) -> u32 { + let key = ns_string!("NSScreenNumber"); + + objc2::rc::autoreleasepool(|_| { + let device_description = screen.deviceDescription(); + + // Retrieve the CGDirectDisplayID associated with this screen + // + // SAFETY: The value from @"NSScreenNumber" in deviceDescription is guaranteed + // to be an NSNumber. See documentation for `deviceDescription` for details: + // + let obj = device_description + .get(key) + .expect("failed getting screen display id from device description"); + let obj: *const AnyObject = obj; + let obj: *const NSNumber = obj.cast(); + let obj: &NSNumber = unsafe { &*obj }; + + obj.as_u32() + }) +} + +/// Core graphics screen coordinates are relative to the top-left corner of +/// the so-called "main" display, with y increasing downwards - which is +/// exactly what we want in Winit. +/// +/// However, `NSWindow` and `NSScreen` changes these coordinates to: +/// 1. Be relative to the bottom-left corner of the "main" screen. +/// 2. Be relative to the bottom-left corner of the window/screen itself. +/// 3. Have y increasing upwards. +/// +/// This conversion happens to be symmetric, so we only need this one function +/// to convert between the two coordinate systems. +pub(crate) fn flip_window_screen_coordinates(frame: NSRect) -> NSPoint { + // It is intentional that we use `CGMainDisplayID` (as opposed to + // `NSScreen::mainScreen`), because that's what the screen coordinates + // are relative to, no matter which display the window is currently on. + let main_screen_height = CGDisplay::main().bounds().size.height; + + let y = main_screen_height - frame.size.height - frame.origin.y; + NSPoint::new(frame.origin.x, y) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/observer.rs b/third_party/winit-0.30.13/src/platform_impl/macos/observer.rs new file mode 100644 index 0000000..8339803 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/observer.rs @@ -0,0 +1,312 @@ +//! Utilities for working with `CFRunLoop`. +//! +//! See Apple's documentation on Run Loops for details: +//! +use std::cell::Cell; +use std::ffi::c_void; +use std::panic::{AssertUnwindSafe, UnwindSafe}; +use std::ptr; +use std::rc::Weak; +use std::time::Instant; + +use block2::Block; +use core_foundation::base::{CFIndex, CFOptionFlags, CFRelease, CFTypeRef}; +use core_foundation::date::CFAbsoluteTimeGetCurrent; +use core_foundation::runloop::{ + kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, kCFRunLoopDefaultMode, + kCFRunLoopExit, CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddTimer, CFRunLoopGetMain, + CFRunLoopObserverCallBack, CFRunLoopObserverContext, CFRunLoopObserverCreate, + CFRunLoopObserverRef, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, + CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, CFRunLoopWakeUp, +}; +use objc2_foundation::MainThreadMarker; +use tracing::error; + +use super::app_state::ApplicationDelegate; +use super::event_loop::{stop_app_on_panic, PanicInfo}; +use super::ffi; + +unsafe fn control_flow_handler(panic_info: *mut c_void, f: F) +where + F: FnOnce(Weak) + UnwindSafe, +{ + let info_from_raw = unsafe { 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); + + let mtm = MainThreadMarker::new().unwrap(); + stop_app_on_panic(mtm, 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`"); + ApplicationDelegate::get(MainThreadMarker::new().unwrap()).wakeup(panic_info); + // trace!("Completed `CFRunLoopAfterWaiting`"); + }, + _ => unreachable!(), + } + }); + } +} + +// end is queued with the lowest priority to ensure it is processed after other observers +// without that, LoopExiting would get sent after AboutToWait +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`"); + ApplicationDelegate::get(MainThreadMarker::new().unwrap()).cleared(panic_info); + // trace!("Completed `CFRunLoopBeforeWaiting`"); + }, + kCFRunLoopExit => (), // unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } + }); + } +} + +#[derive(Debug)] +pub struct RunLoop(CFRunLoopRef); + +impl Default for RunLoop { + fn default() -> Self { + Self(ptr::null_mut()) + } +} + +impl RunLoop { + pub fn main(mtm: MainThreadMarker) -> Self { + // SAFETY: We have a MainThreadMarker here, which means we know we're on the main thread, so + // scheduling (and scheduling a non-`Send` block) to that thread is allowed. + let _ = mtm; + RunLoop(unsafe { CFRunLoopGetMain() }) + } + + pub fn wakeup(&self) { + unsafe { CFRunLoopWakeUp(self.0) } + } + + unsafe fn add_observer( + &self, + flags: CFOptionFlags, + priority: CFIndex, + handler: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, + ) { + let observer = unsafe { + 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, + ) + }; + unsafe { CFRunLoopAddObserver(self.0, observer, kCFRunLoopCommonModes) }; + } + + /// Submit a closure to run on the main thread as the next step in the run loop, before other + /// event sources are processed. + /// + /// This is used for running event handlers, as those are not allowed to run re-entrantly. + /// + /// # Implementation + /// + /// This queuing could be implemented in the following several ways with subtle differences in + /// timing. This list is sorted in rough order in which they are run: + /// + /// 1. Using `CFRunLoopPerformBlock` or `-[NSRunLoop performBlock:]`. + /// + /// 2. Using `-[NSObject performSelectorOnMainThread:withObject:waitUntilDone:]` or wrapping the + /// event in `NSEvent` and posting that to `-[NSApplication postEvent:atStart:]` (both + /// creates a custom `CFRunLoopSource`, and signals that to wake up the main event loop). + /// + /// a. `atStart = true`. + /// + /// b. `atStart = false`. + /// + /// 3. `dispatch_async` or `dispatch_async_f`. Note that this may appear before 2b, it does not + /// respect the ordering that runloop events have. + /// + /// We choose the first one, both for ease-of-implementation, but mostly for consistency, as we + /// want the event to be queued in a way that preserves the order the events originally arrived + /// in. + /// + /// As an example, let's assume that we receive two events from the user, a mouse click which we + /// handled by queuing it, and a window resize which we handled immediately. If we allowed + /// AppKit to choose the ordering when queuing the mouse event, it might get put in the back of + /// the queue, and the events would appear out of order to the user of Winit. So we must instead + /// put the event at the very front of the queue, to be handled as soon as possible after + /// handling whatever event it's currently handling. + pub fn queue_closure(&self, closure: impl FnOnce() + 'static) { + extern "C" { + fn CFRunLoopPerformBlock(rl: CFRunLoopRef, mode: CFTypeRef, block: &Block); + } + + // Convert `FnOnce()` to `Block`. + let closure = Cell::new(Some(closure)); + let block = block2::RcBlock::new(move || { + if let Some(closure) = closure.take() { + closure() + } else { + error!("tried to execute queued closure on main thread twice"); + } + }); + + // There are a few common modes (`kCFRunLoopCommonModes`) defined by Cocoa: + // - `NSDefaultRunLoopMode`, alias of `kCFRunLoopDefaultMode`. + // - `NSEventTrackingRunLoopMode`, used when mouse-dragging and live-resizing a window. + // - `NSModalPanelRunLoopMode`, used when running a modal inside the Winit event loop. + // - `NSConnectionReplyMode`: TODO. + // + // We only want to run event handlers in the default mode, as we support running a blocking + // modal inside a Winit event handler (see [#1779]) which outrules the modal panel mode, and + // resizing such panel window enters the event tracking run loop mode, so we can't directly + // trigger events inside that mode either. + // + // Any events that are queued while running a modal or when live-resizing will instead wait, + // and be delivered to the application afterwards. + // + // [#1779]: https://github.com/rust-windowing/winit/issues/1779 + let mode = unsafe { kCFRunLoopDefaultMode as CFTypeRef }; + + // SAFETY: The runloop is valid, the mode is a `CFStringRef`, and the block is `'static`. + unsafe { CFRunLoopPerformBlock(self.0, mode, &block) } + } +} + +pub fn setup_control_flow_observers(mtm: MainThreadMarker, panic_info: Weak) { + let run_loop = RunLoop::main(mtm); + unsafe { + let mut context = CFRunLoopObserverContext { + info: Weak::into_raw(panic_info) as *mut _, + version: 0, + retain: None, + release: None, + copyDescription: None, + }; + run_loop.add_observer( + 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 _, + ); + } +} + +#[derive(Debug)] +pub struct EventLoopWaker { + timer: CFRunLoopTimerRef, + + /// An arbitrary instant in the past, that will trigger an immediate wake + /// We save this as the `next_fire_date` for consistency so we can + /// easily check if the next_fire_date needs updating. + start_instant: Instant, + + /// This is what the `NextFireDate` has been set to. + /// `None` corresponds to `waker.stop()` and `start_instant` is used + /// for `waker.start()` + next_fire_date: Option, +} + +impl Drop for EventLoopWaker { + fn drop(&mut self) { + unsafe { + CFRunLoopTimerInvalidate(self.timer); + CFRelease(self.timer as _); + } + } +} + +impl EventLoopWaker { + pub(crate) fn new() -> Self { + 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); + Self { timer, start_instant: Instant::now(), next_fire_date: None } + } + } + + pub fn stop(&mut self) { + if self.next_fire_date.is_some() { + self.next_fire_date = None; + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MAX) } + } + } + + pub fn start(&mut self) { + if self.next_fire_date != Some(self.start_instant) { + self.next_fire_date = Some(self.start_instant); + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MIN) } + } + } + + pub fn start_at(&mut self, instant: Option) { + let now = Instant::now(); + match instant { + Some(instant) if now >= instant => { + self.start(); + }, + Some(instant) => { + if self.next_fire_date != Some(instant) { + self.next_fire_date = Some(instant); + 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) + } + } + }, + None => { + self.stop(); + }, + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/util.rs b/third_party/winit-0.30.13/src/platform_impl/macos/util.rs new file mode 100644 index 0000000..8110573 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/util.rs @@ -0,0 +1,27 @@ +use tracing::trace; + +macro_rules! trace_scope { + ($s:literal) => { + let _crate = $crate::platform_impl::platform::util::TraceGuard::new(module_path!(), $s); + }; +} + +pub(crate) struct TraceGuard { + module_path: &'static str, + called_from_fn: &'static str, +} + +impl TraceGuard { + #[inline] + pub(crate) fn new(module_path: &'static str, called_from_fn: &'static str) -> Self { + trace!(target = module_path, "Triggered `{}`", called_from_fn); + Self { module_path, called_from_fn } + } +} + +impl Drop for TraceGuard { + #[inline] + fn drop(&mut self) { + trace!(target = self.module_path, "Completed `{}`", self.called_from_fn); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/view.rs b/third_party/winit-0.30.13/src/platform_impl/macos/view.rs new file mode 100644 index 0000000..c7ca5bc --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/view.rs @@ -0,0 +1,1142 @@ +#![allow(clippy::unnecessary_cast)] +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, VecDeque}; +use std::ptr; + +use objc2::rc::{Retained, WeakId}; +use objc2::runtime::{AnyObject, Sel}; +use objc2::{declare_class, msg_send_id, mutability, sel, ClassType, DeclaredClass}; +use objc2_app_kit::{ + NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, + NSTrackingRectTag, NSView, NSViewFrameDidChangeNotification, +}; +use objc2_foundation::{ + MainThreadMarker, NSArray, NSAttributedString, NSAttributedStringKey, NSCopying, + NSMutableAttributedString, NSNotFound, NSNotificationCenter, NSObject, NSObjectProtocol, + NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger, +}; + +use super::app_state::ApplicationDelegate; +use super::cursor::{default_cursor, invisible_cursor}; +use super::event::{ + code_to_key, code_to_location, create_key_event, event_mods, lalt_pressed, ralt_pressed, + scancode_to_physicalkey, KeyEventExtra, +}; +use super::window::WinitWindow; +use super::DEVICE_ID; +use crate::dpi::{LogicalPosition, LogicalSize}; +use crate::event::{ + DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, + WindowEvent, +}; +use crate::keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey}; +use crate::platform::macos::OptionAsAlt; + +#[derive(Debug)] +struct CursorState { + visible: bool, + cursor: Retained, +} + +impl Default for CursorState { + fn default() -> Self { + Self { visible: true, cursor: default_cursor() } + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Default)] +enum ImeState { + #[default] + /// The IME events are disabled, so only `ReceivedCharacter` is being sent to the user. + Disabled, + + /// The ground state of enabled IME input. It means that both Preedit and regular keyboard + /// input could be start from it. + Ground, + + /// The IME is in preedit. + Preedit, + + /// The text was just committed, so the next input from the keyboard must be ignored. + Committed, +} + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq)] + struct ModLocationMask: u8 { + const LEFT = 0b0001; + const RIGHT = 0b0010; + } +} +impl ModLocationMask { + fn from_location(loc: KeyLocation) -> ModLocationMask { + match loc { + KeyLocation::Left => ModLocationMask::LEFT, + KeyLocation::Right => ModLocationMask::RIGHT, + _ => unreachable!(), + } + } +} + +fn key_to_modifier(key: &Key) -> Option { + match key { + Key::Named(NamedKey::Alt) => Some(ModifiersState::ALT), + Key::Named(NamedKey::Control) => Some(ModifiersState::CONTROL), + Key::Named(NamedKey::Super) => Some(ModifiersState::SUPER), + Key::Named(NamedKey::Shift) => Some(ModifiersState::SHIFT), + _ => None, + } +} + +fn get_right_modifier_code(key: &Key) -> KeyCode { + match key { + Key::Named(NamedKey::Alt) => KeyCode::AltRight, + Key::Named(NamedKey::Control) => KeyCode::ControlRight, + Key::Named(NamedKey::Shift) => KeyCode::ShiftRight, + Key::Named(NamedKey::Super) => KeyCode::SuperRight, + _ => unreachable!(), + } +} + +fn get_left_modifier_code(key: &Key) -> KeyCode { + match key { + Key::Named(NamedKey::Alt) => KeyCode::AltLeft, + Key::Named(NamedKey::Control) => KeyCode::ControlLeft, + Key::Named(NamedKey::Shift) => KeyCode::ShiftLeft, + Key::Named(NamedKey::Super) => KeyCode::SuperLeft, + _ => unreachable!(), + } +} + +#[derive(Debug)] +pub struct ViewState { + /// Strong reference to the global application state. + app_delegate: Retained, + + cursor_state: RefCell, + ime_position: Cell, + ime_size: Cell, + modifiers: Cell, + phys_modifiers: RefCell>, + tracking_rect: Cell>, + ime_state: Cell, + input_source: RefCell, + + /// True iff the application wants IME events. + /// + /// Can be set using `set_ime_allowed` + ime_allowed: Cell, + + /// True if the current key event should be forwarded + /// to the application, even during IME + forward_key_to_app: Cell, + + marked_text: RefCell>, + accepts_first_mouse: bool, + + // Weak reference because the window keeps a strong reference to the view + _ns_window: WeakId, + + /// The state of the `Option` as `Alt`. + option_as_alt: Cell, +} + +declare_class!( + pub(super) struct WinitView; + + unsafe impl ClassType for WinitView { + #[inherits(NSResponder, NSObject)] + type Super = NSView; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitView"; + } + + impl DeclaredClass for WinitView { + type Ivars = ViewState; + } + + unsafe impl WinitView { + #[method(isFlipped)] + fn is_flipped(&self) -> bool { + // `winit` uses the upper-left corner as the origin. + true + } + + #[method(viewDidMoveToWindow)] + fn view_did_move_to_window(&self) { + trace_scope!("viewDidMoveToWindow"); + if let Some(tracking_rect) = self.ivars().tracking_rect.take() { + self.removeTrackingRect(tracking_rect); + } + + let rect = self.frame(); + let tracking_rect = unsafe { + self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false) + }; + assert_ne!(tracking_rect, 0, "failed adding tracking rect"); + self.ivars().tracking_rect.set(Some(tracking_rect)); + } + + #[method(frameDidChange:)] + fn frame_did_change(&self, _event: &NSEvent) { + trace_scope!("frameDidChange:"); + if let Some(tracking_rect) = self.ivars().tracking_rect.take() { + self.removeTrackingRect(tracking_rect); + } + + let rect = self.frame(); + let tracking_rect = unsafe { + self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false) + }; + assert_ne!(tracking_rect, 0, "failed adding tracking rect"); + self.ivars().tracking_rect.set(Some(tracking_rect)); + + // Emit resize event here rather than from windowDidResize because: + // 1. When a new window is created as a tab, the frame size may change without a window resize occurring. + // 2. Even when a window resize does occur on a new tabbed window, it contains the wrong size (includes tab height). + let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); + let size = logical_size.to_physical::(self.scale_factor()); + self.queue_event(WindowEvent::Resized(size)); + } + + #[method(drawRect:)] + fn draw_rect(&self, _rect: NSRect) { + trace_scope!("drawRect:"); + + // It's a workaround for https://github.com/rust-windowing/winit/issues/2640, don't replace with `self.window_id()`. + if let Some(window) = self.ivars()._ns_window.load() { + self.ivars().app_delegate.handle_redraw(window.id()); + } + + // This is a direct subclass of NSView, no need to call superclass' drawRect: + } + + #[method(acceptsFirstResponder)] + fn accepts_first_responder(&self) -> bool { + trace_scope!("acceptsFirstResponder"); + true + } + + // 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` + #[method_id(touchBar)] + fn touch_bar(&self) -> Option> { + trace_scope!("touchBar"); + None + } + + #[method(resetCursorRects)] + fn reset_cursor_rects(&self) { + trace_scope!("resetCursorRects"); + let bounds = self.bounds(); + let cursor_state = self.ivars().cursor_state.borrow(); + // We correctly invoke `addCursorRect` only from inside `resetCursorRects` + if cursor_state.visible { + self.addCursorRect_cursor(bounds, &cursor_state.cursor); + } else { + self.addCursorRect_cursor(bounds, &invisible_cursor()); + } + } + } + + unsafe impl NSTextInputClient for WinitView { + #[method(hasMarkedText)] + fn has_marked_text(&self) -> bool { + trace_scope!("hasMarkedText"); + self.ivars().marked_text.borrow().length() > 0 + } + + #[method(markedRange)] + fn marked_range(&self) -> NSRange { + trace_scope!("markedRange"); + let length = self.ivars().marked_text.borrow().length(); + if length > 0 { + NSRange::new(0, length) + } else { + // Documented to return `{NSNotFound, 0}` if there is no marked range. + NSRange::new(NSNotFound as NSUInteger, 0) + } + } + + #[method(selectedRange)] + fn selected_range(&self) -> NSRange { + trace_scope!("selectedRange"); + // Documented to return `{NSNotFound, 0}` if there is no selection. + NSRange::new(NSNotFound as NSUInteger, 0) + } + + #[method(setMarkedText:selectedRange:replacementRange:)] + fn set_marked_text( + &self, + string: &NSObject, + selected_range: NSRange, + _replacement_range: NSRange, + ) { + // TODO: Use _replacement_range, requires changing the event to report surrounding text. + trace_scope!("setMarkedText:selectedRange:replacementRange:"); + + // SAFETY: This method is guaranteed to get either a `NSString` or a `NSAttributedString`. + let (marked_text, string) = if string.is_kind_of::() { + let string: *const NSObject = string; + let string: *const NSAttributedString = string.cast(); + let string = unsafe { &*string }; + ( + NSMutableAttributedString::from_attributed_nsstring(string), + string.string(), + ) + } else { + let string: *const NSObject = string; + let string: *const NSString = string.cast(); + let string = unsafe { &*string }; + ( + NSMutableAttributedString::from_nsstring(string), + string.copy(), + ) + }; + + // Update marked text. + *self.ivars().marked_text.borrow_mut() = marked_text; + + // Notify IME is active if application still doesn't know it. + if self.ivars().ime_state.get() == ImeState::Disabled { + *self.ivars().input_source.borrow_mut() = self.current_input_source(); + self.queue_event(WindowEvent::Ime(Ime::Enabled)); + } + + if unsafe { self.hasMarkedText() } { + self.ivars().ime_state.set(ImeState::Preedit); + } else { + // In case the preedit was cleared, set IME into the Ground state. + self.ivars().ime_state.set(ImeState::Ground); + } + + let cursor_range = if string.is_empty() { + // An empty string basically means that there's no preedit, so indicate that by + // sending a `None` cursor range. + None + } else { + // Clamp to string length to avoid NSRangeException from out-of-bounds + // indices sent by macOS IME (e.g. native Pinyin, see + // https://github.com/alacritty/alacritty/issues/8791). + let len = string.length(); + let location = selected_range.location.min(len); + let end = selected_range.end().min(len); + // Convert the selected range from UTF-16 indices to UTF-8 indices. + let sub_string_a = unsafe { string.substringToIndex(location) }; + let sub_string_b = unsafe { string.substringToIndex(end) }; + let lowerbound_utf8 = sub_string_a.len(); + let upperbound_utf8 = sub_string_b.len(); + Some((lowerbound_utf8, upperbound_utf8)) + }; + + // Send WindowEvent for updating marked text + self.queue_event(WindowEvent::Ime(Ime::Preedit(string.to_string(), cursor_range))); + } + + #[method(unmarkText)] + fn unmark_text(&self) { + trace_scope!("unmarkText"); + *self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new(); + + let input_context = self.inputContext().expect("input context"); + input_context.discardMarkedText(); + + self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None))); + if self.is_ime_enabled() { + // Leave the Preedit self.ivars() + self.ivars().ime_state.set(ImeState::Ground); + } else { + tracing::warn!("Expected to have IME enabled when receiving unmarkText"); + } + } + + #[method_id(validAttributesForMarkedText)] + fn valid_attributes_for_marked_text(&self) -> Retained> { + trace_scope!("validAttributesForMarkedText"); + NSArray::new() + } + + #[method_id(attributedSubstringForProposedRange:actualRange:)] + fn attributed_substring_for_proposed_range( + &self, + _range: NSRange, + _actual_range: *mut NSRange, + ) -> Option> { + trace_scope!("attributedSubstringForProposedRange:actualRange:"); + None + } + + #[method(characterIndexForPoint:)] + fn character_index_for_point(&self, _point: NSPoint) -> NSUInteger { + trace_scope!("characterIndexForPoint:"); + 0 + } + + #[method(firstRectForCharacterRange:actualRange:)] + fn first_rect_for_character_range( + &self, + _range: NSRange, + _actual_range: *mut NSRange, + ) -> NSRect { + trace_scope!("firstRectForCharacterRange:actualRange:"); + let rect = NSRect::new( + self.ivars().ime_position.get(), + self.ivars().ime_size.get() + ); + // Return value is expected to be in screen coordinates, so we need a conversion here + self.window() + .convertRectToScreen(self.convertRect_toView(rect, None)) + } + + #[method(insertText:replacementRange:)] + fn insert_text(&self, string: &NSObject, _replacement_range: NSRange) { + // TODO: Use _replacement_range, requires changing the event to report surrounding text. + trace_scope!("insertText:replacementRange:"); + + // SAFETY: This method is guaranteed to get either a `NSString` or a `NSAttributedString`. + let string = if string.is_kind_of::() { + let string: *const NSObject = string; + let string: *const NSAttributedString = string.cast(); + unsafe { &*string }.string().to_string() + } else { + let string: *const NSObject = string; + let string: *const NSString = string.cast(); + unsafe { &*string }.to_string() + }; + + let is_control = string.chars().next().is_some_and(|c| c.is_control()); + + // Commit only if we have marked text. + if unsafe { self.hasMarkedText() } && self.is_ime_enabled() && !is_control { + self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None))); + self.queue_event(WindowEvent::Ime(Ime::Commit(string))); + self.ivars().ime_state.set(ImeState::Committed); + } + } + + // 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. + #[method(doCommandBySelector:)] + fn do_command_by_selector(&self, _command: Sel) { + trace_scope!("doCommandBySelector:"); + // We shouldn't forward any character from just committed text, since we'll end up sending + // it twice with some IMEs like Korean one. We'll also always send `Enter` in that case, + // which is not desired given it was used to confirm IME input. + if self.ivars().ime_state.get() == ImeState::Committed { + return; + } + + self.ivars().forward_key_to_app.set(true); + + if unsafe { self.hasMarkedText() } && self.ivars().ime_state.get() == ImeState::Preedit + { + // Leave preedit so that we also report the key-up for this key. + self.ivars().ime_state.set(ImeState::Ground); + } + } + } + + unsafe impl WinitView { + #[method(keyDown:)] + fn key_down(&self, event: &NSEvent) { + trace_scope!("keyDown:"); + { + let mut prev_input_source = self.ivars().input_source.borrow_mut(); + let current_input_source = self.current_input_source(); + if *prev_input_source != current_input_source && self.is_ime_enabled() { + *prev_input_source = current_input_source; + drop(prev_input_source); + self.ivars().ime_state.set(ImeState::Disabled); + self.queue_event(WindowEvent::Ime(Ime::Disabled)); + } + } + + // Get the characters from the event. + let old_ime_state = self.ivars().ime_state.get(); + self.ivars().forward_key_to_app.set(false); + let event = replace_event(event, self.option_as_alt()); + + // The `interpretKeyEvents` function might call + // `setMarkedText`, `insertText`, and `doCommandBySelector`. + // It's important that we call this before queuing the KeyboardInput, because + // we must send the `KeyboardInput` event during IME if it triggered + // `doCommandBySelector`. (doCommandBySelector means that the keyboard input + // is not handled by IME and should be handled by the application) + if self.ivars().ime_allowed.get() { + let events_for_nsview = NSArray::from_slice(&[&*event]); + unsafe { self.interpretKeyEvents(&events_for_nsview) }; + + // If the text was committed we must treat the next keyboard event as IME related. + if self.ivars().ime_state.get() == ImeState::Committed { + // Remove any marked text, so normal input can continue. + *self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new(); + } + } + + self.update_modifiers(&event, false); + + let had_ime_input = match self.ivars().ime_state.get() { + ImeState::Committed => { + // Allow normal input after the commit. + self.ivars().ime_state.set(ImeState::Ground); + true + } + ImeState::Preedit => true, + // `key_down` could result in preedit clear, so compare old and current state. + _ => old_ime_state != self.ivars().ime_state.get(), + }; + + if !had_ime_input || self.ivars().forward_key_to_app.get() { + let key_event = create_key_event(&event, true, unsafe { event.isARepeat() }); + self.queue_event(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: key_event, + is_synthetic: false, + }); + } + } + + #[method(keyUp:)] + fn key_up(&self, event: &NSEvent) { + trace_scope!("keyUp:"); + + let event = replace_event(event, self.option_as_alt()); + self.update_modifiers(&event, false); + + // We want to send keyboard input when we are currently in the ground state. + if matches!( + self.ivars().ime_state.get(), + ImeState::Ground | ImeState::Disabled + ) { + self.queue_event(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: create_key_event(&event, false, false), + is_synthetic: false, + }); + } + } + + #[method(flagsChanged:)] + fn flags_changed(&self, event: &NSEvent) { + trace_scope!("flagsChanged:"); + + self.update_modifiers(event, true); + } + + #[method(insertTab:)] + fn insert_tab(&self, _sender: Option<&AnyObject>) { + trace_scope!("insertTab:"); + let window = self.window(); + if let Some(first_responder) = window.firstResponder() { + if *first_responder == ***self { + window.selectNextKeyView(Some(self)) + } + } + } + + #[method(insertBackTab:)] + fn insert_back_tab(&self, _sender: Option<&AnyObject>) { + trace_scope!("insertBackTab:"); + let window = self.window(); + if let Some(first_responder) = window.firstResponder() { + if *first_responder == ***self { + window.selectPreviousKeyView(Some(self)) + } + } + } + + // Allows us to receive Cmd-. (the shortcut for closing a dialog) + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6 + #[method(cancelOperation:)] + fn cancel_operation(&self, _sender: Option<&AnyObject>) { + let mtm = MainThreadMarker::from(self); + trace_scope!("cancelOperation:"); + + let event = NSApplication::sharedApplication(mtm) + .currentEvent() + .expect("could not find current event"); + + self.update_modifiers(&event, false); + let event = create_key_event(&event, true, unsafe { event.isARepeat() }); + + self.queue_event(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + + // In the past (?), `mouseMoved:` events were not generated when the + // user hovered over a window from a separate window, and as such the + // application might not know the location of the mouse in the event. + // + // To fix this, we emit `mouse_motion` inside of mouse click, mouse + // scroll, magnify and other gesture event handlers, to ensure that + // the application's state of where the mouse click was located is up + // to date. + // + // See https://github.com/rust-windowing/winit/pull/1490 for history. + + #[method(mouseDown:)] + fn mouse_down(&self, event: &NSEvent) { + trace_scope!("mouseDown:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Pressed); + } + + #[method(mouseUp:)] + fn mouse_up(&self, event: &NSEvent) { + trace_scope!("mouseUp:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Released); + } + + #[method(rightMouseDown:)] + fn right_mouse_down(&self, event: &NSEvent) { + trace_scope!("rightMouseDown:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Pressed); + } + + #[method(rightMouseUp:)] + fn right_mouse_up(&self, event: &NSEvent) { + trace_scope!("rightMouseUp:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Released); + } + + #[method(otherMouseDown:)] + fn other_mouse_down(&self, event: &NSEvent) { + trace_scope!("otherMouseDown:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Pressed); + } + + #[method(otherMouseUp:)] + fn other_mouse_up(&self, event: &NSEvent) { + trace_scope!("otherMouseUp:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Released); + } + + // No tracing on these because that would be overly verbose + + #[method(mouseMoved:)] + fn mouse_moved(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(mouseDragged:)] + fn mouse_dragged(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(rightMouseDragged:)] + fn right_mouse_dragged(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(otherMouseDragged:)] + fn other_mouse_dragged(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(mouseEntered:)] + fn mouse_entered(&self, _event: &NSEvent) { + trace_scope!("mouseEntered:"); + self.queue_event(WindowEvent::CursorEntered { + device_id: DEVICE_ID, + }); + } + + #[method(mouseExited:)] + fn mouse_exited(&self, _event: &NSEvent) { + trace_scope!("mouseExited:"); + + self.queue_event(WindowEvent::CursorLeft { + device_id: DEVICE_ID, + }); + } + + #[method(scrollWheel:)] + fn scroll_wheel(&self, event: &NSEvent) { + trace_scope!("scrollWheel:"); + + self.mouse_motion(event); + + let delta = { + let (x, y) = unsafe { (event.scrollingDeltaX(), event.scrollingDeltaY()) }; + if unsafe { event.hasPreciseScrollingDeltas() } { + let delta = LogicalPosition::new(x, y).to_physical(self.scale_factor()); + MouseScrollDelta::PixelDelta(delta) + } else { + MouseScrollDelta::LineDelta(x as f32, y as f32) + } + }; + + // The "momentum phase," if any, has higher priority than touch phase (the two should + // be mutually exclusive anyhow, which is why the API is rather incoherent). If no momentum + // phase is recorded (or rather, the started/ended cases of the momentum phase) then we + // report the touch phase. + #[allow(non_upper_case_globals)] + let phase = match unsafe { event.momentumPhase() } { + NSEventPhase::MayBegin | NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Ended | NSEventPhase::Cancelled => TouchPhase::Ended, + _ => match unsafe { event.phase() } { + NSEventPhase::MayBegin | NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Ended | NSEventPhase::Cancelled => TouchPhase::Ended, + _ => TouchPhase::Moved, + }, + }; + + self.update_modifiers(event, false); + + self.ivars().app_delegate.maybe_queue_device_event(DeviceEvent::MouseWheel { delta }); + self.queue_event(WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta, + phase, + }); + } + + #[method(magnifyWithEvent:)] + fn magnify_with_event(&self, event: &NSEvent) { + trace_scope!("magnifyWithEvent:"); + + self.mouse_motion(event); + + #[allow(non_upper_case_globals)] + let phase = match unsafe { event.phase() } { + NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Changed => TouchPhase::Moved, + NSEventPhase::Cancelled => TouchPhase::Cancelled, + NSEventPhase::Ended => TouchPhase::Ended, + _ => return, + }; + + self.queue_event(WindowEvent::PinchGesture { + device_id: DEVICE_ID, + delta: unsafe { event.magnification() }, + phase, + }); + } + + #[method(smartMagnifyWithEvent:)] + fn smart_magnify_with_event(&self, event: &NSEvent) { + trace_scope!("smartMagnifyWithEvent:"); + + self.mouse_motion(event); + + self.queue_event(WindowEvent::DoubleTapGesture { + device_id: DEVICE_ID, + }); + } + + #[method(rotateWithEvent:)] + fn rotate_with_event(&self, event: &NSEvent) { + trace_scope!("rotateWithEvent:"); + + self.mouse_motion(event); + + #[allow(non_upper_case_globals)] + let phase = match unsafe { event.phase() } { + NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Changed => TouchPhase::Moved, + NSEventPhase::Cancelled => TouchPhase::Cancelled, + NSEventPhase::Ended => TouchPhase::Ended, + _ => return, + }; + + self.queue_event(WindowEvent::RotationGesture { + device_id: DEVICE_ID, + delta: unsafe { event.rotation() }, + phase, + }); + } + + #[method(pressureChangeWithEvent:)] + fn pressure_change_with_event(&self, event: &NSEvent) { + trace_scope!("pressureChangeWithEvent:"); + + self.queue_event(WindowEvent::TouchpadPressure { + device_id: DEVICE_ID, + pressure: unsafe { event.pressure() }, + stage: unsafe { event.stage() } as i64, + }); + } + + // 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 + #[method(_wantsKeyDownForEvent:)] + fn wants_key_down_for_event(&self, _event: &NSEvent) -> bool { + trace_scope!("_wantsKeyDownForEvent:"); + true + } + + #[method(acceptsFirstMouse:)] + fn accepts_first_mouse(&self, _event: &NSEvent) -> bool { + trace_scope!("acceptsFirstMouse:"); + self.ivars().accepts_first_mouse + } + } +); + +impl WinitView { + pub(super) fn new( + app_delegate: &ApplicationDelegate, + window: &WinitWindow, + accepts_first_mouse: bool, + option_as_alt: OptionAsAlt, + ) -> Retained { + let mtm = MainThreadMarker::from(window); + let this = mtm.alloc().set_ivars(ViewState { + app_delegate: app_delegate.retain(), + cursor_state: Default::default(), + ime_position: Default::default(), + ime_size: Default::default(), + modifiers: Default::default(), + phys_modifiers: Default::default(), + tracking_rect: Default::default(), + ime_state: Default::default(), + input_source: Default::default(), + ime_allowed: Default::default(), + forward_key_to_app: Default::default(), + marked_text: Default::default(), + accepts_first_mouse, + _ns_window: WeakId::new(&window.retain()), + option_as_alt: Cell::new(option_as_alt), + }); + let this: Retained = unsafe { msg_send_id![super(this), init] }; + + this.setPostsFrameChangedNotifications(true); + let notification_center = unsafe { NSNotificationCenter::defaultCenter() }; + unsafe { + notification_center.addObserver_selector_name_object( + &this, + sel!(frameDidChange:), + Some(NSViewFrameDidChangeNotification), + Some(&this), + ) + } + + *this.ivars().input_source.borrow_mut() = this.current_input_source(); + + this + } + + fn window(&self) -> Retained { + // TODO: Simply use `window` property on `NSView`. + // That only returns a window _after_ the view has been attached though! + // (which is incompatible with `frameDidChange:`) + // + // unsafe { msg_send_id![self, window] } + self.ivars()._ns_window.load().expect("view to have a window") + } + + fn queue_event(&self, event: WindowEvent) { + self.ivars().app_delegate.maybe_queue_window_event(self.window().id(), event); + } + + fn scale_factor(&self) -> f64 { + self.window().backingScaleFactor() as f64 + } + + fn is_ime_enabled(&self) -> bool { + !matches!(self.ivars().ime_state.get(), ImeState::Disabled) + } + + fn current_input_source(&self) -> String { + self.inputContext() + .expect("input context") + .selectedKeyboardInputSource() + .map(|input_source| input_source.to_string()) + .unwrap_or_default() + } + + pub(super) fn cursor_icon(&self) -> Retained { + self.ivars().cursor_state.borrow().cursor.clone() + } + + pub(super) fn set_cursor_icon(&self, icon: Retained) { + let mut cursor_state = self.ivars().cursor_state.borrow_mut(); + cursor_state.cursor = icon; + } + + /// Set whether the cursor should be visible or not. + /// + /// Returns whether the state changed. + pub(super) fn set_cursor_visible(&self, visible: bool) -> bool { + let mut cursor_state = self.ivars().cursor_state.borrow_mut(); + if visible != cursor_state.visible { + cursor_state.visible = visible; + true + } else { + false + } + } + + pub(super) fn set_ime_allowed(&self, ime_allowed: bool) { + if self.ivars().ime_allowed.get() == ime_allowed { + return; + } + self.ivars().ime_allowed.set(ime_allowed); + if self.ivars().ime_allowed.get() { + return; + } + + // Clear markedText + *self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new(); + + if self.ivars().ime_state.get() != ImeState::Disabled { + self.ivars().ime_state.set(ImeState::Disabled); + self.queue_event(WindowEvent::Ime(Ime::Disabled)); + } + } + + pub(super) fn set_ime_cursor_area(&self, position: NSPoint, size: NSSize) { + self.ivars().ime_position.set(position); + self.ivars().ime_size.set(size); + let input_context = self.inputContext().expect("input context"); + input_context.invalidateCharacterCoordinates(); + } + + /// Reset modifiers and emit a synthetic ModifiersChanged event if deemed necessary. + pub(super) fn reset_modifiers(&self) { + if !self.ivars().modifiers.get().state().is_empty() { + self.ivars().modifiers.set(Modifiers::default()); + self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get())); + } + } + + pub(super) fn set_option_as_alt(&self, value: OptionAsAlt) { + self.ivars().option_as_alt.set(value) + } + + pub(super) fn option_as_alt(&self) -> OptionAsAlt { + self.ivars().option_as_alt.get() + } + + /// Update modifiers if `event` has something different + fn update_modifiers(&self, ns_event: &NSEvent, is_flags_changed_event: bool) { + use ElementState::{Pressed, Released}; + + let current_modifiers = event_mods(ns_event); + let prev_modifiers = self.ivars().modifiers.get(); + self.ivars().modifiers.set(current_modifiers); + + // This function was called form the flagsChanged event, which is triggered + // when the user presses/releases a modifier even if the same kind of modifier + // has already been pressed. + // + // When flags changed event has key code of zero it means that event doesn't carry any key + // event, thus we can't generate regular presses based on that. The `ModifiersChanged` + // later will work though, since the flags are attached to the event and contain valid + // information. + 'send_event: { + if is_flags_changed_event && unsafe { ns_event.keyCode() } != 0 { + let scancode = unsafe { ns_event.keyCode() }; + let physical_key = scancode_to_physicalkey(scancode as u32); + + let logical_key = code_to_key(physical_key, scancode); + // Ignore processing of unknown modifiers because we can't determine whether + // it was pressed or release reliably. + // + // Furthermore, sometimes normal keys are reported inside flagsChanged:, such as + // when holding Caps Lock while pressing another key, see: + // https://github.com/alacritty/alacritty/issues/8268 + let Some(event_modifier) = key_to_modifier(&logical_key) else { + break 'send_event; + }; + + let mut event = KeyEvent { + location: code_to_location(physical_key), + logical_key: logical_key.clone(), + physical_key, + repeat: false, + // We'll correct this later. + state: Pressed, + text: None, + platform_specific: KeyEventExtra { + text_with_all_modifiers: None, + key_without_modifiers: logical_key.clone(), + }, + }; + + let location_mask = ModLocationMask::from_location(event.location); + + let mut phys_mod_state = self.ivars().phys_modifiers.borrow_mut(); + let phys_mod = + phys_mod_state.entry(logical_key).or_insert(ModLocationMask::empty()); + + let is_active = current_modifiers.state().contains(event_modifier); + let mut events = VecDeque::with_capacity(2); + + // There is no API for getting whether the button was pressed or released + // during this event. For this reason we have to do a bit of magic below + // to come up with a good guess whether this key was pressed or released. + // (This is not trivial because there are multiple buttons that may affect + // the same modifier) + if !is_active { + event.state = Released; + if phys_mod.contains(ModLocationMask::LEFT) { + let mut event = event.clone(); + event.location = KeyLocation::Left; + event.physical_key = get_left_modifier_code(&event.logical_key).into(); + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + if phys_mod.contains(ModLocationMask::RIGHT) { + event.location = KeyLocation::Right; + event.physical_key = get_right_modifier_code(&event.logical_key).into(); + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + *phys_mod = ModLocationMask::empty(); + } else { + if *phys_mod == location_mask { + // Here we hit a contradiction: + // The modifier state was "changed" to active, + // yet the only pressed modifier key was the one that we + // just got a change event for. + // This seemingly means that the only pressed modifier is now released, + // but at the same time the modifier became active. + // + // But this scenario is possible if we released modifiers + // while the application was not in focus. (Because we don't + // get informed of modifier key events while the application + // is not focused) + + // In this case we prioritize the information + // about the current modifier state which means + // that the button was pressed. + event.state = Pressed; + } else { + phys_mod.toggle(location_mask); + let is_pressed = phys_mod.contains(location_mask); + event.state = if is_pressed { Pressed } else { Released }; + } + + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + + drop(phys_mod_state); + + for event in events { + self.queue_event(event); + } + } + } + + if prev_modifiers == current_modifiers { + return; + } + + self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get())); + } + + fn mouse_click(&self, event: &NSEvent, button_state: ElementState) { + let button = mouse_button(event); + + self.update_modifiers(event, false); + + self.queue_event(WindowEvent::MouseInput { + device_id: DEVICE_ID, + state: button_state, + button, + }); + } + + fn mouse_motion(&self, event: &NSEvent) { + let window_point = unsafe { event.locationInWindow() }; + let view_point = self.convertPoint_fromView(window_point, None); + let frame = self.frame(); + + if view_point.x.is_sign_negative() + || view_point.y.is_sign_negative() + || view_point.x > frame.size.width + || view_point.y > frame.size.height + { + let mouse_buttons_down = unsafe { NSEvent::pressedMouseButtons() }; + if mouse_buttons_down == 0 { + // Point is outside of the client area (view) and no buttons are pressed + return; + } + } + + let view_point = LogicalPosition::new(view_point.x, view_point.y); + + self.update_modifiers(event, false); + + self.queue_event(WindowEvent::CursorMoved { + device_id: DEVICE_ID, + position: view_point.to_physical(self.scale_factor()), + }); + } +} + +/// Get the mouse button from the NSEvent. +fn mouse_button(event: &NSEvent) -> MouseButton { + // The buttonNumber property only makes sense for the mouse events: + // NSLeftMouse.../NSRightMouse.../NSOtherMouse... + // For the other events, it's always set to 0. + // MacOS only defines the left, right and middle buttons, 3..=31 are left as generic buttons, + // but 3 and 4 are very commonly used as Back and Forward by hardware vendors and applications. + match unsafe { event.buttonNumber() } { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Back, + 4 => MouseButton::Forward, + n => MouseButton::Other(n as u16), + } +} + +// NOTE: to get option as alt working we need to rewrite events +// we're getting from the operating system, which makes it +// impossible to provide such events as extra in `KeyEvent`. +fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Retained { + let ev_mods = event_mods(event).state; + let ignore_alt_characters = match option_as_alt { + OptionAsAlt::OnlyLeft if lalt_pressed(event) => true, + OptionAsAlt::OnlyRight if ralt_pressed(event) => true, + OptionAsAlt::Both if ev_mods.alt_key() => true, + _ => false, + } && !ev_mods.control_key() + && !ev_mods.super_key(); + + if ignore_alt_characters { + let ns_chars = unsafe { + event.charactersIgnoringModifiers().expect("expected characters to be non-null") + }; + + unsafe { + NSEvent::keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode( + event.r#type(), + event.locationInWindow(), + event.modifierFlags(), + event.timestamp(), + event.windowNumber(), + None, + &ns_chars, + &ns_chars, + event.isARepeat(), + event.keyCode(), + ) + .unwrap() + } + } else { + event.copy() + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/window.rs b/third_party/winit-0.30.13/src/platform_impl/macos/window.rs new file mode 100644 index 0000000..04da8b2 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/window.rs @@ -0,0 +1,126 @@ +#![allow(clippy::unnecessary_cast)] + +use objc2::rc::{autoreleasepool, Retained}; +use objc2::{declare_class, mutability, ClassType, DeclaredClass}; +use objc2_app_kit::{NSResponder, NSWindow}; +use objc2_foundation::{MainThreadBound, MainThreadMarker, NSObject}; + +use super::event_loop::ActiveEventLoop; +use super::window_delegate::WindowDelegate; +use crate::error::OsError as RootOsError; +use crate::window::WindowAttributes; + +pub(crate) struct Window { + window: MainThreadBound>, + /// The window only keeps a weak reference to this, so we must keep it around here. + delegate: MainThreadBound>, +} + +impl Drop for Window { + fn drop(&mut self) { + self.window.get_on_main(|window| autoreleasepool(|_| window.close())) + } +} + +impl Window { + pub(crate) fn new( + window_target: &ActiveEventLoop, + attributes: WindowAttributes, + ) -> Result { + let mtm = window_target.mtm; + let delegate = autoreleasepool(|_| { + WindowDelegate::new(window_target.app_delegate(), attributes, mtm) + })?; + Ok(Window { + window: MainThreadBound::new(delegate.window().retain(), mtm), + delegate: MainThreadBound::new(delegate, mtm), + }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&WindowDelegate) + Send + 'static) { + // For now, don't actually do queuing, since it may be less predictable + self.maybe_wait_on_main(f) + } + + pub(crate) fn maybe_wait_on_main( + &self, + f: impl FnOnce(&WindowDelegate) -> R + Send, + ) -> R { + self.delegate.get_on_main(|delegate| f(delegate)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_window_handle_rwh_06( + &self, + ) -> Result { + if let Some(mtm) = MainThreadMarker::new() { + Ok(self.delegate.get(mtm).raw_window_handle_rwh_06()) + } else { + Err(rwh_06::HandleError::Unavailable) + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new())) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(pub usize); + +impl WindowId { + pub const fn dummy() -> Self { + Self(0) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as usize) + } +} + +declare_class!( + #[derive(Debug)] + pub struct WinitWindow; + + unsafe impl ClassType for WinitWindow { + #[inherits(NSResponder, NSObject)] + type Super = NSWindow; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitWindow"; + } + + impl DeclaredClass for WinitWindow {} + + unsafe impl WinitWindow { + #[method(canBecomeMainWindow)] + fn can_become_main_window(&self) -> bool { + trace_scope!("canBecomeMainWindow"); + true + } + + #[method(canBecomeKeyWindow)] + fn can_become_key_window(&self) -> bool { + trace_scope!("canBecomeKeyWindow"); + true + } + } +); + +impl WinitWindow { + pub(super) fn id(&self) -> WindowId { + WindowId(self as *const Self as usize) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/macos/window_delegate.rs b/third_party/winit-0.30.13/src/platform_impl/macos/window_delegate.rs new file mode 100644 index 0000000..becce31 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/macos/window_delegate.rs @@ -0,0 +1,1906 @@ +#![allow(clippy::unnecessary_cast)] +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::ffi::c_void; +use std::ptr; +use std::sync::{Arc, Mutex}; + +use core_graphics::display::{CGDisplay, CGPoint}; +use monitor::VideoModeHandle; +use objc2::rc::{autoreleasepool, Retained}; +use objc2::runtime::{AnyObject, ProtocolObject}; +use objc2::{declare_class, msg_send_id, mutability, sel, ClassType, DeclaredClass}; +use objc2_app_kit::{ + NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization, + NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType, + NSColor, NSDraggingDestination, NSFilenamesPboardType, NSPasteboard, + NSRequestUserAttentionType, NSScreen, NSView, NSWindowButton, NSWindowDelegate, + NSWindowFullScreenButton, NSWindowLevel, NSWindowOcclusionState, NSWindowOrderingMode, + NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility, +}; +use objc2_foundation::{ + ns_string, CGFloat, MainThreadMarker, NSArray, NSCopying, NSDictionary, NSKeyValueChangeKey, + NSKeyValueChangeNewKey, NSKeyValueChangeOldKey, NSKeyValueObservingOptions, NSObject, + NSObjectNSDelayedPerforming, NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSPoint, + NSRect, NSSize, NSString, +}; +use tracing::{trace, warn}; + +use super::app_state::ApplicationDelegate; +use super::cursor::cursor_from_icon; +use super::monitor::{self, flip_window_screen_coordinates, get_display_id}; +use super::observer::RunLoop; +use super::view::WinitView; +use super::window::WinitWindow; +use super::{ffi, Fullscreen, MonitorHandle, OsError, WindowId}; +use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::{InnerSizeWriter, WindowEvent}; +use crate::platform::macos::{OptionAsAlt, WindowExtMacOS}; +use crate::window::{ + Cursor, CursorGrabMode, Icon, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, +}; + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + 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 disallow_hidpi: bool, + pub has_shadow: bool, + pub accepts_first_mouse: bool, + pub tabbing_identifier: Option, + pub option_as_alt: OptionAsAlt, + pub borderless_game: bool, +} + +impl Default for PlatformSpecificWindowAttributes { + #[inline] + fn default() -> Self { + Self { + movable_by_window_background: false, + titlebar_transparent: false, + title_hidden: false, + titlebar_hidden: false, + titlebar_buttons_hidden: false, + fullsize_content_view: false, + disallow_hidpi: false, + has_shadow: true, + accepts_first_mouse: true, + tabbing_identifier: None, + option_as_alt: Default::default(), + borderless_game: false, + } + } +} + +#[derive(Debug)] +pub(crate) struct State { + /// Strong reference to the global application state. + app_delegate: Retained, + + window: Retained, + + // During `windowDidResize`, we use this to only send Moved if the position changed. + // + // This is expressed in desktop coordinates, and flipped to match Winit's coordinate system. + previous_position: Cell, + + // Used to prevent redundant events. + previous_scale_factor: Cell, + + /// The current resize increments for the window content. + resize_increments: Cell, + /// Whether the window is showing decorations. + decorations: Cell, + resizable: Cell, + maximized: Cell, + + /// 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: Cell>, + // This is set when WindowAttributes::with_fullscreen was set, + // see comments of `window_did_fail_to_enter_fullscreen` + initial_fullscreen: Cell, + /// This field tracks the current fullscreen state of the window + /// (as seen by `WindowDelegate`). + fullscreen: RefCell>, + // If it is attempted to toggle fullscreen when in_fullscreen_transition is true, + // Set target_fullscreen and do after fullscreen transition is end. + target_fullscreen: RefCell>>, + // This is true between windowWillEnterFullScreen and windowDidEnterFullScreen + // or windowWillExitFullScreen and windowDidExitFullScreen. + // We must not toggle fullscreen when this is true. + in_fullscreen_transition: Cell, + standard_frame: Cell>, + is_simple_fullscreen: Cell, + saved_style: Cell>, + is_borderless_game: Cell, +} + +declare_class!( + pub(crate) struct WindowDelegate; + + unsafe impl ClassType for WindowDelegate { + type Super = NSObject; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitWindowDelegate"; + } + + impl DeclaredClass for WindowDelegate { + type Ivars = State; + } + + unsafe impl NSObjectProtocol for WindowDelegate {} + + unsafe impl NSWindowDelegate for WindowDelegate { + #[method(windowShouldClose:)] + fn window_should_close(&self, _: Option<&AnyObject>) -> bool { + trace_scope!("windowShouldClose:"); + self.queue_event(WindowEvent::CloseRequested); + false + } + + #[method(windowWillClose:)] + fn window_will_close(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillClose:"); + // `setDelegate:` retains the previous value and then autoreleases it + autoreleasepool(|_| { + // Since El Capitan, we need to be careful that delegate methods can't + // be called after the window closes. + self.window().setDelegate(None); + }); + self.queue_event(WindowEvent::Destroyed); + } + + #[method(windowDidResize:)] + fn window_did_resize(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidResize:"); + // NOTE: WindowEvent::Resized is reported in frameDidChange. + self.emit_move_event(); + } + + #[method(windowWillStartLiveResize:)] + fn window_will_start_live_resize(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillStartLiveResize:"); + + let increments = self.ivars().resize_increments.get(); + self.set_resize_increments_inner(increments); + } + + #[method(windowDidEndLiveResize:)] + fn window_did_end_live_resize(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidEndLiveResize:"); + self.set_resize_increments_inner(NSSize::new(1., 1.)); + } + + // This won't be triggered if the move was part of a resize. + #[method(windowDidMove:)] + fn window_did_move(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidMove:"); + self.emit_move_event(); + } + + #[method(windowDidChangeBackingProperties:)] + fn window_did_change_backing_properties(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidChangeBackingProperties:"); + let scale_factor = self.scale_factor(); + if scale_factor == self.ivars().previous_scale_factor.get() { + return; + }; + self.ivars().previous_scale_factor.set(scale_factor); + + let mtm = MainThreadMarker::from(self); + let this = self.retain(); + RunLoop::main(mtm).queue_closure(move || { + this.handle_scale_factor_changed(scale_factor); + }); + } + + #[method(windowDidBecomeKey:)] + fn window_did_become_key(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidBecomeKey:"); + // TODO: center the cursor if the window had mouse grab when it + // lost focus + self.queue_event(WindowEvent::Focused(true)); + } + + #[method(windowDidResignKey:)] + fn window_did_resign_key(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidResignKey:"); + // 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. + self.view().reset_modifiers(); + + self.queue_event(WindowEvent::Focused(false)); + } + + /// Invoked when before enter fullscreen + #[method(windowWillEnterFullScreen:)] + fn window_will_enter_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillEnterFullScreen:"); + + self.ivars().maximized.set(self.is_zoomed()); + let mut fullscreen = self.ivars().fullscreen.borrow_mut(); + match &*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 = self.current_monitor_inner(); + *fullscreen = Some(Fullscreen::Borderless(current_monitor)); + }, + } + self.ivars().in_fullscreen_transition.set(true); + } + + /// Invoked when before exit fullscreen + #[method(windowWillExitFullScreen:)] + fn window_will_exit_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillExitFullScreen:"); + + self.ivars().in_fullscreen_transition.set(true); + } + + #[method(window:willUseFullScreenPresentationOptions:)] + fn window_will_use_fullscreen_presentation_options( + &self, + _: Option<&AnyObject>, + proposed_options: NSApplicationPresentationOptions, + ) -> NSApplicationPresentationOptions { + trace_scope!("window:willUseFullScreenPresentationOptions:"); + // 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 = proposed_options; + let fullscreen = self.ivars().fullscreen.borrow(); + if let Some(Fullscreen::Exclusive(_)) = &*fullscreen { + options = NSApplicationPresentationOptions::NSApplicationPresentationFullScreen + | NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar; + } + + options + } + + /// Invoked when entered fullscreen + #[method(windowDidEnterFullScreen:)] + fn window_did_enter_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidEnterFullScreen:"); + self.ivars().initial_fullscreen.set(false); + self.ivars().in_fullscreen_transition.set(false); + if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() { + self.set_fullscreen(target_fullscreen); + } + } + + /// Invoked when exited fullscreen + #[method(windowDidExitFullScreen:)] + fn window_did_exit_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidExitFullScreen:"); + + self.restore_state_from_fullscreen(); + self.ivars().in_fullscreen_transition.set(false); + if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() { + self.set_fullscreen(target_fullscreen); + } + } + + /// 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 desktop 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 initialization (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. + #[method(windowDidFailToEnterFullScreen:)] + fn window_did_fail_to_enter_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidFailToEnterFullScreen:"); + self.ivars().in_fullscreen_transition.set(false); + self.ivars().target_fullscreen.replace(None); + if self.ivars().initial_fullscreen.get() { + unsafe { + self.window().performSelector_withObject_afterDelay( + sel!(toggleFullScreen:), + None, + 0.5, + ) + }; + } else { + self.restore_state_from_fullscreen(); + } + } + + // Invoked when the occlusion state of the window changes + #[method(windowDidChangeOcclusionState:)] + fn window_did_change_occlusion_state(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidChangeOcclusionState:"); + let visible = self.window().occlusionState().contains(NSWindowOcclusionState::Visible); + self.queue_event(WindowEvent::Occluded(!visible)); + } + + #[method(windowDidChangeScreen:)] + fn window_did_change_screen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidChangeScreen:"); + let is_simple_fullscreen = self.ivars().is_simple_fullscreen.get(); + if is_simple_fullscreen { + if let Some(screen) = self.window().screen() { + self.window().setFrame_display(screen.frame(), true); + } + } + } + } + + unsafe impl NSDraggingDestination for WindowDelegate { + /// Invoked when the dragged image enters destination bounds or frame + #[method(draggingEntered:)] + fn dragging_entered(&self, sender: &NSObject) -> bool { + trace_scope!("draggingEntered:"); + + use std::path::PathBuf; + + let pb: Retained = unsafe { msg_send_id![sender, draggingPasteboard] }; + let filenames = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) { + Some(filenames) => filenames, + None => return false.into(), + }; + let filenames: Retained> = unsafe { Retained::cast(filenames) }; + + filenames.into_iter().for_each(|file| { + let path = PathBuf::from(file.to_string()); + self.queue_event(WindowEvent::HoveredFile(path)); + }); + + true + } + + /// Invoked when the image is released + #[method(prepareForDragOperation:)] + fn prepare_for_drag_operation(&self, _sender: &NSObject) -> bool { + trace_scope!("prepareForDragOperation:"); + true + } + + /// Invoked after the released image has been removed from the screen + #[method(performDragOperation:)] + fn perform_drag_operation(&self, sender: &NSObject) -> bool { + trace_scope!("performDragOperation:"); + + use std::path::PathBuf; + + let pb: Retained = unsafe { msg_send_id![sender, draggingPasteboard] }; + let filenames = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) { + Some(filenames) => filenames, + None => return false.into(), + }; + let filenames: Retained> = unsafe { Retained::cast(filenames) }; + + filenames.into_iter().for_each(|file| { + let path = PathBuf::from(file.to_string()); + self.queue_event(WindowEvent::DroppedFile(path)); + }); + + true + } + + /// Invoked when the dragging operation is complete + #[method(concludeDragOperation:)] + fn conclude_drag_operation(&self, _sender: Option<&NSObject>) { + trace_scope!("concludeDragOperation:"); + } + + /// Invoked when the dragging operation is cancelled + #[method(draggingExited:)] + fn dragging_exited(&self, _sender: Option<&NSObject>) { + trace_scope!("draggingExited:"); + self.queue_event(WindowEvent::HoveredFileCancelled); + } + } + + // Key-Value Observing + unsafe impl WindowDelegate { + #[method(observeValueForKeyPath:ofObject:change:context:)] + fn observe_value( + &self, + key_path: Option<&NSString>, + _object: Option<&AnyObject>, + change: Option<&NSDictionary>, + _context: *mut c_void, + ) { + trace_scope!("observeValueForKeyPath:ofObject:change:context:"); + // NOTE: We don't _really_ need to check the key path, as there should only be one, but + // in the future we might want to observe other key paths. + if key_path == Some(ns_string!("effectiveAppearance")) { + let change = change.expect("requested a change dictionary in `addObserver`, but none was provided"); + let old = change.get(unsafe { NSKeyValueChangeOldKey }).expect("requested change dictionary did not contain `NSKeyValueChangeOldKey`"); + let new = change.get(unsafe { NSKeyValueChangeNewKey }).expect("requested change dictionary did not contain `NSKeyValueChangeNewKey`"); + + // SAFETY: The value of `effectiveAppearance` is `NSAppearance` + let old: *const AnyObject = old; + let old: *const NSAppearance = old.cast(); + let old: &NSAppearance = unsafe { &*old }; + let new: *const AnyObject = new; + let new: *const NSAppearance = new.cast(); + let new: &NSAppearance = unsafe { &*new }; + + trace!(old = %unsafe { old.name() }, new = %unsafe { new.name() }, "effectiveAppearance changed"); + + // Ignore the change if the window's theme is customized by the user (since in that + // case the `effectiveAppearance` is only emitted upon said customization, and then + // it's triggered directly by a user action, and we don't want to emit the event). + if unsafe { self.window().appearance() }.is_some() { + return; + } + + let old = appearance_to_theme(old); + let new = appearance_to_theme(new); + // Check that the theme changed in Winit's terms (the theme might have changed on + // other parameters, such as level of contrast, but the event should not be emitted + // in those cases). + if old == new { + return; + } + + self.queue_event(WindowEvent::ThemeChanged(new)); + } else { + panic!("unknown observed keypath {key_path:?}"); + } + } + } +); + +impl Drop for WindowDelegate { + fn drop(&mut self) { + unsafe { + self.window().removeObserver_forKeyPath(self, ns_string!("effectiveAppearance")); + } + } +} + +fn new_window( + app_delegate: &ApplicationDelegate, + attrs: &WindowAttributes, + mtm: MainThreadMarker, +) -> Option> { + autoreleasepool(|_| { + let screen = match attrs.fullscreen.clone().map(Into::into) { + Some(Fullscreen::Borderless(Some(monitor))) + | Some(Fullscreen::Exclusive(VideoModeHandle { monitor, .. })) => { + monitor.ns_screen(mtm).or_else(|| NSScreen::mainScreen(mtm)) + }, + Some(Fullscreen::Borderless(None)) => NSScreen::mainScreen(mtm), + None => None, + }; + let frame = match &screen { + Some(screen) => screen.frame(), + None => { + let scale_factor = NSScreen::mainScreen(mtm) + .map(|screen| screen.backingScaleFactor() as f64) + .unwrap_or(1.0); + let size = match attrs.inner_size { + Some(size) => { + let size = size.to_logical(scale_factor); + NSSize::new(size.width, size.height) + }, + None => NSSize::new(800.0, 600.0), + }; + let position = match attrs.position { + Some(position) => { + let position = position.to_logical(scale_factor); + flip_window_screen_coordinates(NSRect::new( + NSPoint::new(position.x, position.y), + size, + )) + }, + // This value is ignored by calling win.center() below + None => NSPoint::new(0.0, 0.0), + }; + NSRect::new(position, size) + }, + }; + + let mut masks = if (!attrs.decorations && screen.is_none()) + || attrs.platform_specific.titlebar_hidden + { + // Resizable without a titlebar or borders + // if decorations is set to false, ignore pl_attrs + // + // if the titlebar is hidden, ignore other 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.enabled_buttons.contains(WindowButtons::MINIMIZE) { + masks &= !NSWindowStyleMask::Miniaturizable; + } + + if !attrs.enabled_buttons.contains(WindowButtons::CLOSE) { + masks &= !NSWindowStyleMask::Closable; + } + + if attrs.platform_specific.fullsize_content_view { + masks |= NSWindowStyleMask::FullSizeContentView; + } + + let window: Option> = unsafe { + msg_send_id![ + super(mtm.alloc().set_ivars(())), + initWithContentRect: frame, + styleMask: masks, + backing: NSBackingStoreType::NSBackingStoreBuffered, + defer: false, + ] + }; + let window = window?; + + // It is very important for correct memory management that we + // disable the extra release that would otherwise happen when + // calling `close` on the window. + unsafe { window.setReleasedWhenClosed(false) }; + + window.setTitle(&NSString::from_str(&attrs.title)); + window.setAcceptsMouseMovedEvents(true); + + if let Some(identifier) = &attrs.platform_specific.tabbing_identifier { + window.setTabbingIdentifier(&NSString::from_str(identifier)); + window.setTabbingMode(NSWindowTabbingMode::Preferred); + } + + if attrs.content_protected { + window.setSharingType(NSWindowSharingType::NSWindowSharingNone); + } + + if attrs.platform_specific.titlebar_transparent { + window.setTitlebarAppearsTransparent(true); + } + if attrs.platform_specific.title_hidden { + window.setTitleVisibility(NSWindowTitleVisibility::NSWindowTitleHidden); + } + if attrs.platform_specific.titlebar_buttons_hidden { + for titlebar_button in &[ + #[allow(deprecated)] + NSWindowFullScreenButton, + NSWindowButton::NSWindowMiniaturizeButton, + NSWindowButton::NSWindowCloseButton, + NSWindowButton::NSWindowZoomButton, + ] { + if let Some(button) = window.standardWindowButton(*titlebar_button) { + button.setHidden(true); + } + } + } + if attrs.platform_specific.movable_by_window_background { + window.setMovableByWindowBackground(true); + } + + if !attrs.enabled_buttons.contains(WindowButtons::MAXIMIZE) { + if let Some(button) = window.standardWindowButton(NSWindowButton::NSWindowZoomButton) { + button.setEnabled(false); + } + } + + if !attrs.platform_specific.has_shadow { + window.setHasShadow(false); + } + if attrs.position.is_none() { + window.center(); + } + + let view = WinitView::new( + app_delegate, + &window, + attrs.platform_specific.accepts_first_mouse, + attrs.platform_specific.option_as_alt, + ); + + // The default value of `setWantsBestResolutionOpenGLSurface:` was `false` until + // macos 10.14 and `true` after 10.15, we should set it to `YES` or `NO` to avoid + // always the default system value in favour of the user's code + #[allow(deprecated)] + view.setWantsBestResolutionOpenGLSurface(!attrs.platform_specific.disallow_hidpi); + + // 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 unsafe { NSAppKitVersionNumber }.floor() > NSAppKitVersionNumber10_12 { + view.setWantsLayer(true); + } + + // Configure the new view as the "key view" for the window + window.setContentView(Some(&view)); + window.setInitialFirstResponder(Some(&view)); + + if attrs.transparent { + window.setOpaque(false); + // See `set_transparent` for details on why we do this. + window.setBackgroundColor(unsafe { Some(&NSColor::clearColor()) }); + } + + // register for drag and drop operations. + window + .registerForDraggedTypes(&NSArray::from_id_slice(&[ + unsafe { NSFilenamesPboardType }.copy() + ])); + + Some(window) + }) +} + +impl WindowDelegate { + pub(super) fn new( + app_delegate: &ApplicationDelegate, + attrs: WindowAttributes, + mtm: MainThreadMarker, + ) -> Result, RootOsError> { + let window = new_window(app_delegate, &attrs, mtm) + .ok_or_else(|| os_error!(OsError::CreationError("couldn't create `NSWindow`")))?; + + #[cfg(feature = "rwh_06")] + match attrs.parent_window.map(|handle| handle.0) { + Some(rwh_06::RawWindowHandle::AppKit(handle)) => { + // SAFETY: Caller ensures the pointer is valid or NULL + // Unwrap is fine, since the pointer comes from `NonNull`. + let parent_view: Retained = + unsafe { Retained::retain(handle.ns_view.as_ptr().cast()) }.unwrap(); + let parent = parent_view.window().ok_or_else(|| { + os_error!(OsError::CreationError("parent view should be installed in a window")) + })?; + + // SAFETY: We know that there are no parent -> child -> parent cycles since the only + // place in `winit` where we allow making a window a child window is + // right here, just after it's been created. + unsafe { + parent.addChildWindow_ordered(&window, NSWindowOrderingMode::NSWindowAbove) + }; + }, + Some(raw) => panic!("invalid raw window handle {raw:?} on macOS"), + None => (), + } + + let resize_increments = + match attrs.resize_increments.map(|i| i.to_logical(window.backingScaleFactor() as _)) { + Some(LogicalSize { width, height }) if width >= 1. && height >= 1. => { + NSSize::new(width, height) + }, + _ => NSSize::new(1., 1.), + }; + + let scale_factor = window.backingScaleFactor() as _; + + if let Some(appearance) = theme_to_appearance(attrs.preferred_theme) { + unsafe { window.setAppearance(Some(&appearance)) }; + } + + let delegate = mtm.alloc().set_ivars(State { + app_delegate: app_delegate.retain(), + window: window.retain(), + previous_position: Cell::new(flip_window_screen_coordinates(window.frame())), + previous_scale_factor: Cell::new(scale_factor), + resize_increments: Cell::new(resize_increments), + decorations: Cell::new(attrs.decorations), + resizable: Cell::new(attrs.resizable), + maximized: Cell::new(attrs.maximized), + save_presentation_opts: Cell::new(None), + initial_fullscreen: Cell::new(attrs.fullscreen.is_some()), + fullscreen: RefCell::new(None), + target_fullscreen: RefCell::new(None), + in_fullscreen_transition: Cell::new(false), + standard_frame: Cell::new(None), + is_simple_fullscreen: Cell::new(false), + saved_style: Cell::new(None), + is_borderless_game: Cell::new(attrs.platform_specific.borderless_game), + }); + let delegate: Retained = unsafe { msg_send_id![super(delegate), init] }; + + if scale_factor != 1.0 { + let delegate = delegate.clone(); + RunLoop::main(mtm).queue_closure(move || { + delegate.handle_scale_factor_changed(scale_factor); + }); + } + window.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + + // Listen for theme change event. + // + // SAFETY: The observer is un-registered in the `Drop` of the delegate. + unsafe { + window.addObserver_forKeyPath_options_context( + &delegate, + ns_string!("effectiveAppearance"), + NSKeyValueObservingOptions::NSKeyValueObservingOptionNew + | NSKeyValueObservingOptions::NSKeyValueObservingOptionOld, + ptr::null_mut(), + ) + }; + + if attrs.blur { + delegate.set_blur(attrs.blur); + } + + if let Some(dim) = attrs.min_inner_size { + delegate.set_min_inner_size(Some(dim)); + } + if let Some(dim) = attrs.max_inner_size { + delegate.set_max_inner_size(Some(dim)); + } + + delegate.set_window_level(attrs.window_level); + + delegate.set_cursor(attrs.cursor); + + // XXX Send `Focused(false)` right after creating the window delegate, so we won't + // obscure the real focused events on the startup. + delegate.queue_event(WindowEvent::Focused(false)); + + // Set fullscreen mode after we setup everything + delegate.set_fullscreen(attrs.fullscreen.map(Into::into)); + + // 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 attrs.visible { + if attrs.active { + // Tightly linked with `app_state::window_activation_hack` + window.makeKeyAndOrderFront(None); + } else { + window.orderFront(None); + } + } + + if attrs.maximized { + delegate.set_maximized(attrs.maximized); + } + + Ok(delegate) + } + + #[track_caller] + pub(super) fn view(&self) -> Retained { + // SAFETY: The view inside WinitWindow is always `WinitView` + unsafe { Retained::cast(self.window().contentView().unwrap()) } + } + + #[track_caller] + pub(super) fn window(&self) -> &WinitWindow { + &self.ivars().window + } + + #[track_caller] + pub(crate) fn id(&self) -> WindowId { + self.window().id() + } + + pub(crate) fn queue_event(&self, event: WindowEvent) { + self.ivars().app_delegate.maybe_queue_window_event(self.window().id(), event); + } + + fn handle_scale_factor_changed(&self, scale_factor: CGFloat) { + let app_delegate = &self.ivars().app_delegate; + let window = self.window(); + + let content_size = window.contentRectForFrameRect(window.frame()).size; + let content_size = LogicalSize::new(content_size.width, content_size.height); + + let suggested_size = content_size.to_physical(scale_factor); + let new_inner_size = Arc::new(Mutex::new(suggested_size)); + app_delegate.handle_window_event(window.id(), WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)), + }); + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + + if physical_size != suggested_size { + let logical_size = physical_size.to_logical(scale_factor); + let size = NSSize::new(logical_size.width, logical_size.height); + window.setContentSize(size); + } + app_delegate.handle_window_event(window.id(), WindowEvent::Resized(physical_size)); + } + + fn emit_move_event(&self) { + let position = flip_window_screen_coordinates(self.window().frame()); + if self.ivars().previous_position.get() == position { + return; + } + self.ivars().previous_position.set(position); + + let position = + LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor()); + self.queue_event(WindowEvent::Moved(position)); + } + + fn set_style_mask(&self, mask: NSWindowStyleMask) { + self.window().setStyleMask(mask); + // If we don't do this, key handling will break + // (at least until the window is clicked again/etc.) + let _ = self.window().makeFirstResponder(Some(&self.view())); + } + + pub fn set_title(&self, title: &str) { + self.window().setTitle(&NSString::from_str(title)) + } + + pub fn set_transparent(&self, transparent: bool) { + // This is just a hint for Quartz, it doesn't actually speculate with window alpha. + // Providing a wrong value here could result in visual artifacts, when the window is + // transparent. + self.window().setOpaque(!transparent); + + // AppKit draws the window with a background color by default, which is usually really + // nice, but gets in the way when we want to allow the contents of the window to be + // transparent, as in that case, the transparent contents will just be drawn on top of + // the background color. As such, to allow the window to be transparent, we must also set + // the background color to one with an empty alpha channel. + let color = if transparent { + unsafe { NSColor::clearColor() } + } else { + unsafe { NSColor::windowBackgroundColor() } + }; + + self.window().setBackgroundColor(Some(&color)); + } + + pub fn set_blur(&self, _blur: bool) { + // IRIS PATCH (App Store, guideline 2.5.1): upstream winit calls the + // private SkyLight API `CGSSetWindowBackgroundBlurRadius` here. Apple's + // static binary scan rejects that symbol from the Mac App Store, and + // iris-gui never requests window blur (ViewportBuilder leaves blur = + // false), so this is a no-op. Removing the call also drops the + // `_CGSSetWindowBackgroundBlurRadius` / `_CGSMainConnectionID` imports + // from the linked binary. See rules/macos/appstore-private-api.md. + } + + pub fn set_visible(&self, visible: bool) { + match visible { + true => self.window().makeKeyAndOrderFront(None), + false => self.window().orderOut(None), + } + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(self.window().isVisible()) + } + + pub fn request_redraw(&self) { + self.ivars().app_delegate.queue_redraw(self.window().id()); + } + + #[inline] + pub fn pre_present_notify(&self) {} + + pub fn outer_position(&self) -> Result, NotSupportedError> { + let position = flip_window_screen_coordinates(self.window().frame()); + Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) + } + + pub fn inner_position(&self) -> Result, NotSupportedError> { + let content_rect = self.window().contentRectForFrameRect(self.window().frame()); + let position = flip_window_screen_coordinates(content_rect); + Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) + } + + pub fn set_outer_position(&self, position: Position) { + let position = position.to_logical(self.scale_factor()); + let point = flip_window_screen_coordinates(NSRect::new( + NSPoint::new(position.x, position.y), + self.window().frame().size, + )); + unsafe { self.window().setFrameOrigin(point) }; + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let content_rect = self.window().contentRectForFrameRect(self.window().frame()); + let logical = LogicalSize::new(content_rect.size.width, content_rect.size.height); + logical.to_physical(self.scale_factor()) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let frame = self.window().frame(); + let logical = LogicalSize::new(frame.size.width, frame.size.height); + logical.to_physical(self.scale_factor()) + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let scale_factor = self.scale_factor(); + let size = size.to_logical(scale_factor); + self.window().setContentSize(NSSize::new(size.width, size.height)); + None + } + + pub fn set_min_inner_size(&self, dimensions: Option) { + let dimensions = + dimensions.unwrap_or(Size::Logical(LogicalSize { width: 0.0, height: 0.0 })); + let min_size = dimensions.to_logical::(self.scale_factor()); + + let min_size = NSSize::new(min_size.width, min_size.height); + unsafe { self.window().setContentMinSize(min_size) }; + + // If necessary, resize the window to match constraint + let mut current_size = self.window().contentRectForFrameRect(self.window().frame()).size; + if current_size.width < min_size.width { + current_size.width = min_size.width; + } + if current_size.height < min_size.height { + current_size.height = min_size.height; + } + self.window().setContentSize(current_size); + } + + pub fn set_max_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.unwrap_or(Size::Logical(LogicalSize { + width: f32::MAX as f64, + height: f32::MAX as f64, + })); + let scale_factor = self.scale_factor(); + let max_size = dimensions.to_logical::(scale_factor); + + let max_size = NSSize::new(max_size.width, max_size.height); + unsafe { self.window().setContentMaxSize(max_size) }; + + // If necessary, resize the window to match constraint + let mut current_size = self.window().contentRectForFrameRect(self.window().frame()).size; + if max_size.width < current_size.width { + current_size.width = max_size.width; + } + if max_size.height < current_size.height { + current_size.height = max_size.height; + } + self.window().setContentSize(current_size); + } + + pub fn resize_increments(&self) -> Option> { + let increments = self.ivars().resize_increments.get(); + let (w, h) = (increments.width, increments.height); + if w > 1.0 || h > 1.0 { + Some(LogicalSize::new(w, h).to_physical(self.scale_factor())) + } else { + None + } + } + + pub fn set_resize_increments(&self, increments: Option) { + // XXX the resize increments are only used during live resizes. + self.ivars().resize_increments.set( + increments + .map(|increments| { + let logical = increments.to_logical::(self.scale_factor()); + NSSize::new(logical.width.max(1.0), logical.height.max(1.0)) + }) + .unwrap_or_else(|| NSSize::new(1.0, 1.0)), + ); + } + + pub(crate) fn set_resize_increments_inner(&self, size: NSSize) { + // It was concluded (#2411) that there is never a use-case for + // "outer" resize increments, hence we set "inner" ones here. + // ("outer" in macOS being just resizeIncrements, and "inner" - contentResizeIncrements) + // This is consistent with X11 size hints behavior + self.window().setContentResizeIncrements(size); + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + self.ivars().resizable.set(resizable); + let fullscreen = self.ivars().fullscreen.borrow().is_some(); + if !fullscreen { + let mut mask = self.window().styleMask(); + if resizable { + mask |= NSWindowStyleMask::Resizable; + } else { + mask &= !NSWindowStyleMask::Resizable; + } + self.set_style_mask(mask); + } + // Otherwise, we don't change the mask until we exit fullscreen. + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.window().isResizable() + } + + #[inline] + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + let mut mask = self.window().styleMask(); + + if buttons.contains(WindowButtons::CLOSE) { + mask |= NSWindowStyleMask::Closable; + } else { + mask &= !NSWindowStyleMask::Closable; + } + + if buttons.contains(WindowButtons::MINIMIZE) { + mask |= NSWindowStyleMask::Miniaturizable; + } else { + mask &= !NSWindowStyleMask::Miniaturizable; + } + + // This must happen before the button's "enabled" status has been set, + // hence we do it synchronously. + self.set_style_mask(mask); + + // We edit the button directly instead of using `NSResizableWindowMask`, + // since that mask also affect the resizability of the window (which is + // controllable by other means in `winit`). + if let Some(button) = self.window().standardWindowButton(NSWindowButton::NSWindowZoomButton) + { + button.setEnabled(buttons.contains(WindowButtons::MAXIMIZE)); + } + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + let mut buttons = WindowButtons::empty(); + if self.window().isMiniaturizable() { + buttons |= WindowButtons::MINIMIZE; + } + if self + .window() + .standardWindowButton(NSWindowButton::NSWindowZoomButton) + .map(|b| b.isEnabled()) + .unwrap_or(true) + { + buttons |= WindowButtons::MAXIMIZE; + } + if self.window().hasCloseBox() { + buttons |= WindowButtons::CLOSE; + } + buttons + } + + pub fn set_cursor(&self, cursor: Cursor) { + let view = self.view(); + + let cursor = match cursor { + Cursor::Icon(icon) => cursor_from_icon(icon), + Cursor::Custom(cursor) => cursor.inner.0, + }; + + if view.cursor_icon() == cursor { + return; + } + + view.set_cursor_icon(cursor); + self.window().invalidateCursorRectsForView(&view); + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let associate_mouse_cursor = match mode { + CursorGrabMode::Locked => false, + CursorGrabMode::None => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + }, + }; + + // TODO: Do this for real https://stackoverflow.com/a/40922095/5435443 + CGDisplay::associate_mouse_and_mouse_cursor_position(associate_mouse_cursor) + .map_err(|status| ExternalError::Os(os_error!(OsError::CGError(status)))) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let view = self.view(); + let state_changed = view.set_cursor_visible(visible); + if state_changed { + self.window().invalidateCursorRectsForView(&view); + } + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.window().backingScaleFactor() 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 drag_window(&self) -> Result<(), ExternalError> { + let mtm = MainThreadMarker::from(self); + let event = + NSApplication::sharedApplication(mtm).currentEvent().ok_or(ExternalError::Ignored)?; + self.window().performWindowDragWithEvent(&event); + Ok(()) + } + + #[inline] + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + self.window().setIgnoresMouseEvents(!hittest); + Ok(()) + } + + pub(crate) fn is_zoomed(&self) -> bool { + // because `isZoomed` doesn't work if the window's borderless, + // we make it resizable temporarily. + let curr_mask = self.window().styleMask(); + + let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable; + let needs_temp_mask = !curr_mask.contains(required); + if needs_temp_mask { + self.set_style_mask(required); + } + + let is_zoomed = self.window().isZoomed(); + + // Roll back temp styles + if needs_temp_mask { + self.set_style_mask(curr_mask); + } + + is_zoomed + } + + fn saved_style(&self) -> NSWindowStyleMask { + let base_mask = + self.ivars().saved_style.take().unwrap_or_else(|| self.window().styleMask()); + if self.ivars().resizable.get() { + 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) { + self.ivars().fullscreen.replace(None); + + let maximized = self.ivars().maximized.get(); + let mask = self.saved_style(); + + self.set_style_mask(mask); + self.set_maximized(maximized); + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let is_minimized = self.window().isMiniaturized(); + if is_minimized == minimized { + return; + } + + if minimized { + self.window().miniaturize(Some(self)); + } else { + unsafe { self.window().deminiaturize(Some(self)) }; + } + } + + #[inline] + pub fn is_minimized(&self) -> Option { + Some(self.window().isMiniaturized()) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let mtm = MainThreadMarker::from(self); + let is_zoomed = self.is_zoomed(); + if is_zoomed == maximized { + return; + }; + + // Save the standard frame sized if it is not zoomed + if !is_zoomed { + self.ivars().standard_frame.set(Some(self.window().frame())); + } + + self.ivars().maximized.set(maximized); + + if self.ivars().fullscreen.borrow().is_some() { + // Handle it in window_did_exit_fullscreen + return; + } + + if self.window().styleMask().contains(NSWindowStyleMask::Resizable) { + // Just use the native zoom if resizable + self.window().zoom(None); + } else { + // if it's not resizable, we set the frame directly + let new_rect = if maximized { + let screen = NSScreen::mainScreen(mtm).expect("no screen found"); + screen.visibleFrame() + } else { + self.ivars().standard_frame.get().unwrap_or(DEFAULT_STANDARD_FRAME) + }; + self.window().setFrame_display(new_rect, false); + } + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + self.ivars().fullscreen.borrow().clone() + } + + #[inline] + pub fn is_maximized(&self) -> bool { + self.is_zoomed() + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + + if self.ivars().is_simple_fullscreen.get() { + return; + } + if self.ivars().in_fullscreen_transition.get() { + // We can't set fullscreen here. + // Set fullscreen after transition. + self.ivars().target_fullscreen.replace(Some(fullscreen)); + return; + } + let old_fullscreen = self.ivars().fullscreen.borrow().clone(); + if fullscreen == old_fullscreen { + return; + } + + // 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(monitor)) => monitor.clone(), + Fullscreen::Borderless(None) => { + if let Some(monitor) = self.current_monitor_inner() { + monitor + } else { + return; + } + }, + Fullscreen::Exclusive(video_mode) => video_mode.monitor(), + } + .ns_screen(mtm) + .unwrap(); + + let old_screen = self.window().screen().unwrap(); + if old_screen != new_screen { + unsafe { self.window().setFrameOrigin(new_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().native_identifier(); + + let mut fade_token = ffi::kCGDisplayFadeReservationInvalidToken; + + if matches!(old_fullscreen, Some(Fullscreen::Borderless(_))) { + self.ivars().save_presentation_opts.replace(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.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); + } + } + } + + self.ivars().fullscreen.replace(fullscreen.clone()); + + fn toggle_fullscreen(window: &WinitWindow) { + // Window level must be restored from `CGShieldingWindowLevel() + // + 1` back to normal in order for `toggleFullScreen` to do + // anything + window.setLevel(ffi::kCGNormalWindowLevel as NSWindowLevel); + window.toggleFullScreen(None); + } + + match (old_fullscreen, fullscreen) { + (None, Some(fullscreen)) => { + // `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`. + let curr_mask = self.window().styleMask(); + let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable; + if !curr_mask.contains(required) { + self.set_style_mask(required); + self.ivars().saved_style.set(Some(curr_mask)); + } + + // In borderless games, we want to disable the dock and menu bar + // by setting the presentation options. We do this here rather than in + // `window:willUseFullScreenPresentationOptions` because for some reason + // the menu bar remains interactable despite being hidden. + if self.is_borderless_game() && matches!(fullscreen, Fullscreen::Borderless(_)) { + let presentation_options = NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar; + app.setPresentationOptions(presentation_options); + } + + toggle_fullscreen(self.window()); + }, + (Some(Fullscreen::Borderless(_)), None) => { + // State is restored by `window_did_exit_fullscreen` + toggle_fullscreen(self.window()); + }, + (Some(Fullscreen::Exclusive(ref video_mode)), None) => { + restore_and_release_display(&video_mode.monitor()); + toggle_fullscreen(self.window()); + }, + (Some(Fullscreen::Borderless(_)), Some(Fullscreen::Exclusive(_))) => { + // 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:`. + self.ivars().save_presentation_opts.set(Some(app.presentationOptions())); + + let presentation_options = + NSApplicationPresentationOptions::NSApplicationPresentationFullScreen + | NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar; + app.setPresentationOptions(presentation_options); + + let window_level = unsafe { ffi::CGShieldingWindowLevel() } as NSWindowLevel + 1; + self.window().setLevel(window_level); + }, + (Some(Fullscreen::Exclusive(ref video_mode)), Some(Fullscreen::Borderless(_))) => { + let presentation_options = self.ivars().save_presentation_opts.get().unwrap_or( + NSApplicationPresentationOptions::NSApplicationPresentationFullScreen + | NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar + ); + app.setPresentationOptions(presentation_options); + + restore_and_release_display(&video_mode.monitor()); + + // Restore the normal window level following the Borderless fullscreen + // `CGShieldingWindowLevel() + 1` hack. + self.window().setLevel(ffi::kCGNormalWindowLevel as NSWindowLevel); + }, + _ => {}, + }; + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + if decorations == self.ivars().decorations.get() { + return; + } + + self.ivars().decorations.set(decorations); + + let fullscreen = self.ivars().fullscreen.borrow().is_some(); + let resizable = self.ivars().resizable.get(); + + // 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(new_mask); + } + + #[inline] + pub fn is_decorated(&self) -> bool { + self.ivars().decorations.get() + } + + #[inline] + pub fn set_window_level(&self, level: WindowLevel) { + let level = match level { + WindowLevel::AlwaysOnTop => ffi::kCGFloatingWindowLevel as NSWindowLevel, + WindowLevel::AlwaysOnBottom => (ffi::kCGNormalWindowLevel - 1) as NSWindowLevel, + WindowLevel::Normal => ffi::kCGNormalWindowLevel as NSWindowLevel, + }; + self.window().setLevel(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 + // `WindowAttributesExt::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_cursor_area(&self, spot: Position, size: Size) { + let scale_factor = self.scale_factor(); + let logical_spot = spot.to_logical(scale_factor); + let logical_spot = NSPoint::new(logical_spot.x, logical_spot.y); + + let size = size.to_logical(scale_factor); + let size = NSSize::new(size.width, size.height); + + self.view().set_ime_cursor_area(logical_spot, size); + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + self.view().set_ime_allowed(allowed); + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn focus_window(&self) { + let mtm = MainThreadMarker::from(self); + let is_minimized = self.window().isMiniaturized(); + let is_visible = self.window().isVisible(); + + if !is_minimized && is_visible { + #[allow(deprecated)] + NSApplication::sharedApplication(mtm).activateIgnoringOtherApps(true); + self.window().makeKeyAndOrderFront(None); + } + } + + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let mtm = MainThreadMarker::from(self); + let ns_request_type = request_type.map(|ty| match ty { + UserAttentionType::Critical => NSRequestUserAttentionType::NSCriticalRequest, + UserAttentionType::Informational => NSRequestUserAttentionType::NSInformationalRequest, + }); + if let Some(ty) = ns_request_type { + NSApplication::sharedApplication(mtm).requestUserAttention(ty); + } + } + + #[inline] + // Allow directly accessing the current monitor internally without unwrapping. + pub(crate) fn current_monitor_inner(&self) -> Option { + let display_id = get_display_id(&*self.window().screen()?); + if let Some(monitor) = MonitorHandle::new(display_id) { + Some(monitor) + } else { + // NOTE: Display ID was just fetched from live NSScreen, but can still result in `None` + // with certain Thunderbolt docked monitors. + warn!(display_id, "got screen with invalid display ID"); + None + } + } + + #[inline] + pub fn current_monitor(&self) -> Option { + self.current_monitor_inner() + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(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.window() as *const WinitWindow as *mut _; + window_handle.ns_view = Retained::as_ptr(&self.view()) as *mut _; + 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.window() as *const WinitWindow as *mut _; + window_handle.ns_view = Retained::as_ptr(&self.view()) as *mut _; + 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) -> rwh_06::RawWindowHandle { + let window_handle = rwh_06::AppKitWindowHandle::new({ + let ptr = Retained::as_ptr(&self.view()) as *mut _; + std::ptr::NonNull::new(ptr).expect("Retained should never be null") + }); + rwh_06::RawWindowHandle::AppKit(window_handle) + } + + fn toggle_style_mask(&self, mask: NSWindowStyleMask, on: bool) { + let current_style_mask = self.window().styleMask(); + if on { + self.set_style_mask(current_style_mask | mask); + } else { + self.set_style_mask(current_style_mask & !mask); + } + } + + #[inline] + pub fn has_focus(&self) -> bool { + self.window().isKeyWindow() + } + + pub fn theme(&self) -> Option { + unsafe { self.window().appearance() } + .map(|appearance| appearance_to_theme(&appearance)) + .or_else(|| { + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + + if app.respondsToSelector(sel!(effectiveAppearance)) { + Some(super::window_delegate::appearance_to_theme(&app.effectiveAppearance())) + } else { + Some(Theme::Light) + } + }) + } + + pub fn set_theme(&self, theme: Option) { + unsafe { self.window().setAppearance(theme_to_appearance(theme).as_deref()) }; + } + + #[inline] + pub fn set_content_protected(&self, protected: bool) { + self.window().setSharingType(if protected { + NSWindowSharingType::NSWindowSharingNone + } else { + NSWindowSharingType::NSWindowSharingReadOnly + }) + } + + pub fn title(&self) -> String { + self.window().title().to_string() + } + + pub fn reset_dead_keys(&self) { + // (Artur) I couldn't find a way to implement this. + } +} + +fn restore_and_release_display(monitor: &MonitorHandle) { + let available_monitors = monitor::available_monitors(); + if available_monitors.contains(monitor) { + unsafe { + ffi::CGRestorePermanentDisplayConfiguration(); + assert_eq!(ffi::CGDisplayRelease(monitor.native_identifier()), ffi::kCGErrorSuccess); + }; + } else { + warn!( + monitor = monitor.name(), + "Tried to restore exclusive fullscreen on a monitor that is no longer available" + ); + } +} + +impl WindowExtMacOS for WindowDelegate { + #[inline] + fn simple_fullscreen(&self) -> bool { + self.ivars().is_simple_fullscreen.get() + } + + #[inline] + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { + let mtm = MainThreadMarker::from(self); + + let app = NSApplication::sharedApplication(mtm); + let is_native_fullscreen = self.ivars().fullscreen.borrow().is_some(); + let is_simple_fullscreen = self.ivars().is_simple_fullscreen.get(); + + // 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 + self.ivars() + .standard_frame + .set(Some(self.window().contentRectForFrameRect(self.window().frame()))); + self.ivars().saved_style.set(Some(self.window().styleMask())); + self.ivars().save_presentation_opts.set(Some(app.presentationOptions())); + + // Tell our window's state that we're in fullscreen + self.ivars().is_simple_fullscreen.set(true); + + // Simulate pre-Lion fullscreen by hiding the dock and menu bar + let presentation_options = if self.is_borderless_game() { + NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar + } else { + NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar + }; + app.setPresentationOptions(presentation_options); + + // Hide the titlebar + self.toggle_style_mask(NSWindowStyleMask::Titled, false); + + // Set the window frame to the screen frame size + let screen = self.window().screen().expect("expected screen to be available"); + self.window().setFrame_display(screen.frame(), true); + + // Fullscreen windows can't be resized, minimized, or moved + self.toggle_style_mask(NSWindowStyleMask::Miniaturizable, false); + self.toggle_style_mask(NSWindowStyleMask::Resizable, false); + self.window().setMovable(false); + } else { + let new_mask = self.saved_style(); + self.ivars().is_simple_fullscreen.set(false); + + let save_presentation_opts = self.ivars().save_presentation_opts.get(); + let frame = self.ivars().standard_frame.get().unwrap_or(DEFAULT_STANDARD_FRAME); + + if let Some(presentation_opts) = save_presentation_opts { + app.setPresentationOptions(presentation_opts); + } + + self.window().setFrame_display(frame, true); + self.window().setMovable(true); + self.set_style_mask(new_mask); + } + + true + } + + #[inline] + fn has_shadow(&self) -> bool { + self.window().hasShadow() + } + + #[inline] + fn set_has_shadow(&self, has_shadow: bool) { + self.window().setHasShadow(has_shadow) + } + + #[inline] + fn set_tabbing_identifier(&self, identifier: &str) { + self.window().setTabbingIdentifier(&NSString::from_str(identifier)) + } + + #[inline] + fn tabbing_identifier(&self) -> String { + self.window().tabbingIdentifier().to_string() + } + + #[inline] + fn select_next_tab(&self) { + self.window().selectNextTab(None) + } + + #[inline] + fn select_previous_tab(&self) { + unsafe { self.window().selectPreviousTab(None) } + } + + #[inline] + fn select_tab_at_index(&self, index: usize) { + if let Some(group) = self.window().tabGroup() { + if let Some(windows) = unsafe { self.window().tabbedWindows() } { + if index < windows.len() { + group.setSelectedWindow(Some(&windows[index])); + } + } + } + } + + #[inline] + fn num_tabs(&self) -> usize { + unsafe { self.window().tabbedWindows() }.map(|windows| windows.len()).unwrap_or(1) + } + + fn is_document_edited(&self) -> bool { + self.window().isDocumentEdited() + } + + fn set_document_edited(&self, edited: bool) { + self.window().setDocumentEdited(edited) + } + + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) { + self.view().set_option_as_alt(option_as_alt); + } + + fn option_as_alt(&self) -> OptionAsAlt { + self.view().option_as_alt() + } + + fn set_borderless_game(&self, borderless_game: bool) { + self.ivars().is_borderless_game.set(borderless_game); + } + + fn is_borderless_game(&self) -> bool { + self.ivars().is_borderless_game.get() + } +} + +const DEFAULT_STANDARD_FRAME: NSRect = + NSRect::new(NSPoint::new(50.0, 50.0), NSSize::new(800.0, 600.0)); + +fn dark_appearance_name() -> &'static NSString { + // Don't use the static `NSAppearanceNameDarkAqua` to allow linking on macOS < 10.14 + ns_string!("NSAppearanceNameDarkAqua") +} + +pub fn appearance_to_theme(appearance: &NSAppearance) -> Theme { + let best_match = appearance.bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[ + unsafe { NSAppearanceNameAqua.copy() }, + dark_appearance_name().copy(), + ])); + if let Some(best_match) = best_match { + if *best_match == *dark_appearance_name() { + Theme::Dark + } else { + Theme::Light + } + } else { + warn!(?appearance, "failed to determine the theme of the appearance"); + // Default to light in this case + Theme::Light + } +} + +fn theme_to_appearance(theme: Option) -> Option> { + let appearance = match theme? { + Theme::Light => unsafe { NSAppearance::appearanceNamed(NSAppearanceNameAqua) }, + Theme::Dark => NSAppearance::appearanceNamed(dark_appearance_name()), + }; + if let Some(appearance) = appearance { + Some(appearance) + } else { + warn!(?theme, "could not find appearance for theme"); + // Assume system appearance in this case + None + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/mod.rs b/third_party/winit-0.30.13/src/platform_impl/mod.rs new file mode 100644 index 0000000..3bfce68 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/mod.rs @@ -0,0 +1,78 @@ +use crate::monitor::{MonitorHandle as RootMonitorHandle, VideoModeHandle as RootVideoModeHandle}; +use crate::window::Fullscreen as RootFullscreen; + +#[cfg(android_platform)] +mod android; +#[cfg(ios_platform)] +mod ios; +#[cfg(any(x11_platform, wayland_platform))] +mod linux; +#[cfg(macos_platform)] +mod macos; +#[cfg(orbital_platform)] +mod orbital; +#[cfg(web_platform)] +mod web; +#[cfg(windows_platform)] +mod windows; + +#[cfg(android_platform)] +use android as platform; +#[cfg(ios_platform)] +use ios as platform; +#[cfg(any(x11_platform, wayland_platform))] +use linux as platform; +#[cfg(macos_platform)] +use macos as platform; +#[cfg(orbital_platform)] +use orbital as platform; +#[cfg(web_platform)] +use web as platform; +#[cfg(windows_platform)] +use windows as platform; + +pub use self::platform::*; + +/// Helper for converting between platform-specific and generic +/// [`VideoModeHandle`]/[`MonitorHandle`] +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum Fullscreen { + Exclusive(VideoModeHandle), + Borderless(Option), +} + +impl From for Fullscreen { + fn from(f: RootFullscreen) -> Self { + match f { + RootFullscreen::Exclusive(mode) => Self::Exclusive(mode.video_mode), + RootFullscreen::Borderless(Some(handle)) => Self::Borderless(Some(handle.inner)), + RootFullscreen::Borderless(None) => Self::Borderless(None), + } + } +} + +impl From for RootFullscreen { + fn from(f: Fullscreen) -> Self { + match f { + Fullscreen::Exclusive(video_mode) => { + Self::Exclusive(RootVideoModeHandle { video_mode }) + }, + Fullscreen::Borderless(Some(inner)) => { + Self::Borderless(Some(RootMonitorHandle { inner })) + }, + Fullscreen::Borderless(None) => Self::Borderless(None), + } + } +} + +#[cfg(all( + not(ios_platform), + not(windows_platform), + not(macos_platform), + not(android_platform), + not(x11_platform), + not(wayland_platform), + not(web_platform), + not(orbital_platform), +))] +compile_error!("The platform you're compiling for is not supported by winit"); diff --git a/third_party/winit-0.30.13/src/platform_impl/orbital/event_loop.rs b/third_party/winit-0.30.13/src/platform_impl/orbital/event_loop.rs new file mode 100644 index 0000000..91f76e7 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/orbital/event_loop.rs @@ -0,0 +1,829 @@ +use std::cell::Cell; +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::Instant; +use std::{mem, slice}; + +use bitflags::bitflags; +use orbclient::{ + ButtonEvent, EventOption, FocusEvent, HoverEvent, KeyEvent, MouseEvent, MouseRelativeEvent, + MoveEvent, QuitEvent, ResizeEvent, ScrollEvent, TextInputEvent, +}; +use smol_str::SmolStr; + +use crate::error::EventLoopError; +use crate::event::{self, Ime, Modifiers, StartCause}; +use crate::event_loop::{self, ControlFlow, DeviceEvents}; +use crate::keyboard::{ + Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NamedKey, NativeKey, NativeKeyCode, + PhysicalKey, +}; +use crate::window::{ + CustomCursor as RootCustomCursor, CustomCursorSource, Theme, WindowId as RootWindowId, +}; + +use super::{ + DeviceId, KeyEventExtra, MonitorHandle, OsError, PlatformSpecificEventLoopAttributes, + RedoxSocket, TimeSocket, WindowId, WindowProperties, +}; + +fn convert_scancode(scancode: u8) -> (PhysicalKey, Option) { + // Key constants from https://docs.rs/orbclient/latest/orbclient/event/index.html + let (key_code, named_key_opt) = match scancode { + orbclient::K_A => (KeyCode::KeyA, None), + orbclient::K_B => (KeyCode::KeyB, None), + orbclient::K_C => (KeyCode::KeyC, None), + orbclient::K_D => (KeyCode::KeyD, None), + orbclient::K_E => (KeyCode::KeyE, None), + orbclient::K_F => (KeyCode::KeyF, None), + orbclient::K_G => (KeyCode::KeyG, None), + orbclient::K_H => (KeyCode::KeyH, None), + orbclient::K_I => (KeyCode::KeyI, None), + orbclient::K_J => (KeyCode::KeyJ, None), + orbclient::K_K => (KeyCode::KeyK, None), + orbclient::K_L => (KeyCode::KeyL, None), + orbclient::K_M => (KeyCode::KeyM, None), + orbclient::K_N => (KeyCode::KeyN, None), + orbclient::K_O => (KeyCode::KeyO, None), + orbclient::K_P => (KeyCode::KeyP, None), + orbclient::K_Q => (KeyCode::KeyQ, None), + orbclient::K_R => (KeyCode::KeyR, None), + orbclient::K_S => (KeyCode::KeyS, None), + orbclient::K_T => (KeyCode::KeyT, None), + orbclient::K_U => (KeyCode::KeyU, None), + orbclient::K_V => (KeyCode::KeyV, None), + orbclient::K_W => (KeyCode::KeyW, None), + orbclient::K_X => (KeyCode::KeyX, None), + orbclient::K_Y => (KeyCode::KeyY, None), + orbclient::K_Z => (KeyCode::KeyZ, None), + orbclient::K_0 => (KeyCode::Digit0, None), + orbclient::K_1 => (KeyCode::Digit1, None), + orbclient::K_2 => (KeyCode::Digit2, None), + orbclient::K_3 => (KeyCode::Digit3, None), + orbclient::K_4 => (KeyCode::Digit4, None), + orbclient::K_5 => (KeyCode::Digit5, None), + orbclient::K_6 => (KeyCode::Digit6, None), + orbclient::K_7 => (KeyCode::Digit7, None), + orbclient::K_8 => (KeyCode::Digit8, None), + orbclient::K_9 => (KeyCode::Digit9, None), + + orbclient::K_ALT => (KeyCode::AltLeft, Some(NamedKey::Alt)), + orbclient::K_ALT_GR => (KeyCode::AltRight, Some(NamedKey::AltGraph)), + orbclient::K_BACKSLASH => (KeyCode::Backslash, None), + orbclient::K_BKSP => (KeyCode::Backspace, Some(NamedKey::Backspace)), + orbclient::K_BRACE_CLOSE => (KeyCode::BracketRight, None), + orbclient::K_BRACE_OPEN => (KeyCode::BracketLeft, None), + orbclient::K_CAPS => (KeyCode::CapsLock, Some(NamedKey::CapsLock)), + orbclient::K_COMMA => (KeyCode::Comma, None), + orbclient::K_CTRL => (KeyCode::ControlLeft, Some(NamedKey::Control)), + orbclient::K_DEL => (KeyCode::Delete, Some(NamedKey::Delete)), + orbclient::K_DOWN => (KeyCode::ArrowDown, Some(NamedKey::ArrowDown)), + orbclient::K_END => (KeyCode::End, Some(NamedKey::End)), + orbclient::K_ENTER => (KeyCode::Enter, Some(NamedKey::Enter)), + orbclient::K_EQUALS => (KeyCode::Equal, None), + orbclient::K_ESC => (KeyCode::Escape, Some(NamedKey::Escape)), + orbclient::K_F1 => (KeyCode::F1, Some(NamedKey::F1)), + orbclient::K_F2 => (KeyCode::F2, Some(NamedKey::F2)), + orbclient::K_F3 => (KeyCode::F3, Some(NamedKey::F3)), + orbclient::K_F4 => (KeyCode::F4, Some(NamedKey::F4)), + orbclient::K_F5 => (KeyCode::F5, Some(NamedKey::F5)), + orbclient::K_F6 => (KeyCode::F6, Some(NamedKey::F6)), + orbclient::K_F7 => (KeyCode::F7, Some(NamedKey::F7)), + orbclient::K_F8 => (KeyCode::F8, Some(NamedKey::F8)), + orbclient::K_F9 => (KeyCode::F9, Some(NamedKey::F9)), + orbclient::K_F10 => (KeyCode::F10, Some(NamedKey::F10)), + orbclient::K_F11 => (KeyCode::F11, Some(NamedKey::F11)), + orbclient::K_F12 => (KeyCode::F12, Some(NamedKey::F12)), + orbclient::K_HOME => (KeyCode::Home, Some(NamedKey::Home)), + orbclient::K_LEFT => (KeyCode::ArrowLeft, Some(NamedKey::ArrowLeft)), + orbclient::K_LEFT_SHIFT => (KeyCode::ShiftLeft, Some(NamedKey::Shift)), + orbclient::K_MINUS => (KeyCode::Minus, None), + orbclient::K_NUM_0 => (KeyCode::Numpad0, None), + orbclient::K_NUM_1 => (KeyCode::Numpad1, None), + orbclient::K_NUM_2 => (KeyCode::Numpad2, None), + orbclient::K_NUM_3 => (KeyCode::Numpad3, None), + orbclient::K_NUM_4 => (KeyCode::Numpad4, None), + orbclient::K_NUM_5 => (KeyCode::Numpad5, None), + orbclient::K_NUM_6 => (KeyCode::Numpad6, None), + orbclient::K_NUM_7 => (KeyCode::Numpad7, None), + orbclient::K_NUM_8 => (KeyCode::Numpad8, None), + orbclient::K_NUM_9 => (KeyCode::Numpad9, None), + orbclient::K_PERIOD => (KeyCode::Period, None), + orbclient::K_PGDN => (KeyCode::PageDown, Some(NamedKey::PageDown)), + orbclient::K_PGUP => (KeyCode::PageUp, Some(NamedKey::PageUp)), + orbclient::K_QUOTE => (KeyCode::Quote, None), + orbclient::K_RIGHT => (KeyCode::ArrowRight, Some(NamedKey::ArrowRight)), + orbclient::K_RIGHT_SHIFT => (KeyCode::ShiftRight, Some(NamedKey::Shift)), + orbclient::K_SEMICOLON => (KeyCode::Semicolon, None), + orbclient::K_SLASH => (KeyCode::Slash, None), + orbclient::K_SPACE => (KeyCode::Space, Some(NamedKey::Space)), + orbclient::K_SUPER => (KeyCode::SuperLeft, Some(NamedKey::Super)), + orbclient::K_TAB => (KeyCode::Tab, Some(NamedKey::Tab)), + orbclient::K_TICK => (KeyCode::Backquote, None), + orbclient::K_UP => (KeyCode::ArrowUp, Some(NamedKey::ArrowUp)), + orbclient::K_VOLUME_DOWN => (KeyCode::AudioVolumeDown, Some(NamedKey::AudioVolumeDown)), + orbclient::K_VOLUME_TOGGLE => (KeyCode::AudioVolumeMute, Some(NamedKey::AudioVolumeMute)), + orbclient::K_VOLUME_UP => (KeyCode::AudioVolumeUp, Some(NamedKey::AudioVolumeUp)), + + _ => return (PhysicalKey::Unidentified(NativeKeyCode::Unidentified), None), + }; + (PhysicalKey::Code(key_code), named_key_opt) +} + +fn element_state(pressed: bool) -> event::ElementState { + if pressed { + event::ElementState::Pressed + } else { + event::ElementState::Released + } +} + +bitflags! { + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + struct KeyboardModifierState: u8 { + const LSHIFT = 1 << 0; + const RSHIFT = 1 << 1; + const LCTRL = 1 << 2; + const RCTRL = 1 << 3; + const LALT = 1 << 4; + const RALT = 1 << 5; + const LSUPER = 1 << 6; + const RSUPER = 1 << 7; + } +} + +bitflags! { + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + struct MouseButtonState: u8 { + const LEFT = 1 << 0; + const MIDDLE = 1 << 1; + const RIGHT = 1 << 2; + } +} + +#[derive(Default)] +struct EventState { + keyboard: KeyboardModifierState, + mouse: MouseButtonState, + resize_opt: Option<(u32, u32)>, +} + +impl EventState { + fn character_all_modifiers(&self, character: char) -> char { + // Modify character if Ctrl is pressed + #[allow(clippy::collapsible_if)] + if self.keyboard.contains(KeyboardModifierState::LCTRL) + || self.keyboard.contains(KeyboardModifierState::RCTRL) + { + if character.is_ascii_lowercase() { + return ((character as u8 - b'a') + 1) as char; + } + // TODO: more control key variants? + } + + // Return character as-is if no special handling required + character + } + + fn key(&mut self, key: PhysicalKey, pressed: bool) { + let code = match key { + PhysicalKey::Code(code) => code, + _ => return, + }; + + match code { + KeyCode::ShiftLeft => self.keyboard.set(KeyboardModifierState::LSHIFT, pressed), + KeyCode::ShiftRight => self.keyboard.set(KeyboardModifierState::RSHIFT, pressed), + KeyCode::ControlLeft => self.keyboard.set(KeyboardModifierState::LCTRL, pressed), + KeyCode::ControlRight => self.keyboard.set(KeyboardModifierState::RCTRL, pressed), + KeyCode::AltLeft => self.keyboard.set(KeyboardModifierState::LALT, pressed), + KeyCode::AltRight => self.keyboard.set(KeyboardModifierState::RALT, pressed), + KeyCode::SuperLeft => self.keyboard.set(KeyboardModifierState::LSUPER, pressed), + KeyCode::SuperRight => self.keyboard.set(KeyboardModifierState::RSUPER, pressed), + _ => (), + } + } + + fn mouse( + &mut self, + left: bool, + middle: bool, + right: bool, + ) -> Option<(event::MouseButton, event::ElementState)> { + if self.mouse.contains(MouseButtonState::LEFT) != left { + self.mouse.set(MouseButtonState::LEFT, left); + return Some((event::MouseButton::Left, element_state(left))); + } + + if self.mouse.contains(MouseButtonState::MIDDLE) != middle { + self.mouse.set(MouseButtonState::MIDDLE, middle); + return Some((event::MouseButton::Middle, element_state(middle))); + } + + if self.mouse.contains(MouseButtonState::RIGHT) != right { + self.mouse.set(MouseButtonState::RIGHT, right); + return Some((event::MouseButton::Right, element_state(right))); + } + + None + } + + fn modifiers(&self) -> Modifiers { + let mut state = ModifiersState::empty(); + let mut pressed_mods = ModifiersKeys::empty(); + + if self.keyboard.intersects(KeyboardModifierState::LSHIFT | KeyboardModifierState::RSHIFT) { + state |= ModifiersState::SHIFT; + } + + pressed_mods + .set(ModifiersKeys::LSHIFT, self.keyboard.contains(KeyboardModifierState::LSHIFT)); + pressed_mods + .set(ModifiersKeys::RSHIFT, self.keyboard.contains(KeyboardModifierState::RSHIFT)); + + if self.keyboard.intersects(KeyboardModifierState::LCTRL | KeyboardModifierState::RCTRL) { + state |= ModifiersState::CONTROL; + } + + pressed_mods + .set(ModifiersKeys::LCONTROL, self.keyboard.contains(KeyboardModifierState::LCTRL)); + pressed_mods + .set(ModifiersKeys::RCONTROL, self.keyboard.contains(KeyboardModifierState::RCTRL)); + + if self.keyboard.intersects(KeyboardModifierState::LALT | KeyboardModifierState::RALT) { + state |= ModifiersState::ALT; + } + + pressed_mods.set(ModifiersKeys::LALT, self.keyboard.contains(KeyboardModifierState::LALT)); + pressed_mods.set(ModifiersKeys::RALT, self.keyboard.contains(KeyboardModifierState::RALT)); + + if self.keyboard.intersects(KeyboardModifierState::LSUPER | KeyboardModifierState::RSUPER) { + state |= ModifiersState::SUPER + } + + pressed_mods + .set(ModifiersKeys::LSUPER, self.keyboard.contains(KeyboardModifierState::LSUPER)); + pressed_mods + .set(ModifiersKeys::RSUPER, self.keyboard.contains(KeyboardModifierState::RSUPER)); + + Modifiers { state, pressed_mods } + } +} + +pub struct EventLoop { + windows: Vec<(Arc, EventState)>, + window_target: event_loop::ActiveEventLoop, + user_events_sender: mpsc::Sender, + user_events_receiver: mpsc::Receiver, +} + +impl EventLoop { + pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Result { + let (user_events_sender, user_events_receiver) = mpsc::channel(); + + let event_socket = Arc::new( + RedoxSocket::event() + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?, + ); + + let wake_socket = Arc::new( + TimeSocket::open() + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?, + ); + + event_socket + .write(&syscall::Event { + id: wake_socket.0.fd, + flags: syscall::EventFlags::EVENT_READ, + data: wake_socket.0.fd, + }) + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?; + + Ok(Self { + windows: Vec::new(), + window_target: event_loop::ActiveEventLoop { + p: ActiveEventLoop { + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(false), + creates: Mutex::new(VecDeque::new()), + redraws: Arc::new(Mutex::new(VecDeque::new())), + destroys: Arc::new(Mutex::new(VecDeque::new())), + event_socket, + wake_socket, + }, + _marker: PhantomData, + }, + user_events_sender, + user_events_receiver, + }) + } + + fn process_event( + window_id: WindowId, + event_option: EventOption, + event_state: &mut EventState, + mut event_handler: F, + ) where + F: FnMut(event::Event), + { + match event_option { + EventOption::Key(KeyEvent { character, scancode, pressed }) => { + // Convert scancode + let (physical_key, named_key_opt) = convert_scancode(scancode); + + // Get previous modifiers and update modifiers based on physical key + let modifiers_before = event_state.keyboard; + event_state.key(physical_key, pressed); + + // Default to unidentified key with no text + let mut logical_key = Key::Unidentified(NativeKey::Unidentified); + let mut key_without_modifiers = logical_key.clone(); + let mut text = None; + let mut text_with_all_modifiers = None; + + // Set key and text based on character + if character != '\0' { + let mut tmp = [0u8; 4]; + let character_str = character.encode_utf8(&mut tmp); + // The key with Shift and Caps Lock applied (but not Ctrl) + logical_key = Key::Character(character_str.into()); + // The key without Shift or Caps Lock applied + key_without_modifiers = + Key::Character(SmolStr::from_iter(character.to_lowercase())); + if pressed { + // The key with Shift and Caps Lock applied (but not Ctrl) + text = Some(character_str.into()); + // The key with Shift, Caps Lock, and Ctrl applied + let character_all_modifiers = + event_state.character_all_modifiers(character); + text_with_all_modifiers = + Some(character_all_modifiers.encode_utf8(&mut tmp).into()) + } + }; + + // Override key if a named key was found (this is to allow Enter to replace '\n') + if let Some(named_key) = named_key_opt { + logical_key = Key::Named(named_key); + key_without_modifiers = logical_key.clone(); + } + + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::KeyboardInput { + device_id: event::DeviceId(DeviceId), + event: event::KeyEvent { + logical_key, + physical_key, + location: KeyLocation::Standard, + state: element_state(pressed), + repeat: false, + text, + platform_specific: KeyEventExtra { + key_without_modifiers, + text_with_all_modifiers, + }, + }, + is_synthetic: false, + }, + }); + + // If the state of the modifiers has changed, send the event. + if modifiers_before != event_state.keyboard { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::ModifiersChanged(event_state.modifiers()), + }) + } + }, + EventOption::TextInput(TextInputEvent { character }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Ime(Ime::Preedit("".into(), None)), + }); + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Ime(Ime::Commit(character.into())), + }); + }, + EventOption::Mouse(MouseEvent { x, y }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CursorMoved { + device_id: event::DeviceId(DeviceId), + position: (x, y).into(), + }, + }); + }, + EventOption::MouseRelative(MouseRelativeEvent { dx, dy }) => { + event_handler(event::Event::DeviceEvent { + device_id: event::DeviceId(DeviceId), + event: event::DeviceEvent::MouseMotion { delta: (dx as f64, dy as f64) }, + }); + }, + EventOption::Button(ButtonEvent { left, middle, right }) => { + while let Some((button, state)) = event_state.mouse(left, middle, right) { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::MouseInput { + device_id: event::DeviceId(DeviceId), + state, + button, + }, + }); + } + }, + EventOption::Scroll(ScrollEvent { x, y }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::MouseWheel { + device_id: event::DeviceId(DeviceId), + delta: event::MouseScrollDelta::LineDelta(x as f32, y as f32), + phase: event::TouchPhase::Moved, + }, + }); + }, + EventOption::Quit(QuitEvent {}) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CloseRequested, + }); + }, + EventOption::Focus(FocusEvent { focused }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Focused(focused), + }); + }, + EventOption::Move(MoveEvent { x, y }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Moved((x, y).into()), + }); + }, + EventOption::Resize(ResizeEvent { width, height }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Resized((width, height).into()), + }); + + // Acknowledge resize after event loop. + event_state.resize_opt = Some((width, height)); + }, + // TODO: Screen, Clipboard, Drop + EventOption::Hover(HoverEvent { entered }) => { + if entered { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CursorEntered { + device_id: event::DeviceId(DeviceId), + }, + }); + } else { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CursorLeft { + device_id: event::DeviceId(DeviceId), + }, + }); + } + }, + other => { + tracing::warn!("unhandled event: {:?}", other); + }, + } + } + + pub fn run(mut self, mut event_handler_inner: F) -> Result<(), EventLoopError> + where + F: FnMut(event::Event, &event_loop::ActiveEventLoop), + { + let mut event_handler = + move |event: event::Event, window_target: &event_loop::ActiveEventLoop| { + event_handler_inner(event, window_target); + }; + + let mut start_cause = StartCause::Init; + + loop { + event_handler(event::Event::NewEvents(start_cause), &self.window_target); + + if start_cause == StartCause::Init { + event_handler(event::Event::Resumed, &self.window_target); + } + + // Handle window creates. + while let Some(window) = { + let mut creates = self.window_target.p.creates.lock().unwrap(); + creates.pop_front() + } { + let window_id = WindowId { fd: window.fd as u64 }; + + let mut buf: [u8; 4096] = [0; 4096]; + let path = window.fpath(&mut buf).expect("failed to read properties"); + let properties = WindowProperties::new(path); + + self.windows.push((window, EventState::default())); + + // Send resize event on create to indicate first size. + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Resized((properties.w, properties.h).into()), + }, + &self.window_target, + ); + + // Send resize event on create to indicate first position. + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Moved((properties.x, properties.y).into()), + }, + &self.window_target, + ); + } + + // Handle window destroys. + while let Some(destroy_id) = { + let mut destroys = self.window_target.p.destroys.lock().unwrap(); + destroys.pop_front() + } { + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(destroy_id), + event: event::WindowEvent::Destroyed, + }, + &self.window_target, + ); + + self.windows.retain(|(window, _event_state)| window.fd as u64 != destroy_id.fd); + } + + // Handle window events. + let mut i = 0; + // While loop is used here because the same window may be processed more than once. + while let Some((window, event_state)) = self.windows.get_mut(i) { + let window_id = WindowId { fd: window.fd as u64 }; + + let mut event_buf = [0u8; 16 * mem::size_of::()]; + let count = + syscall::read(window.fd, &mut event_buf).expect("failed to read window events"); + // Safety: orbclient::Event is a packed struct designed to be transferred over a + // socket. + let events = unsafe { + slice::from_raw_parts( + event_buf.as_ptr() as *const orbclient::Event, + count / mem::size_of::(), + ) + }; + + for orbital_event in events { + Self::process_event( + window_id, + orbital_event.to_option(), + event_state, + |event| event_handler(event, &self.window_target), + ); + } + + if count == event_buf.len() { + // If event buf was full, process same window again to ensure all events are + // drained. + continue; + } + + // Acknowledge the latest resize event. + if let Some((w, h)) = event_state.resize_opt.take() { + window + .write(format!("S,{w},{h}").as_bytes()) + .expect("failed to acknowledge resize"); + + // Require redraw after resize. + let mut redraws = self.window_target.p.redraws.lock().unwrap(); + if !redraws.contains(&window_id) { + redraws.push_back(window_id); + } + } + + // Move to next window. + i += 1; + } + + while let Ok(event) = self.user_events_receiver.try_recv() { + event_handler(event::Event::UserEvent(event), &self.window_target); + } + + // To avoid deadlocks the redraws lock is not held during event processing. + while let Some(window_id) = { + let mut redraws = self.window_target.p.redraws.lock().unwrap(); + redraws.pop_front() + } { + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::RedrawRequested, + }, + &self.window_target, + ); + } + + event_handler(event::Event::AboutToWait, &self.window_target); + + if self.window_target.p.exiting() { + break; + } + + let requested_resume = match self.window_target.p.control_flow() { + ControlFlow::Poll => { + start_cause = StartCause::Poll; + continue; + }, + ControlFlow::Wait => None, + ControlFlow::WaitUntil(instant) => Some(instant), + }; + + // Re-using wake socket caused extra wake events before because there were leftover + // timeouts, and then new timeouts were added each time a spurious timeout expired. + let timeout_socket = TimeSocket::open().unwrap(); + + self.window_target + .p + .event_socket + .write(&syscall::Event { + id: timeout_socket.0.fd, + flags: syscall::EventFlags::EVENT_READ, + data: 0, + }) + .unwrap(); + + let start = Instant::now(); + if let Some(instant) = requested_resume { + let mut time = timeout_socket.current_time().unwrap(); + + if let Some(duration) = instant.checked_duration_since(start) { + time.tv_sec += duration.as_secs() as i64; + time.tv_nsec += duration.subsec_nanos() as i32; + // Normalize timespec so tv_nsec is not greater than one second. + while time.tv_nsec >= 1_000_000_000 { + time.tv_sec += 1; + time.tv_nsec -= 1_000_000_000; + } + } + + timeout_socket.timeout(&time).unwrap(); + } + + // Wait for event if needed. + let mut event = syscall::Event::default(); + self.window_target.p.event_socket.read(&mut event).unwrap(); + + // TODO: handle spurious wakeups (redraw caused wakeup but redraw already handled) + match requested_resume { + Some(requested_resume) if event.id == timeout_socket.0.fd => { + // If the event is from the special timeout socket, report that resume + // time was reached. + start_cause = StartCause::ResumeTimeReached { start, requested_resume }; + }, + _ => { + // Normal window event or spurious timeout. + start_cause = StartCause::WaitCancelled { start, requested_resume }; + }, + } + } + + event_handler(event::Event::LoopExiting, &self.window_target); + + Ok(()) + } + + pub fn window_target(&self) -> &event_loop::ActiveEventLoop { + &self.window_target + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + user_events_sender: self.user_events_sender.clone(), + wake_socket: self.window_target.p.wake_socket.clone(), + } + } +} + +pub struct EventLoopProxy { + user_events_sender: mpsc::Sender, + wake_socket: Arc, +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), event_loop::EventLoopClosed> { + self.user_events_sender + .send(event) + .map_err(|mpsc::SendError(x)| event_loop::EventLoopClosed(x))?; + + self.wake_socket.wake().unwrap(); + + Ok(()) + } +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { + user_events_sender: self.user_events_sender.clone(), + wake_socket: self.wake_socket.clone(), + } + } +} + +impl Unpin for EventLoopProxy {} + +pub struct ActiveEventLoop { + control_flow: Cell, + exit: Cell, + pub(super) creates: Mutex>>, + pub(super) redraws: Arc>>, + pub(super) destroys: Arc>>, + pub(super) event_socket: Arc, + pub(super) wake_socket: Arc, +} + +impl ActiveEventLoop { + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + let _ = source.inner; + RootCustomCursor { inner: super::PlatformCustomCursor } + } + + pub fn primary_monitor(&self) -> Option { + Some(MonitorHandle) + } + + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle); + v + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Orbital(rwh_05::OrbitalDisplayHandle::empty()) + } + + #[inline] + pub fn system_theme(&self) -> Option { + None + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Orbital(rwh_06::OrbitalDisplayHandle::new())) + } + + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(true); + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get() + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::OrbitalDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::OrbitalDisplayHandle::new().into()) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/orbital/mod.rs b/third_party/winit-0.30.13/src/platform_impl/orbital/mod.rs new file mode 100644 index 0000000..2d15251 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/orbital/mod.rs @@ -0,0 +1,255 @@ +#![cfg(target_os = "redox")] + +use std::fmt::{self, Display, Formatter}; +use std::str; +use std::sync::Arc; + +use smol_str::SmolStr; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::keyboard::Key; + +pub(crate) use self::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle}; +mod event_loop; + +pub use self::window::Window; +mod window; + +struct RedoxSocket { + fd: usize, +} + +impl RedoxSocket { + fn event() -> syscall::Result { + Self::open_raw("event:") + } + + fn orbital(properties: &WindowProperties<'_>) -> syscall::Result { + Self::open_raw(&format!("{properties}")) + } + + // Paths should be checked to ensure they are actually sockets and not normal files. If a + // non-socket path is used, it could cause read and write to not function as expected. For + // example, the seek would change in a potentially unpredictable way if either read or write + // were called at the same time by multiple threads. + fn open_raw(path: &str) -> syscall::Result { + let fd = syscall::open(path, syscall::O_RDWR | syscall::O_CLOEXEC)?; + Ok(Self { fd }) + } + + fn read(&self, buf: &mut [u8]) -> syscall::Result<()> { + let count = syscall::read(self.fd, buf)?; + if count == buf.len() { + Ok(()) + } else { + Err(syscall::Error::new(syscall::EINVAL)) + } + } + + fn write(&self, buf: &[u8]) -> syscall::Result<()> { + let count = syscall::write(self.fd, buf)?; + if count == buf.len() { + Ok(()) + } else { + Err(syscall::Error::new(syscall::EINVAL)) + } + } + + fn fpath<'a>(&self, buf: &'a mut [u8]) -> syscall::Result<&'a str> { + let count = syscall::fpath(self.fd, buf)?; + str::from_utf8(&buf[..count]).map_err(|_err| syscall::Error::new(syscall::EINVAL)) + } +} + +impl Drop for RedoxSocket { + fn drop(&mut self) { + let _ = syscall::close(self.fd); + } +} + +pub struct TimeSocket(RedoxSocket); + +impl TimeSocket { + fn open() -> syscall::Result { + RedoxSocket::open_raw("time:4").map(Self) + } + + // Read current time. + fn current_time(&self) -> syscall::Result { + let mut timespec = syscall::TimeSpec::default(); + self.0.read(&mut timespec)?; + Ok(timespec) + } + + // Write a timeout. + fn timeout(&self, timespec: &syscall::TimeSpec) -> syscall::Result<()> { + self.0.write(timespec) + } + + // Wake immediately. + fn wake(&self) -> syscall::Result<()> { + // Writing a default TimeSpec will always trigger a time event. + self.timeout(&syscall::TimeSpec::default()) + } +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct WindowId { + fd: u64, +} + +impl WindowId { + pub const fn dummy() -> Self { + WindowId { fd: u64::MAX } + } +} + +impl From for u64 { + fn from(id: WindowId) -> Self { + id.fd + } +} + +impl From for WindowId { + fn from(fd: u64) -> Self { + Self { fd } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct DeviceId; + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct PlatformSpecificWindowAttributes; + +struct WindowProperties<'a> { + flags: &'a str, + x: i32, + y: i32, + w: u32, + h: u32, + title: &'a str, +} + +impl<'a> WindowProperties<'a> { + fn new(path: &'a str) -> Self { + // orbital:flags/x/y/w/h/t + let mut parts = path.splitn(6, '/'); + let flags = parts.next().unwrap_or(""); + let x = parts.next().map_or(0, |part| part.parse::().unwrap_or(0)); + let y = parts.next().map_or(0, |part| part.parse::().unwrap_or(0)); + let w = parts.next().map_or(0, |part| part.parse::().unwrap_or(0)); + let h = parts.next().map_or(0, |part| part.parse::().unwrap_or(0)); + let title = parts.next().unwrap_or(""); + Self { flags, x, y, w, h, title } + } +} + +impl fmt::Display for WindowProperties<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "orbital:{}/{}/{}/{}/{}/{}", + self.flags, self.x, self.y, self.w, self.h, self.title + ) + } +} + +#[derive(Clone, Debug)] +pub struct OsError(Arc); + +impl OsError { + fn new(error: syscall::Error) -> Self { + Self(Arc::new(error)) + } +} + +impl Display for OsError { + fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> { + self.0.fmt(fmt) + } +} + +pub(crate) use crate::cursor::{ + NoCustomCursor as PlatformCustomCursor, NoCustomCursor as PlatformCustomCursorSource, +}; +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("Redox Device".to_owned()) + } + + pub fn size(&self) -> PhysicalSize { + PhysicalSize::new(0, 0) // TODO + } + + pub fn position(&self) -> PhysicalPosition { + (0, 0).into() + } + + pub fn scale_factor(&self) -> f64 { + 1.0 // TODO + } + + pub fn refresh_rate_millihertz(&self) -> Option { + // FIXME no way to get real refresh rate for now. + None + } + + pub fn video_modes(&self) -> impl Iterator { + let size = self.size().into(); + // FIXME this is not the real refresh rate + // (it is guaranteed to support 32 bit color though) + std::iter::once(VideoModeHandle { + size, + bit_depth: 32, + refresh_rate_millihertz: 60000, + monitor: self.clone(), + }) + } +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VideoModeHandle { + size: (u32, u32), + bit_depth: u16, + refresh_rate_millihertz: u32, + monitor: MonitorHandle, +} + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub key_without_modifiers: Key, + pub text_with_all_modifiers: Option, +} diff --git a/third_party/winit-0.30.13/src/platform_impl/orbital/window.rs b/third_party/winit-0.30.13/src/platform_impl/orbital/window.rs new file mode 100644 index 0000000..3e676c3 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/orbital/window.rs @@ -0,0 +1,509 @@ +use std::collections::VecDeque; +use std::sync::{Arc, Mutex}; + +use crate::cursor::Cursor; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::platform_impl::Fullscreen; +use crate::window::ImePurpose; +use crate::{error, window}; + +use super::{ + ActiveEventLoop, MonitorHandle, OsError, RedoxSocket, TimeSocket, WindowId, WindowProperties, +}; + +// These values match the values uses in the `window_new` function in orbital: +// https://gitlab.redox-os.org/redox-os/orbital/-/blob/master/src/scheme.rs +const ORBITAL_FLAG_ASYNC: char = 'a'; +const ORBITAL_FLAG_BACK: char = 'b'; +const ORBITAL_FLAG_FRONT: char = 'f'; +const ORBITAL_FLAG_HIDDEN: char = 'h'; +const ORBITAL_FLAG_BORDERLESS: char = 'l'; +const ORBITAL_FLAG_MAXIMIZED: char = 'm'; +const ORBITAL_FLAG_RESIZABLE: char = 'r'; +const ORBITAL_FLAG_TRANSPARENT: char = 't'; + +pub struct Window { + window_socket: Arc, + redraws: Arc>>, + destroys: Arc>>, + wake_socket: Arc, +} + +impl Window { + pub(crate) fn new( + el: &ActiveEventLoop, + attrs: window::WindowAttributes, + ) -> Result { + let scale = MonitorHandle.scale_factor(); + + let (x, y) = if let Some(pos) = attrs.position { + pos.to_physical::(scale).into() + } else { + // These coordinates are a special value to center the window. + (-1, -1) + }; + + let (w, h): (u32, u32) = if let Some(size) = attrs.inner_size { + size.to_physical::(scale).into() + } else { + (1024, 768) + }; + + // TODO: min/max inner_size + + // Async by default. + let mut flag_str = ORBITAL_FLAG_ASYNC.to_string(); + + if attrs.maximized { + flag_str.push(ORBITAL_FLAG_MAXIMIZED); + } + + if attrs.resizable { + flag_str.push(ORBITAL_FLAG_RESIZABLE); + } + + // TODO: fullscreen + + if attrs.transparent { + flag_str.push(ORBITAL_FLAG_TRANSPARENT); + } + + if !attrs.decorations { + flag_str.push(ORBITAL_FLAG_BORDERLESS); + } + + if !attrs.visible { + flag_str.push(ORBITAL_FLAG_HIDDEN); + } + + match attrs.window_level { + window::WindowLevel::AlwaysOnBottom => { + flag_str.push(ORBITAL_FLAG_BACK); + }, + window::WindowLevel::Normal => {}, + window::WindowLevel::AlwaysOnTop => { + flag_str.push(ORBITAL_FLAG_FRONT); + }, + } + + // TODO: window_icon + + // Open window. + let window = RedoxSocket::orbital(&WindowProperties { + flags: &flag_str, + x, + y, + w, + h, + title: &attrs.title, + }) + .expect("failed to open window"); + + // Add to event socket. + el.event_socket + .write(&syscall::Event { + id: window.fd, + flags: syscall::EventFlags::EVENT_READ, + data: window.fd, + }) + .unwrap(); + + let window_socket = Arc::new(window); + + // Notify event thread that this window was created, it will send some default events. + { + let mut creates = el.creates.lock().unwrap(); + creates.push_back(window_socket.clone()); + } + + el.wake_socket.wake().unwrap(); + + Ok(Self { + window_socket, + redraws: el.redraws.clone(), + destroys: el.destroys.clone(), + wake_socket: el.wake_socket.clone(), + }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + f(self) + } + + fn get_flag(&self, flag: char) -> Result { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self + .window_socket + .fpath(&mut buf) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + let properties = WindowProperties::new(path); + Ok(properties.flags.contains(flag)) + } + + fn set_flag(&self, flag: char, value: bool) -> Result<(), error::ExternalError> { + self.window_socket + .write(format!("F,{flag},{}", if value { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn id(&self) -> WindowId { + WindowId { fd: self.window_socket.fd as u64 } + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + Some(MonitorHandle) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle); + v + } + + #[inline] + pub fn current_monitor(&self) -> Option { + Some(MonitorHandle) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + MonitorHandle.scale_factor() + } + + #[inline] + pub fn request_redraw(&self) { + let window_id = self.id(); + let mut redraws = self.redraws.lock().unwrap(); + if !redraws.contains(&window_id) { + redraws.push_back(window_id); + + self.wake_socket.wake().unwrap(); + } + } + + #[inline] + pub fn pre_present_notify(&self) {} + + #[inline] + pub fn reset_dead_keys(&self) { + // TODO? + } + + #[inline] + pub fn inner_position(&self) -> Result, error::NotSupportedError> { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self.window_socket.fpath(&mut buf).expect("failed to read properties"); + let properties = WindowProperties::new(path); + Ok((properties.x, properties.y).into()) + } + + #[inline] + pub fn outer_position(&self) -> Result, error::NotSupportedError> { + // TODO: adjust for window decorations + self.inner_position() + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + // TODO: adjust for window decorations + let (x, y): (i32, i32) = position.to_physical::(self.scale_factor()).into(); + self.window_socket.write(format!("P,{x},{y}").as_bytes()).expect("failed to set position"); + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self.window_socket.fpath(&mut buf).expect("failed to read properties"); + let properties = WindowProperties::new(path); + (properties.w, properties.h).into() + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let (w, h): (u32, u32) = size.to_physical::(self.scale_factor()).into(); + self.window_socket.write(format!("S,{w},{h}").as_bytes()).expect("failed to set size"); + None + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + // TODO: adjust for window decorations + self.inner_size() + } + + #[inline] + pub fn set_min_inner_size(&self, _: Option) {} + + #[inline] + pub fn set_max_inner_size(&self, _: Option) {} + + #[inline] + pub fn title(&self) -> String { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self.window_socket.fpath(&mut buf).expect("failed to read properties"); + let properties = WindowProperties::new(path); + properties.title.to_string() + } + + #[inline] + pub fn set_title(&self, title: &str) { + self.window_socket.write(format!("T,{title}").as_bytes()).expect("failed to set title"); + } + + #[inline] + pub fn set_transparent(&self, transparent: bool) { + let _ = self.set_flag(ORBITAL_FLAG_TRANSPARENT, transparent); + } + + #[inline] + pub fn set_blur(&self, _blur: bool) {} + + #[inline] + pub fn set_visible(&self, visible: bool) { + let _ = self.set_flag(ORBITAL_FLAG_HIDDEN, !visible); + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(!self.get_flag(ORBITAL_FLAG_HIDDEN).unwrap_or(false)) + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + None + } + + #[inline] + pub fn set_resize_increments(&self, _increments: Option) {} + + #[inline] + pub fn set_resizable(&self, resizeable: bool) { + let _ = self.set_flag(ORBITAL_FLAG_RESIZABLE, resizeable); + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.get_flag(ORBITAL_FLAG_RESIZABLE).unwrap_or(false) + } + + #[inline] + pub fn set_minimized(&self, _minimized: bool) {} + + #[inline] + pub fn is_minimized(&self) -> Option { + None + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let _ = self.set_flag(ORBITAL_FLAG_MAXIMIZED, maximized); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + self.get_flag(ORBITAL_FLAG_MAXIMIZED).unwrap_or(false) + } + + #[inline] + pub(crate) fn set_fullscreen(&self, _monitor: Option) {} + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + None + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + let _ = self.set_flag(ORBITAL_FLAG_BORDERLESS, !decorations); + } + + #[inline] + pub fn is_decorated(&self) -> bool { + !self.get_flag(ORBITAL_FLAG_BORDERLESS).unwrap_or(false) + } + + #[inline] + pub fn set_window_level(&self, level: window::WindowLevel) { + match level { + window::WindowLevel::AlwaysOnBottom => { + let _ = self.set_flag(ORBITAL_FLAG_BACK, true); + }, + window::WindowLevel::Normal => { + let _ = self.set_flag(ORBITAL_FLAG_BACK, false); + let _ = self.set_flag(ORBITAL_FLAG_FRONT, false); + }, + window::WindowLevel::AlwaysOnTop => { + let _ = self.set_flag(ORBITAL_FLAG_FRONT, true); + }, + } + } + + #[inline] + pub fn set_window_icon(&self, _window_icon: Option) {} + + #[inline] + pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) {} + + #[inline] + pub fn set_ime_allowed(&self, _allowed: bool) {} + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn focus_window(&self) {} + + #[inline] + pub fn request_user_attention(&self, _request_type: Option) {} + + #[inline] + pub fn set_cursor(&self, _: Cursor) {} + + #[inline] + pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported(error::NotSupportedError::new())) + } + + #[inline] + pub fn set_cursor_grab( + &self, + mode: window::CursorGrabMode, + ) -> Result<(), error::ExternalError> { + let (grab, relative) = match mode { + window::CursorGrabMode::None => (false, false), + window::CursorGrabMode::Confined => (true, false), + window::CursorGrabMode::Locked => (true, true), + }; + self.window_socket + .write(format!("M,G,{}", if grab { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + self.window_socket + .write(format!("M,R,{}", if relative { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let _ = self.window_socket.write(format!("M,C,{}", if visible { 1 } else { 0 }).as_bytes()); + } + + #[inline] + pub fn drag_window(&self) -> Result<(), error::ExternalError> { + self.window_socket + .write(b"D") + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn drag_resize_window( + &self, + direction: window::ResizeDirection, + ) -> Result<(), error::ExternalError> { + let arg = match direction { + window::ResizeDirection::East => "R", + window::ResizeDirection::North => "T", + window::ResizeDirection::NorthEast => "T,R", + window::ResizeDirection::NorthWest => "T,L", + window::ResizeDirection::South => "B", + window::ResizeDirection::SouthEast => "B,R", + window::ResizeDirection::SouthWest => "B,L", + window::ResizeDirection::West => "L", + }; + self.window_socket + .write(format!("D,{arg}").as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + #[inline] + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported(error::NotSupportedError::new())) + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut handle = rwh_04::OrbitalHandle::empty(); + handle.window = self.window_socket.fd as *mut _; + rwh_04::RawWindowHandle::Orbital(handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut handle = rwh_05::OrbitalWindowHandle::empty(); + handle.window = self.window_socket.fd as *mut _; + rwh_05::RawWindowHandle::Orbital(handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Orbital(rwh_05::OrbitalDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let handle = rwh_06::OrbitalWindowHandle::new({ + let window = self.window_socket.fd as *mut _; + std::ptr::NonNull::new(window).expect("orbital fd should never be null") + }); + Ok(rwh_06::RawWindowHandle::Orbital(handle)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Orbital(rwh_06::OrbitalDisplayHandle::new())) + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: window::WindowButtons) {} + + #[inline] + pub fn enabled_buttons(&self) -> window::WindowButtons { + window::WindowButtons::all() + } + + #[inline] + pub fn theme(&self) -> Option { + None + } + + #[inline] + pub fn has_focus(&self) -> bool { + false + } + + #[inline] + pub fn set_theme(&self, _theme: Option) {} + + pub fn set_content_protected(&self, _protected: bool) {} +} + +impl Drop for Window { + fn drop(&mut self) { + { + let mut destroys = self.destroys.lock().unwrap(); + destroys.push_back(self.id()); + } + + self.wake_socket.wake().unwrap(); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/async/abortable.rs b/third_party/winit-0.30.13/src/platform_impl/web/async/abortable.rs new file mode 100644 index 0000000..671ae87 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/async/abortable.rs @@ -0,0 +1,96 @@ +use std::error::Error; +use std::fmt::{self, Display, Formatter}; +use std::future::Future; +use std::pin::Pin; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use pin_project::pin_project; + +use super::AtomicWaker; + +#[pin_project] +pub struct Abortable { + #[pin] + future: F, + shared: Arc, +} + +impl Abortable { + pub fn new(handle: AbortHandle, future: F) -> Self { + Self { future, shared: handle.0 } + } +} + +impl Future for Abortable { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.shared.aborted.load(Ordering::Relaxed) { + return Poll::Ready(Err(Aborted)); + } + + if let Poll::Ready(value) = self.as_mut().project().future.poll(cx) { + return Poll::Ready(Ok(value)); + } + + self.shared.waker.register(cx.waker()); + + if self.shared.aborted.load(Ordering::Relaxed) { + return Poll::Ready(Err(Aborted)); + } + + Poll::Pending + } +} + +#[derive(Debug)] +struct Shared { + waker: AtomicWaker, + aborted: AtomicBool, +} + +#[derive(Clone, Debug)] +pub struct AbortHandle(Arc); + +impl AbortHandle { + pub fn new() -> Self { + Self(Arc::new(Shared { waker: AtomicWaker::new(), aborted: AtomicBool::new(false) })) + } + + pub fn abort(&self) { + self.0.aborted.store(true, Ordering::Relaxed); + self.0.waker.wake() + } +} + +#[derive(Debug)] +pub struct DropAbortHandle(AbortHandle); + +impl DropAbortHandle { + pub fn new(handle: AbortHandle) -> Self { + Self(handle) + } + + pub fn handle(&self) -> AbortHandle { + self.0.clone() + } +} + +impl Drop for DropAbortHandle { + fn drop(&mut self) { + self.0.abort() + } +} + +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub struct Aborted; + +impl Display for Aborted { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "`Abortable` future has been aborted") + } +} + +impl Error for Aborted {} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/async/atomic_waker.rs b/third_party/winit-0.30.13/src/platform_impl/web/async/atomic_waker.rs new file mode 100644 index 0000000..87b8361 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/async/atomic_waker.rs @@ -0,0 +1,35 @@ +use std::cell::RefCell; +use std::ops::Deref; +use std::task::Waker; + +#[derive(Debug)] +pub struct AtomicWaker(RefCell>); + +impl AtomicWaker { + pub const fn new() -> Self { + Self(RefCell::new(None)) + } + + pub fn register(&self, waker: &Waker) { + let mut this = self.0.borrow_mut(); + + if let Some(old_waker) = this.deref() { + if old_waker.will_wake(waker) { + return; + } + } + + *this = Some(waker.clone()); + } + + pub fn wake(&self) { + if let Some(waker) = self.0.borrow_mut().take() { + waker.wake(); + } + } +} + +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Send for AtomicWaker {} +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Sync for AtomicWaker {} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/async/channel.rs b/third_party/winit-0.30.13/src/platform_impl/web/async/channel.rs new file mode 100644 index 0000000..11a7a47 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/async/channel.rs @@ -0,0 +1,113 @@ +use std::future; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{self, RecvError, SendError, TryRecvError}; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + +use super::AtomicWaker; + +pub fn channel() -> (Sender, Receiver) { + let (sender, receiver) = mpsc::channel(); + let shared = Arc::new(Shared { closed: AtomicBool::new(false), waker: AtomicWaker::new() }); + + let sender = + Sender(Arc::new(SenderInner { sender: Mutex::new(sender), shared: Arc::clone(&shared) })); + let receiver = Receiver { receiver: Rc::new(receiver), shared }; + + (sender, receiver) +} + +pub struct Sender(Arc>); + +struct SenderInner { + // We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't + // be accessed on the main thread, as it could block. Additionally we need + // to wrap `Sender` in an `Arc` to make it cloneable on the main thread without + // having to block. + sender: Mutex>, + shared: Arc, +} + +impl Sender { + pub fn send(&self, event: T) -> Result<(), SendError> { + self.0.sender.lock().unwrap().send(event)?; + self.0.shared.waker.wake(); + + Ok(()) + } +} + +impl SenderInner { + fn close(&self) { + self.shared.closed.store(true, Ordering::Relaxed); + self.shared.waker.wake(); + } +} + +impl Clone for Sender { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} + +impl Drop for SenderInner { + fn drop(&mut self) { + self.close(); + } +} + +pub struct Receiver { + receiver: Rc>, + shared: Arc, +} + +impl Receiver { + pub async fn next(&self) -> Result { + future::poll_fn(|cx| match self.receiver.try_recv() { + Ok(event) => Poll::Ready(Ok(event)), + Err(TryRecvError::Empty) => { + self.shared.waker.register(cx.waker()); + + match self.receiver.try_recv() { + Ok(event) => Poll::Ready(Ok(event)), + Err(TryRecvError::Empty) => { + if self.shared.closed.load(Ordering::Relaxed) { + Poll::Ready(Err(RecvError)) + } else { + Poll::Pending + } + }, + Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), + } + }, + Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), + }) + .await + } + + pub fn try_recv(&self) -> Result, RecvError> { + match self.receiver.try_recv() { + Ok(value) => Ok(Some(value)), + Err(TryRecvError::Empty) => Ok(None), + Err(TryRecvError::Disconnected) => Err(RecvError), + } + } +} + +impl Clone for Receiver { + fn clone(&self) -> Self { + Self { receiver: Rc::clone(&self.receiver), shared: Arc::clone(&self.shared) } + } +} + +impl Drop for Receiver { + fn drop(&mut self) { + self.shared.closed.store(true, Ordering::Relaxed); + } +} + +struct Shared { + closed: AtomicBool, + waker: AtomicWaker, +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/async/concurrent_queue.rs b/third_party/winit-0.30.13/src/platform_impl/web/async/concurrent_queue.rs new file mode 100644 index 0000000..70afb3d --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/async/concurrent_queue.rs @@ -0,0 +1,52 @@ +use std::cell::{Cell, RefCell}; + +#[derive(Debug)] +pub struct ConcurrentQueue { + queue: RefCell>, + closed: Cell, +} + +pub enum PushError { + #[allow(dead_code)] + Full(T), + Closed(T), +} + +pub enum PopError { + Empty, + Closed, +} + +impl ConcurrentQueue { + pub fn unbounded() -> Self { + Self { queue: RefCell::new(Vec::new()), closed: Cell::new(false) } + } + + pub fn push(&self, value: T) -> Result<(), PushError> { + if self.closed.get() { + return Err(PushError::Closed(value)); + } + + self.queue.borrow_mut().push(value); + Ok(()) + } + + pub fn pop(&self) -> Result { + self.queue.borrow_mut().pop().ok_or_else(|| { + if self.closed.get() { + PopError::Closed + } else { + PopError::Empty + } + }) + } + + pub fn close(&self) -> bool { + !self.closed.replace(true) + } +} + +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Send for ConcurrentQueue {} +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Sync for ConcurrentQueue {} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/async/dispatcher.rs b/third_party/winit-0.30.13/src/platform_impl/web/async/dispatcher.rs new file mode 100644 index 0000000..10ab345 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/async/dispatcher.rs @@ -0,0 +1,106 @@ +use super::super::main_thread::MainThreadMarker; +use super::{channel, Receiver, Sender, Wrapper}; +use std::cell::Ref; +use std::sync::{Arc, Condvar, Mutex}; + +pub struct Dispatcher(Wrapper>, Closure>); + +struct Closure(Box); + +impl Dispatcher { + #[track_caller] + pub fn new(main_thread: MainThreadMarker, value: T) -> Option<(Self, DispatchRunner)> { + let (sender, receiver) = channel::>(); + + Wrapper::new( + main_thread, + value, + |value, Closure(closure)| { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do + // anything funny with it here. See `Self::queue()`. + closure(value.borrow().as_ref().unwrap()) + }, + { + let receiver = receiver.clone(); + move |value| async move { + while let Ok(Closure(closure)) = receiver.next().await { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't + // do anything funny with it here. See + // `Self::queue()`. + closure(value.borrow().as_ref().unwrap()) + } + } + }, + sender, + |sender, closure| { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do + // anything funny with it here. See `Self::queue()`. + sender.send(closure).unwrap() + }, + ) + .map(|wrapper| (Self(wrapper.clone()), DispatchRunner { wrapper, receiver })) + } + + pub fn value(&self) -> Option> { + self.0.value() + } + + pub fn dispatch(&self, f: impl 'static + FnOnce(&T) + Send) { + if let Some(value) = self.0.value() { + f(&value) + } else { + self.0.send(Closure(Box::new(f))) + } + } + + pub fn queue(&self, f: impl FnOnce(&T) -> R + Send) -> R { + if let Some(value) = self.0.value() { + f(&value) + } else { + let pair = Arc::new((Mutex::new(None), Condvar::new())); + let closure = Box::new({ + let pair = pair.clone(); + move |value: &T| { + *pair.0.lock().unwrap() = Some(f(value)); + pair.1.notify_one(); + } + }) as Box; + // SAFETY: The `transmute` is necessary because `Closure` requires `'static`. This is + // safe because this function won't return until `f` has finished executing. See + // `Self::new()`. + let closure = Closure(unsafe { + std::mem::transmute::< + Box, + Box, + >(closure) + }); + + self.0.send(closure); + + let mut started = pair.0.lock().unwrap(); + + while started.is_none() { + started = pair.1.wait(started).unwrap(); + } + + started.take().unwrap() + } + } +} + +pub struct DispatchRunner { + wrapper: Wrapper>, Closure>, + receiver: Receiver>, +} + +impl DispatchRunner { + pub fn run(&self) { + while let Some(Closure(closure)) = + self.receiver.try_recv().expect("should only be closed when `Dispatcher` is dropped") + { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything + // funny with it here. See `Self::queue()`. + closure(&self.wrapper.value().expect("don't call this outside the main thread")) + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/async/mod.rs b/third_party/winit-0.30.13/src/platform_impl/web/async/mod.rs new file mode 100644 index 0000000..4681cf9 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/async/mod.rs @@ -0,0 +1,19 @@ +mod abortable; +#[cfg(not(target_feature = "atomics"))] +mod atomic_waker; +mod channel; +#[cfg(not(target_feature = "atomics"))] +mod concurrent_queue; +mod dispatcher; +mod notifier; +mod waker; +mod wrapper; + +pub use self::abortable::{AbortHandle, Abortable, DropAbortHandle}; +pub use self::channel::{channel, Receiver, Sender}; +pub use self::dispatcher::{DispatchRunner, Dispatcher}; +pub use self::notifier::{Notified, Notifier}; +pub use self::waker::{Waker, WakerSpawner}; +use self::wrapper::Wrapper; +use atomic_waker::AtomicWaker; +use concurrent_queue::{ConcurrentQueue, PushError}; diff --git a/third_party/winit-0.30.13/src/platform_impl/web/async/notifier.rs b/third_party/winit-0.30.13/src/platform_impl/web/async/notifier.rs new file mode 100644 index 0000000..81655d1 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/async/notifier.rs @@ -0,0 +1,72 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, OnceLock}; +use std::task::{Context, Poll, Waker}; + +use super::{ConcurrentQueue, PushError}; + +#[derive(Debug)] +pub struct Notifier(Arc>); + +impl Notifier { + pub fn new() -> Self { + Self(Arc::new(Inner { queue: ConcurrentQueue::unbounded(), value: OnceLock::new() })) + } + + pub fn notify(self, value: T) { + if self.0.value.set(value).is_err() { + unreachable!("value set before") + } + + self.0.queue.close(); + + while let Ok(waker) = self.0.queue.pop() { + waker.wake() + } + } + + pub fn notified(&self) -> Notified { + Notified(Some(Arc::clone(&self.0))) + } +} + +#[derive(Clone, Debug)] +pub struct Notified(Option>>); + +impl Future for Notified { + type Output = T; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.0.take().expect("`Receiver` polled after completion"); + + if this.value.get().is_none() { + match this.queue.push(cx.waker().clone()) { + Ok(()) => { + if this.value.get().is_none() { + self.0 = Some(this); + return Poll::Pending; + } + }, + Err(PushError::Closed(_)) => (), + Err(PushError::Full(_)) => { + unreachable!("found full queue despite using unbounded queue") + }, + } + } + + let (Ok(Some(value)) | Err(Some(value))) = Arc::try_unwrap(this) + .map(|mut inner| inner.value.take()) + .map_err(|this| this.value.get().cloned()) + else { + unreachable!("found no value despite being ready") + }; + + Poll::Ready(value) + } +} + +#[derive(Debug)] +struct Inner { + queue: ConcurrentQueue, + value: OnceLock, +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/async/waker.rs b/third_party/winit-0.30.13/src/platform_impl/web/async/waker.rs new file mode 100644 index 0000000..40b316d --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/async/waker.rs @@ -0,0 +1,131 @@ +use std::future; +use std::num::NonZeroUsize; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::Arc; +use std::task::Poll; + +use super::super::main_thread::MainThreadMarker; +use super::{AtomicWaker, Wrapper}; + +pub struct WakerSpawner(Wrapper, Sender, NonZeroUsize>); + +pub struct Waker(Wrapper, Sender, NonZeroUsize>); + +struct Handler { + value: T, + handler: fn(&T, NonZeroUsize, bool), +} + +#[derive(Clone)] +struct Sender(Arc); + +impl WakerSpawner { + #[track_caller] + pub fn new( + main_thread: MainThreadMarker, + value: T, + handler: fn(&T, NonZeroUsize, bool), + ) -> Option { + let inner = Arc::new(Inner { + counter: AtomicUsize::new(0), + waker: AtomicWaker::new(), + closed: AtomicBool::new(false), + }); + + let handler = Handler { value, handler }; + + let sender = Sender(Arc::clone(&inner)); + + let wrapper = Wrapper::new( + main_thread, + handler, + |handler, count| { + let handler = handler.borrow(); + let handler = handler.as_ref().unwrap(); + (handler.handler)(&handler.value, count, true); + }, + { + let inner = Arc::clone(&inner); + + move |handler| async move { + while let Some(count) = future::poll_fn(|cx| { + let count = inner.counter.swap(0, Ordering::Relaxed); + + match NonZeroUsize::new(count) { + Some(count) => Poll::Ready(Some(count)), + None => { + inner.waker.register(cx.waker()); + + let count = inner.counter.swap(0, Ordering::Relaxed); + + match NonZeroUsize::new(count) { + Some(count) => Poll::Ready(Some(count)), + None => { + if inner.closed.load(Ordering::Relaxed) { + return Poll::Ready(None); + } + + Poll::Pending + }, + } + }, + } + }) + .await + { + let handler = handler.borrow(); + let handler = handler.as_ref().unwrap(); + (handler.handler)(&handler.value, count, false); + } + } + }, + sender, + |inner, _| { + inner.0.counter.fetch_add(1, Ordering::Relaxed); + inner.0.waker.wake(); + }, + )?; + + Some(Self(wrapper)) + } + + pub fn waker(&self) -> Waker { + Waker(self.0.clone()) + } + + pub fn fetch(&self) -> usize { + debug_assert!( + MainThreadMarker::new().is_some(), + "this should only be called from the main thread" + ); + + self.0.with_sender_data(|inner| inner.0.counter.swap(0, Ordering::Relaxed)) + } +} + +impl Drop for WakerSpawner { + fn drop(&mut self) { + self.0.with_sender_data(|inner| { + inner.0.closed.store(true, Ordering::Relaxed); + inner.0.waker.wake(); + }); + } +} + +impl Waker { + pub fn wake(&self) { + self.0.send(NonZeroUsize::MIN) + } +} + +impl Clone for Waker { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +struct Inner { + counter: AtomicUsize, + waker: AtomicWaker, + closed: AtomicBool, +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/async/wrapper.rs b/third_party/winit-0.30.13/src/platform_impl/web/async/wrapper.rs new file mode 100644 index 0000000..2c7f694 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/async/wrapper.rs @@ -0,0 +1,89 @@ +use super::super::main_thread::MainThreadMarker; +use std::cell::{Ref, RefCell}; +use std::future::Future; +use std::marker::PhantomData; +use std::sync::Arc; + +// Unsafe wrapper type that allows us to use `T` when it's not `Send` from other threads. +// `value` **must** only be accessed on the main thread. +pub struct Wrapper { + value: Value, + handler: fn(&RefCell>, E), + sender_data: S, + sender_handler: fn(&S, E), +} + +struct Value { + // SAFETY: + // This value must not be accessed if not on the main thread. + // + // - We wrap this in an `Arc` to allow it to be safely cloned without accessing the value. + // - The `RefCell` lets us mutably access in the main thread but is safe to drop in any thread + // because it has no `Drop` behavior. + // - The `Option` lets us safely drop `T` only in the main thread. + value: Arc>>, + // Prevent's `Send` or `Sync` to be automatically implemented. + local: PhantomData<*const ()>, +} + +// SAFETY: See `Self::value`. +unsafe impl Send for Value {} +// SAFETY: See `Self::value`. +unsafe impl Sync for Value {} + +impl Wrapper { + #[track_caller] + pub fn new>( + _: MainThreadMarker, + value: V, + handler: fn(&RefCell>, E), + receiver: impl 'static + FnOnce(Arc>>) -> R, + sender_data: S, + sender_handler: fn(&S, E), + ) -> Option { + let value = Arc::new(RefCell::new(Some(value))); + + wasm_bindgen_futures::spawn_local({ + let value = Arc::clone(&value); + async move { + receiver(Arc::clone(&value)).await; + drop(value.borrow_mut().take().unwrap()); + } + }); + + Some(Self { + value: Value { value, local: PhantomData }, + handler, + sender_data, + sender_handler, + }) + } + + pub fn send(&self, event: E) { + if MainThreadMarker::new().is_some() { + (self.handler)(&self.value.value, event) + } else { + (self.sender_handler)(&self.sender_data, event) + } + } + + pub fn value(&self) -> Option> { + MainThreadMarker::new() + .map(|_| Ref::map(self.value.value.borrow(), |value| value.as_ref().unwrap())) + } + + pub fn with_sender_data(&self, f: impl FnOnce(&S) -> T) -> T { + f(&self.sender_data) + } +} + +impl Clone for Wrapper { + fn clone(&self) -> Self { + Self { + value: Value { value: self.value.value.clone(), local: PhantomData }, + handler: self.handler, + sender_data: self.sender_data.clone(), + sender_handler: self.sender_handler, + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/cursor.rs b/third_party/winit-0.30.13/src/platform_impl/web/cursor.rs new file mode 100644 index 0000000..214ff45 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/cursor.rs @@ -0,0 +1,730 @@ +use std::cell::RefCell; +use std::future::{self, Future}; +use std::hash::{Hash, Hasher}; +use std::mem; +use std::ops::{Deref, DerefMut}; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::Arc; +use std::task::{ready, Context, Poll, Waker}; +use std::time::Duration; + +use cursor_icon::CursorIcon; +use js_sys::{Array, Object}; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; +use web_sys::{ + Blob, Document, DomException, HtmlCanvasElement, HtmlImageElement, ImageBitmap, + ImageBitmapOptions, ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window, +}; + +use super::backend::Style; +use super::main_thread::{MainThreadMarker, MainThreadSafe}; +use super::r#async::{AbortHandle, Abortable, DropAbortHandle, Notified, Notifier}; +use super::ActiveEventLoop; +use crate::cursor::{BadImage, Cursor, CursorImage, CustomCursor as RootCustomCursor}; +use crate::platform::web::CustomCursorError; + +#[derive(Debug)] +pub(crate) enum CustomCursorSource { + Image(CursorImage), + Url { url: String, hotspot_x: u16, hotspot_y: u16 }, + Animation { duration: Duration, cursors: Vec }, +} + +impl CustomCursorSource { + pub fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + Ok(CustomCursorSource::Image(CursorImage::from_rgba( + rgba, width, height, hotspot_x, hotspot_y, + )?)) + } +} + +#[derive(Clone, Debug)] +pub struct CustomCursor { + pub(crate) animation: bool, + state: Arc>>, +} + +impl Hash for CustomCursor { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.state).hash(state); + } +} + +impl PartialEq for CustomCursor { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.state, &other.state) + } +} + +impl Eq for CustomCursor {} + +impl CustomCursor { + pub(crate) fn new(event_loop: &ActiveEventLoop, source: CustomCursorSource) -> Self { + match source { + CustomCursorSource::Image(image) => Self::build_spawn( + event_loop, + from_rgba(event_loop.runner.window(), event_loop.runner.document().clone(), &image), + false, + ), + CustomCursorSource::Url { url, hotspot_x, hotspot_y } => Self::build_spawn( + event_loop, + from_url(UrlType::Plain(url), hotspot_x, hotspot_y), + false, + ), + CustomCursorSource::Animation { duration, cursors } => Self::build_spawn( + event_loop, + from_animation( + event_loop.runner.main_thread(), + duration, + cursors.into_iter().map(|cursor| cursor.inner), + ), + true, + ), + } + } + + fn build_spawn(window_target: &ActiveEventLoop, task: F, animation: bool) -> CustomCursor + where + F: 'static + Future>, + S: Into, + { + let handle = AbortHandle::new(); + let this = CustomCursor { + animation, + state: Arc::new(MainThreadSafe::new( + window_target.runner.main_thread(), + RefCell::new(ImageState::Loading { + notifier: Notifier::new(), + _handle: DropAbortHandle::new(handle.clone()), + }), + )), + }; + let weak = Arc::downgrade(&this.state); + let main_thread = window_target.runner.main_thread(); + + let task = Abortable::new(handle, { + async move { + let result = task.await; + + let this = weak.upgrade().expect("`CursorHandler` invalidated without aborting"); + let mut this = this.get(main_thread).borrow_mut(); + + match result { + Ok(new_state) => { + let ImageState::Loading { notifier, .. } = + mem::replace(this.deref_mut(), new_state.into()) + else { + unreachable!("found invalid state"); + }; + notifier.notify(Ok(())); + }, + Err(error) => { + let ImageState::Loading { notifier, .. } = + mem::replace(this.deref_mut(), ImageState::Failed(error.clone())) + else { + unreachable!("found invalid state"); + }; + notifier.notify(Err(error)); + }, + } + } + }); + + wasm_bindgen_futures::spawn_local(async move { + let _ = task.await; + }); + + this + } + + pub(crate) fn new_async( + event_loop: &ActiveEventLoop, + source: CustomCursorSource, + ) -> CustomCursorFuture { + let CustomCursor { animation, state } = Self::new(event_loop, source); + let binding = state.get(event_loop.runner.main_thread()).borrow(); + let ImageState::Loading { notifier, .. } = binding.deref() else { + unreachable!("found invalid state") + }; + let notified = notifier.notified(); + drop(binding); + + CustomCursorFuture { notified, animation, state: Some(state) } + } +} + +#[derive(Debug)] +pub struct CustomCursorFuture { + notified: Notified>, + animation: bool, + state: Option>>>, +} + +impl Future for CustomCursorFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.state.is_none() { + panic!("`CustomCursorFuture` polled after completion") + } + + let result = ready!(Pin::new(&mut self.notified).poll(cx)); + let state = self.state.take().expect("`CustomCursorFuture` polled after completion"); + + Poll::Ready(result.map(|_| CustomCursor { animation: self.animation, state })) + } +} + +#[derive(Debug)] +pub struct CursorHandler(Rc>); + +#[derive(Debug)] +struct Inner { + main_thread: MainThreadMarker, + canvas: HtmlCanvasElement, + style: Style, + visible: bool, + cursor: SelectedCursor, +} + +impl CursorHandler { + pub(crate) fn new( + main_thread: MainThreadMarker, + canvas: HtmlCanvasElement, + style: Style, + ) -> Self { + Self(Rc::new(RefCell::new(Inner { + main_thread, + canvas, + style, + visible: true, + cursor: SelectedCursor::default(), + }))) + } + + pub fn set_cursor(&self, cursor: Cursor) { + let mut this = self.0.borrow_mut(); + + match cursor { + Cursor::Icon(icon) => { + if let SelectedCursor::Icon(old_icon) + | SelectedCursor::Loading { previous: Previous::Icon(old_icon), .. } = + &this.cursor + { + if *old_icon == icon { + return; + } + } + + this.cursor = SelectedCursor::Icon(icon); + this.set_style(); + }, + Cursor::Custom(cursor) => { + let cursor = cursor.inner; + + if let SelectedCursor::Loading { cursor: old_cursor, .. } + | SelectedCursor::Image(old_cursor) + | SelectedCursor::Animation { cursor: old_cursor, .. } = &this.cursor + { + if *old_cursor == cursor { + return; + } + } + + let state = cursor.state.get(this.main_thread).borrow(); + + match state.deref() { + ImageState::Loading { notifier, .. } => { + let notified = notifier.notified(); + let handle = DropAbortHandle::new(AbortHandle::new()); + let task = Abortable::new(handle.handle(), { + let weak = Rc::downgrade(&self.0); + async move { + let _ = notified.await; + let handler = weak + .upgrade() + .expect("`CursorHandler` invalidated without aborting"); + handler.borrow_mut().notify(); + } + }); + wasm_bindgen_futures::spawn_local(async move { + let _ = task.await; + }); + + drop(state); + this.cursor = SelectedCursor::Loading { + cursor, + previous: mem::take(&mut this.cursor).into(), + _handle: handle, + }; + }, + ImageState::Failed(error) => { + tracing::error!( + "trying to load custom cursor that has failed to load: {error}" + ) + }, + ImageState::Image(_) => { + drop(state); + this.cursor = SelectedCursor::Image(cursor); + this.set_style(); + }, + ImageState::Animation(animation) => { + let canvas: &CanvasAnimateExt = this.canvas.unchecked_ref(); + let animation = canvas.animate_with_keyframe_animation_options( + Some(&animation.keyframes), + &animation.options, + ); + drop(state); + + if !this.visible { + animation.cancel(); + } + + this.cursor = SelectedCursor::Animation { + animation: AnimationDropper(animation), + cursor, + }; + this.set_style(); + }, + }; + }, + } + } + + pub fn set_cursor_visible(&self, visible: bool) { + let mut this = self.0.borrow_mut(); + + if !visible && this.visible { + this.visible = false; + this.style.set("cursor", "none"); + + if let SelectedCursor::Animation { animation, .. } = &this.cursor { + animation.0.cancel(); + } + } else if visible && !this.visible { + this.visible = true; + this.set_style(); + } + } +} + +impl Inner { + fn set_style(&self) { + if self.visible { + match &self.cursor { + SelectedCursor::Icon(icon) + | SelectedCursor::Loading { previous: Previous::Icon(icon), .. } => { + if let CursorIcon::Default = icon { + self.style.remove("cursor") + } else { + self.style.set("cursor", icon.name()) + } + }, + SelectedCursor::Loading { previous: Previous::Image(cursor), .. } + | SelectedCursor::Image(cursor) => { + match cursor.state.get(self.main_thread).borrow().deref() { + ImageState::Image(Image { style, .. }) => self.style.set("cursor", style), + _ => unreachable!("found invalid saved state"), + } + }, + SelectedCursor::Loading { + previous: Previous::Animation { animation, .. }, .. + } + | SelectedCursor::Animation { animation, .. } => { + self.style.remove("cursor"); + animation.0.play() + }, + } + } + } + + fn notify(&mut self) { + let SelectedCursor::Loading { cursor, previous, .. } = mem::take(&mut self.cursor) else { + unreachable!("found wrong state") + }; + + let state = cursor.state.get(self.main_thread).borrow(); + match state.deref() { + ImageState::Image(_) => { + drop(state); + self.cursor = SelectedCursor::Image(cursor); + self.set_style(); + }, + ImageState::Animation(animation) => { + let canvas: &CanvasAnimateExt = self.canvas.unchecked_ref(); + let animation = canvas.animate_with_keyframe_animation_options( + Some(&animation.keyframes), + &animation.options, + ); + drop(state); + + if !self.visible { + animation.cancel(); + } + + self.cursor = + SelectedCursor::Animation { animation: AnimationDropper(animation), cursor }; + self.set_style(); + }, + ImageState::Failed(error) => { + tracing::error!("custom cursor failed to load: {error}"); + self.cursor = previous.into() + }, + ImageState::Loading { .. } => unreachable!("notified without being ready"), + } + } +} + +#[derive(Debug)] +enum SelectedCursor { + Icon(CursorIcon), + Loading { cursor: CustomCursor, previous: Previous, _handle: DropAbortHandle }, + Image(CustomCursor), + Animation { cursor: CustomCursor, animation: AnimationDropper }, +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Icon(Default::default()) + } +} + +impl From for SelectedCursor { + fn from(previous: Previous) -> Self { + match previous { + Previous::Icon(icon) => Self::Icon(icon), + Previous::Image(cursor) => Self::Image(cursor), + Previous::Animation { cursor, animation } => Self::Animation { cursor, animation }, + } + } +} + +#[derive(Debug)] +enum Previous { + Icon(CursorIcon), + Image(CustomCursor), + Animation { cursor: CustomCursor, animation: AnimationDropper }, +} + +impl From for Previous { + fn from(value: SelectedCursor) -> Self { + match value { + SelectedCursor::Icon(icon) => Self::Icon(icon), + SelectedCursor::Loading { previous, .. } => previous, + SelectedCursor::Image(image) => Self::Image(image), + SelectedCursor::Animation { cursor, animation } => { + Self::Animation { cursor, animation } + }, + } + } +} + +#[derive(Debug)] +enum ImageState { + Loading { notifier: Notifier>, _handle: DropAbortHandle }, + Failed(CustomCursorError), + Image(Image), + Animation(Animation), +} + +#[derive(Debug)] +struct Image { + style: String, + _object_url: Option, + _image: HtmlImageElement, +} + +impl From for ImageState { + fn from(image: Image) -> Self { + Self::Image(image) + } +} + +#[derive(Debug)] +struct Animation { + keyframes: Array, + options: KeyframeAnimationOptions, + _images: Vec, +} + +impl From for ImageState { + fn from(animation: Animation) -> Self { + Self::Animation(animation) + } +} + +#[derive(Debug)] +enum UrlType { + Plain(String), + Object(ObjectUrl), +} + +impl UrlType { + fn url(&self) -> &str { + match &self { + UrlType::Plain(url) => url, + UrlType::Object(object_url) => &object_url.0, + } + } +} + +#[derive(Debug)] +struct ObjectUrl(String); + +impl Drop for ObjectUrl { + fn drop(&mut self) { + Url::revoke_object_url(&self.0).expect("unexpected exception in `URL.revokeObjectURL()`"); + } +} + +#[derive(Debug)] +struct AnimationDropper(WebAnimation); + +impl Drop for AnimationDropper { + fn drop(&mut self) { + self.0.cancel() + } +} + +fn from_rgba( + window: &Window, + document: Document, + image: &CursorImage, +) -> impl Future> { + // 1. Create an `ImageData` from the RGBA data. + // 2. Create an `ImageBitmap` from the `ImageData`. + // 3. Draw `ImageBitmap` on an `HTMLCanvasElement`. + // 4. Create a `Blob` from the `HTMLCanvasElement`. + // 5. Create an object URL from the `Blob`. + // 6. Decode the image on an `HTMLImageElement` from the URL. + + // 1. Create an `ImageData` from the RGBA data. + // Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223 + #[cfg(target_feature = "atomics")] + // Can't share `SharedArrayBuffer` with `ImageData`. + let result = { + use js_sys::{Uint8Array, Uint8ClampedArray}; + use wasm_bindgen::JsValue; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(js_namespace = ImageData)] + type ImageDataExt; + #[wasm_bindgen(catch, constructor, js_class = ImageData)] + fn new(array: Uint8ClampedArray, sw: u32) -> Result; + } + + let array = Uint8Array::new_with_length(image.rgba.len() as u32); + array.copy_from(&image.rgba); + let array = Uint8ClampedArray::new(&array); + ImageDataExt::new(array, image.width as u32) + .map(JsValue::from) + .map(ImageData::unchecked_from_js) + }; + #[cfg(not(target_feature = "atomics"))] + let result = ImageData::new_with_u8_clamped_array( + wasm_bindgen::Clamped(&image.rgba), + image.width as u32, + ); + let image_data = result.expect("found wrong image size"); + + // 2. Create an `ImageBitmap` from the `ImageData`. + // + // We call `createImageBitmap()` before spawning the future, + // to not have to clone the image buffer. + let options = ImageBitmapOptions::new(); + options.set_premultiply_alpha(PremultiplyAlpha::None); + let bitmap = JsFuture::from( + window + .create_image_bitmap_with_image_data_and_image_bitmap_options(&image_data, &options) + .expect("unexpected exception in `createImageBitmap()`"), + ); + + let CursorImage { width, height, hotspot_x, hotspot_y, .. } = *image; + async move { + let bitmap: ImageBitmap = + bitmap.await.expect("found invalid state in `ImageData`").unchecked_into(); + + let canvas: HtmlCanvasElement = + document.create_element("canvas").expect("invalid tag name").unchecked_into(); + #[allow(clippy::disallowed_methods)] + canvas.set_width(width as u32); + #[allow(clippy::disallowed_methods)] + canvas.set_height(height as u32); + + // 3. Draw `ImageBitmap` on an `HTMLCanvasElement`. + let context: ImageBitmapRenderingContext = canvas + .get_context("bitmaprenderer") + .expect("unexpected exception in `HTMLCanvasElement.getContext()`") + .expect("`bitmaprenderer` context unsupported") + .unchecked_into(); + context.transfer_from_image_bitmap(&bitmap); + drop(bitmap); + drop(context); + + // 4. Create a `Blob` from the `HTMLCanvasElement`. + // + // To keep the `Closure` alive until `HTMLCanvasElement.toBlob()` is done, + // we do the whole `Waker` strategy. Commonly on `Drop` the callback is aborted, + // but it would increase complexity and isn't possible in this case. + // Keep in mind that `HTMLCanvasElement.toBlob()` can call the callback immediately. + let value = Rc::new(RefCell::new(None)); + let waker = Rc::new(RefCell::>::new(None)); + let callback = Closure::once({ + let value = value.clone(); + let waker = waker.clone(); + move |blob: Option| { + *value.borrow_mut() = Some(blob); + if let Some(waker) = waker.borrow_mut().take() { + waker.wake(); + } + } + }); + canvas + .to_blob(callback.as_ref().unchecked_ref()) + .expect("failed with `SecurityError` despite only source coming from memory"); + let blob = future::poll_fn(|cx| { + if let Some(blob) = value.borrow_mut().take() { + Poll::Ready(blob) + } else { + *waker.borrow_mut() = Some(cx.waker().clone()); + Poll::Pending + } + }) + .await; + drop(canvas); + + let Some(blob) = blob else { + return Err(CustomCursorError::Blob); + }; + + // 5. Create an object URL from the `Blob`. + let url = Url::create_object_url_with_blob(&blob) + .expect("unexpected exception in `URL.createObjectURL()`"); + let url = UrlType::Object(ObjectUrl(url)); + + from_url(url, hotspot_x, hotspot_y).await + } +} + +async fn from_url( + url: UrlType, + hotspot_x: u16, + hotspot_y: u16, +) -> Result { + // 6. Decode the image on an `HTMLImageElement` from the URL. + let image = HtmlImageElement::new().expect("unexpected exception in `new HtmlImageElement`"); + image.set_src(url.url()); + let result = JsFuture::from(image.decode()).await; + + if let Err(error) = result { + debug_assert!(error.has_type::()); + let error: DomException = error.unchecked_into(); + debug_assert_eq!(error.name(), "EncodingError"); + let error = error.message(); + + return Err(CustomCursorError::Decode(error)); + } + + Ok(Image { + style: format!("url({}) {hotspot_x} {hotspot_y}, auto", url.url()), + _object_url: match url { + UrlType::Plain(_) => None, + UrlType::Object(object_url) => Some(object_url), + }, + _image: image, + }) +} + +#[allow(clippy::await_holding_refcell_ref)] // false-positive +async fn from_animation( + main_thread: MainThreadMarker, + duration: Duration, + cursors: impl ExactSizeIterator, +) -> Result { + let keyframes = Array::new(); + let mut images = Vec::with_capacity(cursors.len()); + + for cursor in cursors { + let state = cursor.state.get(main_thread).borrow(); + + match state.deref() { + ImageState::Loading { notifier, .. } => { + let notified = notifier.notified(); + drop(state); + notified.await?; + }, + ImageState::Failed(error) => return Err(error.clone()), + ImageState::Image(_) => drop(state), + ImageState::Animation(_) => unreachable!("check in `CustomCursorSource` failed"), + } + + let state = cursor.state.get(main_thread).borrow(); + let style = match state.deref() { + ImageState::Image(Image { style, .. }) => style, + _ => unreachable!("found invalid state"), + }; + + let keyframe: Keyframe = Object::new().unchecked_into(); + keyframe.set_cursor(style); + keyframes.push(&keyframe); + drop(state); + + images.push(cursor); + } + + keyframes.push(&keyframes.get(0)); + + let options: KeyframeAnimationOptions = Object::new().unchecked_into(); + options.set_duration(duration.as_millis() as f64); + options.set_iterations(f64::INFINITY); + + Ok(Animation { keyframes, options, _images: images }) +} + +#[wasm_bindgen] +extern "C" { + type CanvasAnimateExt; + + #[wasm_bindgen(method, js_name = animate)] + fn animate_with_keyframe_animation_options( + this: &CanvasAnimateExt, + keyframes: Option<&Object>, + options: &KeyframeAnimationOptions, + ) -> WebAnimation; + + #[derive(Debug)] + type WebAnimation; + + #[wasm_bindgen(method)] + fn cancel(this: &WebAnimation); + + #[wasm_bindgen(method)] + fn play(this: &WebAnimation); + + #[wasm_bindgen(extends = Object)] + type Keyframe; + + #[wasm_bindgen(method, setter, js_name = cursor)] + fn set_cursor(this: &Keyframe, value: &str); + + #[derive(Debug)] + #[wasm_bindgen(extends = Object)] + type KeyframeAnimationOptions; + + #[wasm_bindgen(method, setter, js_name = duration)] + fn set_duration(this: &KeyframeAnimationOptions, value: f64); + + #[wasm_bindgen(method, setter, js_name = iterations)] + fn set_iterations(this: &KeyframeAnimationOptions, value: f64); +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/device.rs b/third_party/winit-0.30.13/src/platform_impl/web/device.rs new file mode 100644 index 0000000..91d0858 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/device.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(pub i32); + +impl DeviceId { + pub const fn dummy() -> Self { + Self(0) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/error.rs b/third_party/winit-0.30.13/src/platform_impl/web/error.rs new file mode 100644 index 0000000..6995f2b --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/error.rs @@ -0,0 +1,10 @@ +use std::fmt; + +#[derive(Debug)] +pub struct OsError(pub String); + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/event_loop/mod.rs b/third_party/winit-0.30.13/src/platform_impl/web/event_loop/mod.rs new file mode 100644 index 0000000..23fe2e0 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/event_loop/mod.rs @@ -0,0 +1,119 @@ +use std::marker::PhantomData; +use std::sync::mpsc::{self, Receiver, Sender}; + +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::ActiveEventLoop as RootActiveEventLoop; +use crate::platform::web::{ActiveEventLoopExtWebSys, PollStrategy, WaitUntilStrategy}; + +use super::{backend, device, window}; + +mod proxy; +pub(crate) mod runner; +mod state; +mod window_target; + +pub(crate) use proxy::EventLoopProxy; +pub(crate) use window_target::{ActiveEventLoop, OwnedDisplayHandle}; + +pub struct EventLoop { + elw: RootActiveEventLoop, + user_event_sender: Sender, + user_event_receiver: Receiver, +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +impl EventLoop { + pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Result { + let (user_event_sender, user_event_receiver) = mpsc::channel(); + let elw = RootActiveEventLoop { p: ActiveEventLoop::new(), _marker: PhantomData }; + Ok(EventLoop { elw, user_event_sender, user_event_receiver }) + } + + pub fn run(self, mut event_handler: F) -> ! + where + F: FnMut(Event, &RootActiveEventLoop), + { + let target = RootActiveEventLoop { p: self.elw.p.clone(), _marker: PhantomData }; + + // SAFETY: Don't use `move` to make sure we leak the `event_handler` and `target`. + let handler: Box)> = Box::new(|event| { + let event = match event.map_nonuser_event() { + Ok(event) => event, + Err(Event::UserEvent(())) => Event::UserEvent( + self.user_event_receiver + .try_recv() + .expect("handler woken up without user event"), + ), + Err(_) => unreachable!(), + }; + event_handler(event, &target) + }); + // SAFETY: The `transmute` is necessary because `run()` requires `'static`. This is safe + // because this function will never return and all resources not cleaned up by the point we + // `throw` will leak, making this actually `'static`. + let handler = unsafe { + std::mem::transmute::)>, Box) + 'static>>( + handler, + ) + }; + self.elw.p.run(handler, false); + + // Throw an exception to break out of Rust execution and use unreachable to tell the + // compiler this function won't return, giving it a return type of '!' + backend::throw( + "Using exceptions for control flow, don't mind me. This isn't actually an error!", + ); + + unreachable!(); + } + + pub fn spawn(self, mut event_handler: F) + where + F: 'static + FnMut(Event, &RootActiveEventLoop), + { + let target = RootActiveEventLoop { p: self.elw.p.clone(), _marker: PhantomData }; + + self.elw.p.run( + Box::new(move |event| { + let event = match event.map_nonuser_event() { + Ok(event) => event, + Err(Event::UserEvent(())) => Event::UserEvent( + self.user_event_receiver + .try_recv() + .expect("handler woken up without user event"), + ), + Err(_) => unreachable!(), + }; + event_handler(event, &target) + }), + true, + ); + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.elw.p.waker(), self.user_event_sender.clone()) + } + + pub fn window_target(&self) -> &RootActiveEventLoop { + &self.elw + } + + pub fn set_poll_strategy(&self, strategy: PollStrategy) { + self.elw.set_poll_strategy(strategy); + } + + pub fn poll_strategy(&self) -> PollStrategy { + self.elw.poll_strategy() + } + + pub fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.elw.set_wait_until_strategy(strategy); + } + + pub fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.elw.wait_until_strategy() + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/event_loop/proxy.rs b/third_party/winit-0.30.13/src/platform_impl/web/event_loop/proxy.rs new file mode 100644 index 0000000..bd8e714 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/event_loop/proxy.rs @@ -0,0 +1,29 @@ +use std::rc::Weak; +use std::sync::mpsc::{SendError, Sender}; + +use super::runner::Execution; +use crate::event_loop::EventLoopClosed; +use crate::platform_impl::platform::r#async::Waker; + +pub struct EventLoopProxy { + runner: Waker>, + sender: Sender, +} + +impl EventLoopProxy { + pub fn new(runner: Waker>, sender: Sender) -> Self { + Self { runner, sender } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.sender.send(event).map_err(|SendError(event)| EventLoopClosed(event))?; + self.runner.wake(); + Ok(()) + } +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { runner: self.runner.clone(), sender: self.sender.clone() } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/event_loop/runner.rs b/third_party/winit-0.30.13/src/platform_impl/web/event_loop/runner.rs new file mode 100644 index 0000000..d9ba3d3 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/event_loop/runner.rs @@ -0,0 +1,808 @@ +use std::cell::{Cell, RefCell}; +use std::collections::{HashSet, VecDeque}; +use std::iter; +use std::num::NonZeroUsize; +use std::ops::Deref; +use std::rc::{Rc, Weak}; + +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::{Document, KeyboardEvent, PageTransitionEvent, PointerEvent, WheelEvent}; +use web_time::{Duration, Instant}; + +use super::super::main_thread::MainThreadMarker; +use super::super::DeviceId; +use super::backend; +use super::state::State; +use crate::dpi::PhysicalSize; +use crate::event::{ + DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, RawKeyEvent, StartCause, + WindowEvent, +}; +use crate::event_loop::{ControlFlow, DeviceEvents}; +use crate::platform::web::{PollStrategy, WaitUntilStrategy}; +use crate::platform_impl::platform::backend::EventListenerHandle; +use crate::platform_impl::platform::r#async::{DispatchRunner, Waker, WakerSpawner}; +use crate::platform_impl::platform::window::Inner; +use crate::window::WindowId; + +pub struct Shared(Rc); + +pub(super) type EventHandler = dyn FnMut(Event<()>); + +impl Clone for Shared { + fn clone(&self) -> Self { + Shared(self.0.clone()) + } +} + +type OnEventHandle = RefCell>>; + +pub struct Execution { + main_thread: MainThreadMarker, + proxy_spawner: WakerSpawner>, + control_flow: Cell, + poll_strategy: Cell, + wait_until_strategy: Cell, + exit: Cell, + runner: RefCell, + suspended: Cell, + event_loop_recreation: Cell, + events: RefCell>, + id: RefCell, + window: web_sys::Window, + document: Document, + #[allow(clippy::type_complexity)] + all_canvases: RefCell>, DispatchRunner)>>, + redraw_pending: RefCell>, + destroy_pending: RefCell>, + page_transition_event_handle: RefCell>, + device_events: Cell, + on_mouse_move: OnEventHandle, + on_wheel: OnEventHandle, + on_mouse_press: OnEventHandle, + on_mouse_release: OnEventHandle, + on_key_press: OnEventHandle, + on_key_release: OnEventHandle, + on_visibility_change: OnEventHandle, +} + +enum RunnerEnum { + /// The `EventLoop` is created but not being run. + Pending, + /// The `EventLoop` is being run. + Running(Runner), + /// The `EventLoop` is exited after being started with `EventLoop::run_app`. Since + /// `EventLoop::run_app` takes ownership of the `EventLoop`, we can be certain + /// that this event loop will never be run again. + Destroyed, +} + +impl RunnerEnum { + fn maybe_runner(&self) -> Option<&Runner> { + match self { + RunnerEnum::Running(runner) => Some(runner), + _ => None, + } + } +} + +struct Runner { + state: State, + event_handler: Box, +} + +impl Runner { + pub fn new(event_handler: Box) -> Self { + Runner { state: State::Init, event_handler } + } + + /// Returns the corresponding `StartCause` for the current `state`, or `None` + /// when in `Exit` state. + fn maybe_start_cause(&self) -> Option { + Some(match self.state { + State::Init => StartCause::Init, + State::Poll { .. } => StartCause::Poll, + State::Wait { start } => StartCause::WaitCancelled { start, requested_resume: None }, + State::WaitUntil { start, end, .. } => { + StartCause::WaitCancelled { start, requested_resume: Some(end) } + }, + State::Exit => return None, + }) + } + + fn handle_single_event(&mut self, runner: &Shared, event: impl Into) { + match event.into() { + EventWrapper::Event(event) => (self.event_handler)(event), + EventWrapper::ScaleChange { canvas, size, scale } => { + if let Some(canvas) = canvas.upgrade() { + canvas.borrow().handle_scale_change( + runner, + |event| (self.event_handler)(event), + size, + scale, + ) + } + }, + } + } +} + +impl Shared { + pub fn new() -> Self { + let main_thread = MainThreadMarker::new().expect("only callable from inside the `Window`"); + #[allow(clippy::disallowed_methods)] + let window = web_sys::window().expect("only callable from inside the `Window`"); + #[allow(clippy::disallowed_methods)] + let document = window.document().expect("Failed to obtain document"); + + Shared(Rc::::new_cyclic(|weak| { + let proxy_spawner = + WakerSpawner::new(main_thread, weak.clone(), |runner, count, local| { + if let Some(runner) = runner.upgrade() { + Shared(runner).send_user_events(count, local) + } + }) + .expect("`EventLoop` has to be created in the main thread"); + + Execution { + main_thread, + proxy_spawner, + control_flow: Cell::new(ControlFlow::default()), + poll_strategy: Cell::new(PollStrategy::default()), + wait_until_strategy: Cell::new(WaitUntilStrategy::default()), + exit: Cell::new(false), + runner: RefCell::new(RunnerEnum::Pending), + suspended: Cell::new(false), + event_loop_recreation: Cell::new(false), + events: RefCell::new(VecDeque::new()), + window, + document, + id: RefCell::new(0), + all_canvases: RefCell::new(Vec::new()), + redraw_pending: RefCell::new(HashSet::new()), + destroy_pending: RefCell::new(VecDeque::new()), + page_transition_event_handle: RefCell::new(None), + device_events: Cell::default(), + on_mouse_move: RefCell::new(None), + on_wheel: RefCell::new(None), + on_mouse_press: RefCell::new(None), + on_mouse_release: RefCell::new(None), + on_key_press: RefCell::new(None), + on_key_release: RefCell::new(None), + on_visibility_change: RefCell::new(None), + } + })) + } + + pub fn main_thread(&self) -> MainThreadMarker { + self.0.main_thread + } + + pub fn window(&self) -> &web_sys::Window { + &self.0.window + } + + pub fn document(&self) -> &Document { + &self.0.document + } + + pub fn add_canvas( + &self, + id: WindowId, + canvas: Weak>, + runner: DispatchRunner, + ) { + self.0.all_canvases.borrow_mut().push((id, canvas, runner)); + } + + pub fn notify_destroy_window(&self, id: WindowId) { + self.0.destroy_pending.borrow_mut().push_back(id); + } + + // Set the event callback to use for the event loop runner + // This the event callback is a fairly thin layer over the user-provided callback that closes + // over a RootActiveEventLoop reference + pub fn set_listener(&self, event_handler: Box) { + { + let mut runner = self.0.runner.borrow_mut(); + assert!(matches!(*runner, RunnerEnum::Pending)); + *runner = RunnerEnum::Running(Runner::new(event_handler)); + } + self.init(); + + *self.0.page_transition_event_handle.borrow_mut() = Some(backend::on_page_transition( + self.window().clone(), + { + let runner = self.clone(); + move |event: PageTransitionEvent| { + if event.persisted() { + runner.0.suspended.set(false); + runner.send_event(Event::Resumed); + } + } + }, + { + let runner = self.clone(); + move |event: PageTransitionEvent| { + runner.0.suspended.set(true); + if event.persisted() { + runner.send_event(Event::Suspended); + } else { + runner.handle_unload(); + } + } + }, + )); + + let runner = self.clone(); + let window = self.window().clone(); + *self.0.on_mouse_move.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "pointermove", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + // chorded button event + let device_id = RootDeviceId(DeviceId(event.pointer_id())); + + if let Some(button) = backend::event::mouse_button(&event) { + let state = if backend::event::mouse_buttons(&event).contains(button.into()) { + ElementState::Pressed + } else { + ElementState::Released + }; + + runner.send_event(Event::DeviceEvent { + device_id, + event: DeviceEvent::Button { button: button.to_id(), state }, + }); + + return; + } + + // pointer move event + let mut delta = backend::event::MouseDelta::init(&window, &event); + runner.send_events(backend::event::pointer_move_event(event).flat_map(|event| { + let delta = delta.delta(&event).to_physical(backend::scale_factor(&window)); + + let x_motion = (delta.x != 0.0).then_some(Event::DeviceEvent { + device_id, + event: DeviceEvent::Motion { axis: 0, value: delta.x }, + }); + + let y_motion = (delta.y != 0.0).then_some(Event::DeviceEvent { + device_id, + event: DeviceEvent::Motion { axis: 1, value: delta.y }, + }); + + x_motion.into_iter().chain(y_motion).chain(iter::once(Event::DeviceEvent { + device_id, + event: DeviceEvent::MouseMotion { delta: (delta.x, delta.y) }, + })) + })); + }), + )); + let runner = self.clone(); + let window = self.window().clone(); + *self.0.on_wheel.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "wheel", + Closure::new(move |event: WheelEvent| { + if !runner.device_events() { + return; + } + + if let Some(delta) = backend::event::mouse_scroll_delta(&window, &event) { + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(0)), + event: DeviceEvent::MouseWheel { delta }, + }); + } + }), + )); + let runner = self.clone(); + *self.0.on_mouse_press.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "pointerdown", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + let button = backend::event::mouse_button(&event).expect("no mouse button pressed"); + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(event.pointer_id())), + event: DeviceEvent::Button { + button: button.to_id(), + state: ElementState::Pressed, + }, + }); + }), + )); + let runner = self.clone(); + *self.0.on_mouse_release.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "pointerup", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + let button = backend::event::mouse_button(&event).expect("no mouse button pressed"); + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(event.pointer_id())), + event: DeviceEvent::Button { + button: button.to_id(), + state: ElementState::Released, + }, + }); + }), + )); + let runner = self.clone(); + *self.0.on_key_press.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "keydown", + Closure::new(move |event: KeyboardEvent| { + if !runner.device_events() { + return; + } + + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId::dummy()), + event: DeviceEvent::Key(RawKeyEvent { + physical_key: backend::event::key_code(&event), + state: ElementState::Pressed, + }), + }); + }), + )); + let runner = self.clone(); + *self.0.on_key_release.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "keyup", + Closure::new(move |event: KeyboardEvent| { + if !runner.device_events() { + return; + } + + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId::dummy()), + event: DeviceEvent::Key(RawKeyEvent { + physical_key: backend::event::key_code(&event), + state: ElementState::Released, + }), + }); + }), + )); + let runner = self.clone(); + *self.0.on_visibility_change.borrow_mut() = Some(EventListenerHandle::new( + // Safari <14 doesn't support the `visibilitychange` event on `Window`. + self.document().clone(), + "visibilitychange", + Closure::new(move |_| { + if !runner.0.suspended.get() { + for (id, canvas, _) in &*runner.0.all_canvases.borrow() { + if let Some(canvas) = canvas.upgrade() { + let is_visible = backend::is_visible(runner.document()); + // only fire if: + // - not visible and intersects + // - not visible and we don't know if it intersects yet + // - visible and intersects + if let (false, Some(true) | None) | (true, Some(true)) = + (is_visible, canvas.borrow().is_intersecting) + { + runner.send_event(Event::WindowEvent { + window_id: *id, + event: WindowEvent::Occluded(!is_visible), + }); + } + } + } + } + }), + )); + } + + // Generate a strictly increasing ID + // This is used to differentiate windows when handling events + pub fn generate_id(&self) -> u32 { + let mut id = self.0.id.borrow_mut(); + *id += 1; + + *id + } + + pub fn request_redraw(&self, id: WindowId) { + self.0.redraw_pending.borrow_mut().insert(id); + self.send_events::(iter::empty()); + } + + pub fn init(&self) { + // NB: For consistency all platforms must emit a 'resumed' event even though web + // applications don't themselves have a formal suspend/resume lifecycle. + self.run_until_cleared([Event::NewEvents(StartCause::Init), Event::Resumed].into_iter()); + } + + // Run the polling logic for the Poll ControlFlow, which involves clearing the queue + pub fn poll(&self) { + let start_cause = Event::NewEvents(StartCause::Poll); + self.run_until_cleared(iter::once(start_cause)); + } + + // Run the logic for waking from a WaitUntil, which involves clearing the queue + // Generally there shouldn't be events built up when this is called + pub fn resume_time_reached(&self, start: Instant, requested_resume: Instant) { + let start_cause = + Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume }); + self.run_until_cleared(iter::once(start_cause)); + } + + // Add an event to the event loop runner, from the user or an event handler + // + // It will determine if the event should be immediately sent to the user or buffered for later + pub(crate) fn send_event>(&self, event: E) { + self.send_events(iter::once(event)); + } + + // Add a series of user events to the event loop runner + // + // This will schedule the event loop to wake up instead of waking it up immediately if its not + // running. + pub(crate) fn send_user_events(&self, count: NonZeroUsize, local: bool) { + // If the event loop is closed, it should discard any new events + if self.is_closed() { + return; + } + + if local { + // If the loop is not running and triggered locally, queue on next microtick. + if let Ok(RunnerEnum::Running(_)) = + self.0.runner.try_borrow().as_ref().map(Deref::deref) + { + self.window().queue_microtask( + &Closure::once_into_js({ + let this = Rc::downgrade(&self.0); + move || { + if let Some(shared) = this.upgrade() { + Shared(shared).send_events( + iter::repeat(Event::UserEvent(())).take(count.get()), + ) + } + } + }) + .unchecked_into(), + ); + + return; + } + } + + self.send_events(iter::repeat(Event::UserEvent(())).take(count.get())) + } + + // Add a series of events to the event loop runner + // + // It will determine if the event should be immediately sent to the user or buffered for later + pub(crate) fn send_events>(&self, events: impl IntoIterator) { + // If the event loop is closed, it should discard any new events + if self.is_closed() { + return; + } + // If we can run the event processing right now, or need to queue this and wait for later + let mut process_immediately = true; + match self.0.runner.try_borrow().as_ref().map(Deref::deref) { + Ok(RunnerEnum::Running(ref runner)) => { + // If we're currently polling, queue this and wait for the poll() method to be + // called. + if let State::Poll { .. } = runner.state { + process_immediately = false; + } + }, + Ok(RunnerEnum::Pending) => { + // The runner still hasn't been attached: queue this event and wait for it to be + process_immediately = false; + }, + // Some other code is mutating the runner, which most likely means + // the event loop is running and busy. So we queue this event for + // it to be processed later. + Err(_) => { + process_immediately = false; + }, + // This is unreachable since `self.is_closed() == true`. + Ok(RunnerEnum::Destroyed) => unreachable!(), + } + if !process_immediately { + // Queue these events to look at later + self.0.events.borrow_mut().extend(events.into_iter().map(Into::into)); + return; + } + // At this point, we know this is a fresh set of events + // Now we determine why new events are incoming, and handle the events + let start_cause = match (self.0.runner.borrow().maybe_runner()) + .unwrap_or_else(|| { + unreachable!("The runner cannot process events when it is not attached") + }) + .maybe_start_cause() + { + Some(c) => c, + // If we're in the exit state, don't do event processing + None => return, + }; + // Take the start event, then the events provided to this function, and run an iteration of + // the event loop + let start_event = Event::NewEvents(start_cause); + let events = + iter::once(EventWrapper::from(start_event)).chain(events.into_iter().map(Into::into)); + self.run_until_cleared(events); + } + + // Process the destroy-pending windows. This should only be called from + // `run_until_cleared`, somewhere between emitting `NewEvents` and `AboutToWait`. + fn process_destroy_pending_windows(&self) { + while let Some(id) = self.0.destroy_pending.borrow_mut().pop_front() { + self.0.all_canvases.borrow_mut().retain(|&(item_id, ..)| item_id != id); + self.handle_event(Event::WindowEvent { + window_id: id, + event: crate::event::WindowEvent::Destroyed, + }); + self.0.redraw_pending.borrow_mut().remove(&id); + } + } + + // Given the set of new events, run the event loop until the main events and redraw events are + // cleared + // + // This will also process any events that have been queued or that are queued during processing + fn run_until_cleared>(&self, events: impl Iterator) { + for event in events { + self.handle_event(event.into()); + } + self.process_destroy_pending_windows(); + + // Collect all of the redraw events to avoid double-locking the RefCell + let redraw_events: Vec = self.0.redraw_pending.borrow_mut().drain().collect(); + for window_id in redraw_events { + self.handle_event(Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }); + } + + self.handle_event(Event::AboutToWait); + + self.apply_control_flow(); + // If the event loop is closed, it has been closed this iteration and now the closing + // event should be emitted + if self.is_closed() { + self.handle_loop_destroyed(); + } + } + + fn handle_unload(&self) { + self.exit(); + self.apply_control_flow(); + // We don't call `handle_loop_destroyed` here because we don't need to + // perform cleanup when the web browser is going to destroy the page. + self.handle_event(Event::LoopExiting); + } + + // handle_event takes in events and either queues them or applies a callback + // + // It should only ever be called from `run_until_cleared`. + fn handle_event(&self, event: impl Into) { + if self.is_closed() { + self.exit(); + } + match *self.0.runner.borrow_mut() { + RunnerEnum::Running(ref mut runner) => { + runner.handle_single_event(self, event); + }, + // If an event is being handled without a runner somehow, add it to the event queue so + // it will eventually be processed + RunnerEnum::Pending => self.0.events.borrow_mut().push_back(event.into()), + // If the Runner has been destroyed, there is nothing to do. + RunnerEnum::Destroyed => return, + } + + let is_closed = self.exiting(); + + // Don't take events out of the queue if the loop is closed or the runner doesn't exist + // If the runner doesn't exist and this method recurses, it will recurse infinitely + if !is_closed && self.0.runner.borrow().maybe_runner().is_some() { + // Pre-fetch window commands to avoid having to wait until the next event loop cycle + // and potentially block other threads in the meantime. + for (_, window, runner) in self.0.all_canvases.borrow().iter() { + if let Some(window) = window.upgrade() { + runner.run(); + drop(window) + } + } + + // Take an event out of the queue and handle it + // Make sure not to let the borrow_mut live during the next handle_event + let event = { + let mut events = self.0.events.borrow_mut(); + + // Pre-fetch `UserEvent`s to avoid having to wait until the next event loop cycle. + events.extend( + iter::repeat(Event::UserEvent(())) + .take(self.0.proxy_spawner.fetch()) + .map(EventWrapper::from), + ); + + events.pop_front() + }; + if let Some(event) = event { + self.handle_event(event); + } + } + } + + // Apply the new ControlFlow that has been selected by the user + // Start any necessary timeouts etc + fn apply_control_flow(&self) { + let new_state = if self.exiting() { + State::Exit + } else { + match self.control_flow() { + ControlFlow::Poll => { + let cloned = self.clone(); + State::Poll { + _request: backend::Schedule::new( + self.poll_strategy(), + self.window(), + move || cloned.poll(), + ), + } + }, + ControlFlow::Wait => State::Wait { start: Instant::now() }, + ControlFlow::WaitUntil(end) => { + let start = Instant::now(); + + let delay = if end <= start { Duration::from_millis(0) } else { end - start }; + + let cloned = self.clone(); + + State::WaitUntil { + start, + end, + _timeout: backend::Schedule::new_with_duration( + self.wait_until_strategy(), + self.window(), + move || cloned.resume_time_reached(start, end), + delay, + ), + } + }, + } + }; + + if let RunnerEnum::Running(ref mut runner) = *self.0.runner.borrow_mut() { + runner.state = new_state; + } + } + + fn handle_loop_destroyed(&self) { + self.handle_event(Event::LoopExiting); + let all_canvases = std::mem::take(&mut *self.0.all_canvases.borrow_mut()); + *self.0.page_transition_event_handle.borrow_mut() = None; + *self.0.on_mouse_move.borrow_mut() = None; + *self.0.on_wheel.borrow_mut() = None; + *self.0.on_mouse_press.borrow_mut() = None; + *self.0.on_mouse_release.borrow_mut() = None; + *self.0.on_key_press.borrow_mut() = None; + *self.0.on_key_release.borrow_mut() = None; + *self.0.on_visibility_change.borrow_mut() = None; + // Dropping the `Runner` drops the event handler closure, which will in + // turn drop all `Window`s moved into the closure. + *self.0.runner.borrow_mut() = RunnerEnum::Destroyed; + for (_, canvas, _) in all_canvases { + // In case any remaining `Window`s are still not dropped, we will need + // to explicitly remove the event handlers associated with their canvases. + if let Some(canvas) = canvas.upgrade() { + let mut canvas = canvas.borrow_mut(); + canvas.remove_listeners(); + } + } + // At this point, the `self.0` `Rc` should only be strongly referenced + // by the following: + // * `self`, i.e. the item which triggered this event loop wakeup, which is usually a + // `wasm-bindgen` `Closure`, which will be dropped after returning to the JS glue code. + // * The `ActiveEventLoop` leaked inside `EventLoop::run_app` due to the JS exception thrown + // at the end. + // * For each undropped `Window`: + // * The `register_redraw_request` closure. + // * The `destroy_fn` closure. + if self.0.event_loop_recreation.get() { + crate::event_loop::EventLoopBuilder::<()>::allow_event_loop_recreation(); + } + } + + // Check if the event loop is currently closed + fn is_closed(&self) -> bool { + match self.0.runner.try_borrow().as_ref().map(Deref::deref) { + Ok(RunnerEnum::Running(runner)) => runner.state.exiting(), + // The event loop is not closed since it is not initialized. + Ok(RunnerEnum::Pending) => false, + // The event loop is closed since it has been destroyed. + Ok(RunnerEnum::Destroyed) => true, + // Some other code is mutating the runner, which most likely means + // the event loop is running and busy. + Err(_) => false, + } + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.0.device_events.set(allowed) + } + + fn device_events(&self) -> bool { + match self.0.device_events.get() { + DeviceEvents::Always => true, + DeviceEvents::WhenFocused => { + self.0.all_canvases.borrow().iter().any(|(_, canvas, _)| { + if let Some(canvas) = canvas.upgrade() { + canvas.borrow().has_focus.get() + } else { + false + } + }) + }, + DeviceEvents::Never => false, + } + } + + pub fn event_loop_recreation(&self, allow: bool) { + self.0.event_loop_recreation.set(allow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.0.control_flow.get() + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.0.control_flow.set(control_flow) + } + + pub(crate) fn exit(&self) { + self.0.exit.set(true) + } + + pub(crate) fn exiting(&self) -> bool { + self.0.exit.get() + } + + pub(crate) fn set_poll_strategy(&self, strategy: PollStrategy) { + self.0.poll_strategy.set(strategy) + } + + pub(crate) fn poll_strategy(&self) -> PollStrategy { + self.0.poll_strategy.get() + } + + pub(crate) fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.0.wait_until_strategy.set(strategy) + } + + pub(crate) fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.0.wait_until_strategy.get() + } + + pub(crate) fn waker(&self) -> Waker> { + self.0.proxy_spawner.waker() + } +} + +pub(crate) enum EventWrapper { + Event(Event<()>), + ScaleChange { canvas: Weak>, size: PhysicalSize, scale: f64 }, +} + +impl From> for EventWrapper { + fn from(value: Event<()>) -> Self { + Self::Event(value) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/event_loop/state.rs b/third_party/winit-0.30.13/src/platform_impl/web/event_loop/state.rs new file mode 100644 index 0000000..d06e30e --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/event_loop/state.rs @@ -0,0 +1,18 @@ +use super::backend; + +use web_time::Instant; + +#[derive(Debug)] +pub enum State { + Init, + WaitUntil { _timeout: backend::Schedule, start: Instant, end: Instant }, + Wait { start: Instant }, + Poll { _request: backend::Schedule }, + Exit, +} + +impl State { + pub fn exiting(&self) -> bool { + matches!(self, State::Exit) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/event_loop/window_target.rs b/third_party/winit-0.30.13/src/platform_impl/web/event_loop/window_target.rs new file mode 100644 index 0000000..d741f09 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/event_loop/window_target.rs @@ -0,0 +1,685 @@ +use std::cell::{Cell, RefCell}; +use std::clone::Clone; +use std::collections::vec_deque::IntoIter as VecDequeIter; +use std::collections::VecDeque; +use std::iter; +use std::rc::{Rc, Weak}; + +use web_sys::Element; + +use super::super::monitor::MonitorHandle; +use super::super::KeyEventExtra; +use super::device::DeviceId; +use super::runner::{EventWrapper, Execution}; +use super::window::WindowId; +use super::{backend, runner}; +use crate::event::{ + DeviceId as RootDeviceId, ElementState, Event, KeyEvent, Touch, TouchPhase, WindowEvent, +}; +use crate::event_loop::{ControlFlow, DeviceEvents}; +use crate::keyboard::ModifiersState; +use crate::platform::web::{CustomCursorFuture, PollStrategy, WaitUntilStrategy}; +use crate::platform_impl::platform::cursor::CustomCursor; +use crate::platform_impl::platform::r#async::Waker; +use crate::window::{ + CustomCursor as RootCustomCursor, CustomCursorSource, Theme, WindowId as RootWindowId, +}; + +#[derive(Default)] +struct ModifiersShared(Rc>); + +impl ModifiersShared { + fn set(&self, new: ModifiersState) { + self.0.set(new) + } + + fn get(&self) -> ModifiersState { + self.0.get() + } +} + +impl Clone for ModifiersShared { + fn clone(&self) -> Self { + Self(Rc::clone(&self.0)) + } +} + +#[derive(Clone)] +pub struct ActiveEventLoop { + pub(crate) runner: runner::Shared, + modifiers: ModifiersShared, +} + +impl ActiveEventLoop { + pub fn new() -> Self { + Self { runner: runner::Shared::new(), modifiers: ModifiersShared::default() } + } + + pub fn run(&self, event_handler: Box, event_loop_recreation: bool) { + self.runner.event_loop_recreation(event_loop_recreation); + self.runner.set_listener(event_handler); + } + + pub fn generate_id(&self) -> WindowId { + WindowId(self.runner.generate_id()) + } + + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + RootCustomCursor { inner: CustomCursor::new(self, source.inner) } + } + + pub fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture { + CustomCursorFuture(CustomCursor::new_async(self, source.inner)) + } + + pub fn register(&self, canvas: &Rc>, id: WindowId) { + let canvas_clone = canvas.clone(); + let mut canvas = canvas.borrow_mut(); + #[cfg(any(feature = "rwh_04", feature = "rwh_05"))] + canvas.set_attribute("data-raw-handle", &id.0.to_string()); + + canvas.on_touch_start(); + + let runner = self.runner.clone(); + let has_focus = canvas.has_focus.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_blur(move || { + has_focus.set(false); + + let clear_modifiers = (!modifiers.get().is_empty()).then(|| { + modifiers.set(ModifiersState::empty()); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(ModifiersState::empty().into()), + } + }); + + runner.send_events(clear_modifiers.into_iter().chain(iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(false), + }))); + }); + + let runner = self.runner.clone(); + let has_focus = canvas.has_focus.clone(); + canvas.on_focus(move || { + if !has_focus.replace(true) { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(true), + }); + } + }); + + // It is possible that at this point the canvas has + // been focused before the callback can be called. + let focused = canvas + .document() + .active_element() + .filter(|element| { + let canvas: &Element = canvas.raw(); + element == canvas + }) + .is_some(); + + if focused { + canvas.has_focus.set(true); + self.runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(true), + }) + } + + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_keyboard_press( + move |physical_key, logical_key, text, location, repeat, active_modifiers| { + let modifiers_changed = (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id = RootDeviceId(DeviceId::dummy()); + + runner.send_events( + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id, + event: KeyEvent { + physical_key, + logical_key, + text, + location, + state: ElementState::Pressed, + repeat, + platform_specific: KeyEventExtra, + }, + is_synthetic: false, + }, + }) + .chain(modifiers_changed), + ); + }, + ); + + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_keyboard_release( + move |physical_key, logical_key, text, location, repeat, active_modifiers| { + let modifiers_changed = (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id = RootDeviceId(DeviceId::dummy()); + + runner.send_events( + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id, + event: KeyEvent { + physical_key, + logical_key, + text, + location, + state: ElementState::Released, + repeat, + platform_specific: KeyEventExtra, + }, + is_synthetic: false, + }, + }) + .chain(modifiers_changed), + ) + }, + ); + + let has_focus = canvas.has_focus.clone(); + canvas.on_cursor_leave({ + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id| { + let focus = (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let pointer = pointer_id.map(|pointer_id| Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorLeft { + device_id: RootDeviceId(DeviceId(pointer_id)), + }, + }); + + if focus.is_some() || pointer.is_some() { + runner.send_events(focus.into_iter().chain(pointer)) + } + } + }); + + canvas.on_cursor_enter({ + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id| { + let focus = (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let pointer = pointer_id.map(|pointer_id| Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorEntered { + device_id: RootDeviceId(DeviceId(pointer_id)), + }, + }); + + if focus.is_some() || pointer.is_some() { + runner.send_events(focus.into_iter().chain(pointer)) + } + } + }); + + canvas.on_cursor_move( + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id, events| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner.send_events(modifiers.into_iter().chain(events.flat_map(|position| { + let device_id = RootDeviceId(DeviceId(pointer_id)); + + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { device_id, position }, + }) + }))); + } + }, + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, device_id, events| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner.send_events(modifiers.into_iter().chain(events.map( + |(location, force)| Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Moved, + force: Some(force), + location, + }), + }, + ))); + } + }, + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, + pointer_id, + position: crate::dpi::PhysicalPosition, + buttons, + button| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id = RootDeviceId(DeviceId(pointer_id)); + + let state = if buttons.contains(button.into()) { + ElementState::Pressed + } else { + ElementState::Released + }; + + // A chorded button event may come in without any prior CursorMoved events, + // therefore we should send a CursorMoved event to make sure that the + // user code has the correct cursor position. + runner.send_events(modifiers.into_iter().chain([ + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { device_id, position }, + }, + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { device_id, state, button }, + }, + ])); + } + }, + ); + + canvas.on_mouse_press( + { + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id, position, button| { + let modifiers = (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id: RootDeviceId = RootDeviceId(DeviceId(pointer_id)); + + // A mouse down event may come in without any prior CursorMoved events, + // therefore we should send a CursorMoved event to make sure that the + // user code has the correct cursor position. + runner.send_events(modifiers.into_iter().chain([ + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { device_id, position }, + }, + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { + device_id, + state: ElementState::Pressed, + button, + }, + }, + ])); + } + }, + { + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, device_id, location, force| { + let modifiers = (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner.send_events(modifiers.into_iter().chain(iter::once( + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Started, + force: Some(force), + location, + }), + }, + ))) + } + }, + ); + + canvas.on_mouse_release( + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id, position, button| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id: RootDeviceId = RootDeviceId(DeviceId(pointer_id)); + + // A mouse up event may come in without any prior CursorMoved events, + // therefore we should send a CursorMoved event to make sure that the + // user code has the correct cursor position. + runner.send_events(modifiers.into_iter().chain([ + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { device_id, position }, + }, + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { + device_id, + state: ElementState::Released, + button, + }, + }, + ])); + } + }, + { + let runner_touch = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, device_id, location, force| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner_touch.send_events(modifiers.into_iter().chain(iter::once( + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Ended, + force: Some(force), + location, + }), + }, + ))); + } + }, + ); + + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_mouse_wheel(move |pointer_id, delta, active_modifiers| { + let modifiers_changed = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner.send_events(modifiers_changed.into_iter().chain(iter::once( + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseWheel { + device_id: RootDeviceId(DeviceId(pointer_id)), + delta, + phase: TouchPhase::Moved, + }, + }, + ))); + }); + + let runner = self.runner.clone(); + canvas.on_touch_cancel(move |device_id, location, force| { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Cancelled, + force: Some(force), + location, + }), + }); + }); + + let runner = self.runner.clone(); + canvas.on_dark_mode(move |is_dark_mode| { + let theme = if is_dark_mode { Theme::Dark } else { Theme::Light }; + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ThemeChanged(theme), + }); + }); + + canvas.on_resize_scale( + { + let runner = self.runner.clone(); + let canvas = canvas_clone.clone(); + + move |size, scale| { + runner.send_event(EventWrapper::ScaleChange { + canvas: Rc::downgrade(&canvas), + size, + scale, + }) + } + }, + { + let runner = self.runner.clone(); + let canvas = canvas_clone.clone(); + + move |new_size| { + let canvas = canvas.borrow(); + canvas.set_current_size(new_size); + if canvas.old_size() != new_size { + canvas.set_old_size(new_size); + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Resized(new_size), + }); + canvas.request_animation_frame(); + } + } + }, + ); + + let runner = self.runner.clone(); + canvas.on_intersection(move |is_intersecting| { + // only fire if visible while skipping the first event if it's intersecting + if backend::is_visible(runner.document()) + && !(is_intersecting && canvas_clone.borrow().is_intersecting.is_none()) + { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Occluded(!is_intersecting), + }); + } + + canvas_clone.borrow_mut().is_intersecting = Some(is_intersecting); + }); + + let runner = self.runner.clone(); + canvas.on_animation_frame(move || runner.request_redraw(RootWindowId(id))); + + canvas.on_context_menu(); + } + + pub fn available_monitors(&self) -> VecDequeIter { + VecDeque::new().into_iter() + } + + pub fn primary_monitor(&self) -> Option { + None + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Web(rwh_05::WebDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Web(rwh_06::WebDisplayHandle::new())) + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.runner.listen_device_events(allowed) + } + + pub fn system_theme(&self) -> Option { + backend::is_dark_mode(self.runner.window()).map(|is_dark_mode| { + if is_dark_mode { + Theme::Dark + } else { + Theme::Light + } + }) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.runner.set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.runner.control_flow() + } + + pub(crate) fn exit(&self) { + self.runner.exit() + } + + pub(crate) fn exiting(&self) -> bool { + self.runner.exiting() + } + + pub(crate) fn set_poll_strategy(&self, strategy: PollStrategy) { + self.runner.set_poll_strategy(strategy) + } + + pub(crate) fn poll_strategy(&self) -> PollStrategy { + self.runner.poll_strategy() + } + + pub(crate) fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.runner.set_wait_until_strategy(strategy) + } + + pub(crate) fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.runner.wait_until_strategy() + } + + pub(crate) fn waker(&self) -> Waker> { + self.runner.waker() + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::WebDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::WebDisplayHandle::new().into()) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/keyboard.rs b/third_party/winit-0.30.13/src/platform_impl/web/keyboard.rs new file mode 100644 index 0000000..6f8d69c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/keyboard.rs @@ -0,0 +1,522 @@ +use smol_str::SmolStr; + +use crate::keyboard::{Key, KeyCode, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub(crate) struct KeyEventExtra; + +impl Key { + pub(crate) fn from_key_attribute_value(kav: &str) -> Self { + Key::Named(match kav { + "Unidentified" => return Key::Unidentified(NativeKey::Web(SmolStr::new(kav))), + "Dead" => return Key::Dead(None), + "Alt" => NamedKey::Alt, + "AltGraph" => NamedKey::AltGraph, + "CapsLock" => NamedKey::CapsLock, + "Control" => NamedKey::Control, + "Fn" => NamedKey::Fn, + "FnLock" => NamedKey::FnLock, + "NumLock" => NamedKey::NumLock, + "ScrollLock" => NamedKey::ScrollLock, + "Shift" => NamedKey::Shift, + "Symbol" => NamedKey::Symbol, + "SymbolLock" => NamedKey::SymbolLock, + "Hyper" => NamedKey::Hyper, + "Meta" => NamedKey::Super, + "Enter" => NamedKey::Enter, + "Tab" => NamedKey::Tab, + " " => NamedKey::Space, + "ArrowDown" => NamedKey::ArrowDown, + "ArrowLeft" => NamedKey::ArrowLeft, + "ArrowRight" => NamedKey::ArrowRight, + "ArrowUp" => NamedKey::ArrowUp, + "End" => NamedKey::End, + "Home" => NamedKey::Home, + "PageDown" => NamedKey::PageDown, + "PageUp" => NamedKey::PageUp, + "Backspace" => NamedKey::Backspace, + "Clear" => NamedKey::Clear, + "Copy" => NamedKey::Copy, + "CrSel" => NamedKey::CrSel, + "Cut" => NamedKey::Cut, + "Delete" => NamedKey::Delete, + "EraseEof" => NamedKey::EraseEof, + "ExSel" => NamedKey::ExSel, + "Insert" => NamedKey::Insert, + "Paste" => NamedKey::Paste, + "Redo" => NamedKey::Redo, + "Undo" => NamedKey::Undo, + "Accept" => NamedKey::Accept, + "Again" => NamedKey::Again, + "Attn" => NamedKey::Attn, + "Cancel" => NamedKey::Cancel, + "ContextMenu" => NamedKey::ContextMenu, + "Escape" => NamedKey::Escape, + "Execute" => NamedKey::Execute, + "Find" => NamedKey::Find, + "Help" => NamedKey::Help, + "Pause" => NamedKey::Pause, + "Play" => NamedKey::Play, + "Props" => NamedKey::Props, + "Select" => NamedKey::Select, + "ZoomIn" => NamedKey::ZoomIn, + "ZoomOut" => NamedKey::ZoomOut, + "BrightnessDown" => NamedKey::BrightnessDown, + "BrightnessUp" => NamedKey::BrightnessUp, + "Eject" => NamedKey::Eject, + "LogOff" => NamedKey::LogOff, + "Power" => NamedKey::Power, + "PowerOff" => NamedKey::PowerOff, + "PrintScreen" => NamedKey::PrintScreen, + "Hibernate" => NamedKey::Hibernate, + "Standby" => NamedKey::Standby, + "WakeUp" => NamedKey::WakeUp, + "AllCandidates" => NamedKey::AllCandidates, + "Alphanumeric" => NamedKey::Alphanumeric, + "CodeInput" => NamedKey::CodeInput, + "Compose" => NamedKey::Compose, + "Convert" => NamedKey::Convert, + "FinalMode" => NamedKey::FinalMode, + "GroupFirst" => NamedKey::GroupFirst, + "GroupLast" => NamedKey::GroupLast, + "GroupNext" => NamedKey::GroupNext, + "GroupPrevious" => NamedKey::GroupPrevious, + "ModeChange" => NamedKey::ModeChange, + "NextCandidate" => NamedKey::NextCandidate, + "NonConvert" => NamedKey::NonConvert, + "PreviousCandidate" => NamedKey::PreviousCandidate, + "Process" => NamedKey::Process, + "SingleCandidate" => NamedKey::SingleCandidate, + "HangulMode" => NamedKey::HangulMode, + "HanjaMode" => NamedKey::HanjaMode, + "JunjaMode" => NamedKey::JunjaMode, + "Eisu" => NamedKey::Eisu, + "Hankaku" => NamedKey::Hankaku, + "Hiragana" => NamedKey::Hiragana, + "HiraganaKatakana" => NamedKey::HiraganaKatakana, + "KanaMode" => NamedKey::KanaMode, + "KanjiMode" => NamedKey::KanjiMode, + "Katakana" => NamedKey::Katakana, + "Romaji" => NamedKey::Romaji, + "Zenkaku" => NamedKey::Zenkaku, + "ZenkakuHankaku" => NamedKey::ZenkakuHankaku, + "Soft1" => NamedKey::Soft1, + "Soft2" => NamedKey::Soft2, + "Soft3" => NamedKey::Soft3, + "Soft4" => NamedKey::Soft4, + "ChannelDown" => NamedKey::ChannelDown, + "ChannelUp" => NamedKey::ChannelUp, + "Close" => NamedKey::Close, + "MailForward" => NamedKey::MailForward, + "MailReply" => NamedKey::MailReply, + "MailSend" => NamedKey::MailSend, + "MediaClose" => NamedKey::MediaClose, + "MediaFastForward" => NamedKey::MediaFastForward, + "MediaPause" => NamedKey::MediaPause, + "MediaPlay" => NamedKey::MediaPlay, + "MediaPlayPause" => NamedKey::MediaPlayPause, + "MediaRecord" => NamedKey::MediaRecord, + "MediaRewind" => NamedKey::MediaRewind, + "MediaStop" => NamedKey::MediaStop, + "MediaTrackNext" => NamedKey::MediaTrackNext, + "MediaTrackPrevious" => NamedKey::MediaTrackPrevious, + "New" => NamedKey::New, + "Open" => NamedKey::Open, + "Print" => NamedKey::Print, + "Save" => NamedKey::Save, + "SpellCheck" => NamedKey::SpellCheck, + "Key11" => NamedKey::Key11, + "Key12" => NamedKey::Key12, + "AudioBalanceLeft" => NamedKey::AudioBalanceLeft, + "AudioBalanceRight" => NamedKey::AudioBalanceRight, + "AudioBassBoostDown" => NamedKey::AudioBassBoostDown, + "AudioBassBoostToggle" => NamedKey::AudioBassBoostToggle, + "AudioBassBoostUp" => NamedKey::AudioBassBoostUp, + "AudioFaderFront" => NamedKey::AudioFaderFront, + "AudioFaderRear" => NamedKey::AudioFaderRear, + "AudioSurroundModeNext" => NamedKey::AudioSurroundModeNext, + "AudioTrebleDown" => NamedKey::AudioTrebleDown, + "AudioTrebleUp" => NamedKey::AudioTrebleUp, + "AudioVolumeDown" => NamedKey::AudioVolumeDown, + "AudioVolumeUp" => NamedKey::AudioVolumeUp, + "AudioVolumeMute" => NamedKey::AudioVolumeMute, + "MicrophoneToggle" => NamedKey::MicrophoneToggle, + "MicrophoneVolumeDown" => NamedKey::MicrophoneVolumeDown, + "MicrophoneVolumeUp" => NamedKey::MicrophoneVolumeUp, + "MicrophoneVolumeMute" => NamedKey::MicrophoneVolumeMute, + "SpeechCorrectionList" => NamedKey::SpeechCorrectionList, + "SpeechInputToggle" => NamedKey::SpeechInputToggle, + "LaunchApplication1" => NamedKey::LaunchApplication1, + "LaunchApplication2" => NamedKey::LaunchApplication2, + "LaunchCalendar" => NamedKey::LaunchCalendar, + "LaunchContacts" => NamedKey::LaunchContacts, + "LaunchMail" => NamedKey::LaunchMail, + "LaunchMediaPlayer" => NamedKey::LaunchMediaPlayer, + "LaunchMusicPlayer" => NamedKey::LaunchMusicPlayer, + "LaunchPhone" => NamedKey::LaunchPhone, + "LaunchScreenSaver" => NamedKey::LaunchScreenSaver, + "LaunchSpreadsheet" => NamedKey::LaunchSpreadsheet, + "LaunchWebBrowser" => NamedKey::LaunchWebBrowser, + "LaunchWebCam" => NamedKey::LaunchWebCam, + "LaunchWordProcessor" => NamedKey::LaunchWordProcessor, + "BrowserBack" => NamedKey::BrowserBack, + "BrowserFavorites" => NamedKey::BrowserFavorites, + "BrowserForward" => NamedKey::BrowserForward, + "BrowserHome" => NamedKey::BrowserHome, + "BrowserRefresh" => NamedKey::BrowserRefresh, + "BrowserSearch" => NamedKey::BrowserSearch, + "BrowserStop" => NamedKey::BrowserStop, + "AppSwitch" => NamedKey::AppSwitch, + "Call" => NamedKey::Call, + "Camera" => NamedKey::Camera, + "CameraFocus" => NamedKey::CameraFocus, + "EndCall" => NamedKey::EndCall, + "GoBack" => NamedKey::GoBack, + "GoHome" => NamedKey::GoHome, + "HeadsetHook" => NamedKey::HeadsetHook, + "LastNumberRedial" => NamedKey::LastNumberRedial, + "Notification" => NamedKey::Notification, + "MannerMode" => NamedKey::MannerMode, + "VoiceDial" => NamedKey::VoiceDial, + "TV" => NamedKey::TV, + "TV3DMode" => NamedKey::TV3DMode, + "TVAntennaCable" => NamedKey::TVAntennaCable, + "TVAudioDescription" => NamedKey::TVAudioDescription, + "TVAudioDescriptionMixDown" => NamedKey::TVAudioDescriptionMixDown, + "TVAudioDescriptionMixUp" => NamedKey::TVAudioDescriptionMixUp, + "TVContentsMenu" => NamedKey::TVContentsMenu, + "TVDataService" => NamedKey::TVDataService, + "TVInput" => NamedKey::TVInput, + "TVInputComponent1" => NamedKey::TVInputComponent1, + "TVInputComponent2" => NamedKey::TVInputComponent2, + "TVInputComposite1" => NamedKey::TVInputComposite1, + "TVInputComposite2" => NamedKey::TVInputComposite2, + "TVInputHDMI1" => NamedKey::TVInputHDMI1, + "TVInputHDMI2" => NamedKey::TVInputHDMI2, + "TVInputHDMI3" => NamedKey::TVInputHDMI3, + "TVInputHDMI4" => NamedKey::TVInputHDMI4, + "TVInputVGA1" => NamedKey::TVInputVGA1, + "TVMediaContext" => NamedKey::TVMediaContext, + "TVNetwork" => NamedKey::TVNetwork, + "TVNumberEntry" => NamedKey::TVNumberEntry, + "TVPower" => NamedKey::TVPower, + "TVRadioService" => NamedKey::TVRadioService, + "TVSatellite" => NamedKey::TVSatellite, + "TVSatelliteBS" => NamedKey::TVSatelliteBS, + "TVSatelliteCS" => NamedKey::TVSatelliteCS, + "TVSatelliteToggle" => NamedKey::TVSatelliteToggle, + "TVTerrestrialAnalog" => NamedKey::TVTerrestrialAnalog, + "TVTerrestrialDigital" => NamedKey::TVTerrestrialDigital, + "TVTimer" => NamedKey::TVTimer, + "AVRInput" => NamedKey::AVRInput, + "AVRPower" => NamedKey::AVRPower, + "ColorF0Red" => NamedKey::ColorF0Red, + "ColorF1Green" => NamedKey::ColorF1Green, + "ColorF2Yellow" => NamedKey::ColorF2Yellow, + "ColorF3Blue" => NamedKey::ColorF3Blue, + "ColorF4Grey" => NamedKey::ColorF4Grey, + "ColorF5Brown" => NamedKey::ColorF5Brown, + "ClosedCaptionToggle" => NamedKey::ClosedCaptionToggle, + "Dimmer" => NamedKey::Dimmer, + "DisplaySwap" => NamedKey::DisplaySwap, + "DVR" => NamedKey::DVR, + "Exit" => NamedKey::Exit, + "FavoriteClear0" => NamedKey::FavoriteClear0, + "FavoriteClear1" => NamedKey::FavoriteClear1, + "FavoriteClear2" => NamedKey::FavoriteClear2, + "FavoriteClear3" => NamedKey::FavoriteClear3, + "FavoriteRecall0" => NamedKey::FavoriteRecall0, + "FavoriteRecall1" => NamedKey::FavoriteRecall1, + "FavoriteRecall2" => NamedKey::FavoriteRecall2, + "FavoriteRecall3" => NamedKey::FavoriteRecall3, + "FavoriteStore0" => NamedKey::FavoriteStore0, + "FavoriteStore1" => NamedKey::FavoriteStore1, + "FavoriteStore2" => NamedKey::FavoriteStore2, + "FavoriteStore3" => NamedKey::FavoriteStore3, + "Guide" => NamedKey::Guide, + "GuideNextDay" => NamedKey::GuideNextDay, + "GuidePreviousDay" => NamedKey::GuidePreviousDay, + "Info" => NamedKey::Info, + "InstantReplay" => NamedKey::InstantReplay, + "Link" => NamedKey::Link, + "ListProgram" => NamedKey::ListProgram, + "LiveContent" => NamedKey::LiveContent, + "Lock" => NamedKey::Lock, + "MediaApps" => NamedKey::MediaApps, + "MediaAudioTrack" => NamedKey::MediaAudioTrack, + "MediaLast" => NamedKey::MediaLast, + "MediaSkipBackward" => NamedKey::MediaSkipBackward, + "MediaSkipForward" => NamedKey::MediaSkipForward, + "MediaStepBackward" => NamedKey::MediaStepBackward, + "MediaStepForward" => NamedKey::MediaStepForward, + "MediaTopMenu" => NamedKey::MediaTopMenu, + "NavigateIn" => NamedKey::NavigateIn, + "NavigateNext" => NamedKey::NavigateNext, + "NavigateOut" => NamedKey::NavigateOut, + "NavigatePrevious" => NamedKey::NavigatePrevious, + "NextFavoriteChannel" => NamedKey::NextFavoriteChannel, + "NextUserProfile" => NamedKey::NextUserProfile, + "OnDemand" => NamedKey::OnDemand, + "Pairing" => NamedKey::Pairing, + "PinPDown" => NamedKey::PinPDown, + "PinPMove" => NamedKey::PinPMove, + "PinPToggle" => NamedKey::PinPToggle, + "PinPUp" => NamedKey::PinPUp, + "PlaySpeedDown" => NamedKey::PlaySpeedDown, + "PlaySpeedReset" => NamedKey::PlaySpeedReset, + "PlaySpeedUp" => NamedKey::PlaySpeedUp, + "RandomToggle" => NamedKey::RandomToggle, + "RcLowBattery" => NamedKey::RcLowBattery, + "RecordSpeedNext" => NamedKey::RecordSpeedNext, + "RfBypass" => NamedKey::RfBypass, + "ScanChannelsToggle" => NamedKey::ScanChannelsToggle, + "ScreenModeNext" => NamedKey::ScreenModeNext, + "Settings" => NamedKey::Settings, + "SplitScreenToggle" => NamedKey::SplitScreenToggle, + "STBInput" => NamedKey::STBInput, + "STBPower" => NamedKey::STBPower, + "Subtitle" => NamedKey::Subtitle, + "Teletext" => NamedKey::Teletext, + "VideoModeNext" => NamedKey::VideoModeNext, + "Wink" => NamedKey::Wink, + "ZoomToggle" => NamedKey::ZoomToggle, + "F1" => NamedKey::F1, + "F2" => NamedKey::F2, + "F3" => NamedKey::F3, + "F4" => NamedKey::F4, + "F5" => NamedKey::F5, + "F6" => NamedKey::F6, + "F7" => NamedKey::F7, + "F8" => NamedKey::F8, + "F9" => NamedKey::F9, + "F10" => NamedKey::F10, + "F11" => NamedKey::F11, + "F12" => NamedKey::F12, + "F13" => NamedKey::F13, + "F14" => NamedKey::F14, + "F15" => NamedKey::F15, + "F16" => NamedKey::F16, + "F17" => NamedKey::F17, + "F18" => NamedKey::F18, + "F19" => NamedKey::F19, + "F20" => NamedKey::F20, + "F21" => NamedKey::F21, + "F22" => NamedKey::F22, + "F23" => NamedKey::F23, + "F24" => NamedKey::F24, + "F25" => NamedKey::F25, + "F26" => NamedKey::F26, + "F27" => NamedKey::F27, + "F28" => NamedKey::F28, + "F29" => NamedKey::F29, + "F30" => NamedKey::F30, + "F31" => NamedKey::F31, + "F32" => NamedKey::F32, + "F33" => NamedKey::F33, + "F34" => NamedKey::F34, + "F35" => NamedKey::F35, + string => return Key::Character(SmolStr::new(string)), + }) + } +} + +impl PhysicalKey { + pub fn from_key_code_attribute_value(kcav: &str) -> Self { + PhysicalKey::Code(match kcav { + "Backquote" => KeyCode::Backquote, + "Backslash" => KeyCode::Backslash, + "BracketLeft" => KeyCode::BracketLeft, + "BracketRight" => KeyCode::BracketRight, + "Comma" => KeyCode::Comma, + "Digit0" => KeyCode::Digit0, + "Digit1" => KeyCode::Digit1, + "Digit2" => KeyCode::Digit2, + "Digit3" => KeyCode::Digit3, + "Digit4" => KeyCode::Digit4, + "Digit5" => KeyCode::Digit5, + "Digit6" => KeyCode::Digit6, + "Digit7" => KeyCode::Digit7, + "Digit8" => KeyCode::Digit8, + "Digit9" => KeyCode::Digit9, + "Equal" => KeyCode::Equal, + "IntlBackslash" => KeyCode::IntlBackslash, + "IntlRo" => KeyCode::IntlRo, + "IntlYen" => KeyCode::IntlYen, + "KeyA" => KeyCode::KeyA, + "KeyB" => KeyCode::KeyB, + "KeyC" => KeyCode::KeyC, + "KeyD" => KeyCode::KeyD, + "KeyE" => KeyCode::KeyE, + "KeyF" => KeyCode::KeyF, + "KeyG" => KeyCode::KeyG, + "KeyH" => KeyCode::KeyH, + "KeyI" => KeyCode::KeyI, + "KeyJ" => KeyCode::KeyJ, + "KeyK" => KeyCode::KeyK, + "KeyL" => KeyCode::KeyL, + "KeyM" => KeyCode::KeyM, + "KeyN" => KeyCode::KeyN, + "KeyO" => KeyCode::KeyO, + "KeyP" => KeyCode::KeyP, + "KeyQ" => KeyCode::KeyQ, + "KeyR" => KeyCode::KeyR, + "KeyS" => KeyCode::KeyS, + "KeyT" => KeyCode::KeyT, + "KeyU" => KeyCode::KeyU, + "KeyV" => KeyCode::KeyV, + "KeyW" => KeyCode::KeyW, + "KeyX" => KeyCode::KeyX, + "KeyY" => KeyCode::KeyY, + "KeyZ" => KeyCode::KeyZ, + "Minus" => KeyCode::Minus, + "Period" => KeyCode::Period, + "Quote" => KeyCode::Quote, + "Semicolon" => KeyCode::Semicolon, + "Slash" => KeyCode::Slash, + "AltLeft" => KeyCode::AltLeft, + "AltRight" => KeyCode::AltRight, + "Backspace" => KeyCode::Backspace, + "CapsLock" => KeyCode::CapsLock, + "ContextMenu" => KeyCode::ContextMenu, + "ControlLeft" => KeyCode::ControlLeft, + "ControlRight" => KeyCode::ControlRight, + "Enter" => KeyCode::Enter, + "MetaLeft" => KeyCode::SuperLeft, + "MetaRight" => KeyCode::SuperRight, + "ShiftLeft" => KeyCode::ShiftLeft, + "ShiftRight" => KeyCode::ShiftRight, + "Space" => KeyCode::Space, + "Tab" => KeyCode::Tab, + "Convert" => KeyCode::Convert, + "KanaMode" => KeyCode::KanaMode, + "Lang1" => KeyCode::Lang1, + "Lang2" => KeyCode::Lang2, + "Lang3" => KeyCode::Lang3, + "Lang4" => KeyCode::Lang4, + "Lang5" => KeyCode::Lang5, + "NonConvert" => KeyCode::NonConvert, + "Delete" => KeyCode::Delete, + "End" => KeyCode::End, + "Help" => KeyCode::Help, + "Home" => KeyCode::Home, + "Insert" => KeyCode::Insert, + "PageDown" => KeyCode::PageDown, + "PageUp" => KeyCode::PageUp, + "ArrowDown" => KeyCode::ArrowDown, + "ArrowLeft" => KeyCode::ArrowLeft, + "ArrowRight" => KeyCode::ArrowRight, + "ArrowUp" => KeyCode::ArrowUp, + "NumLock" => KeyCode::NumLock, + "Numpad0" => KeyCode::Numpad0, + "Numpad1" => KeyCode::Numpad1, + "Numpad2" => KeyCode::Numpad2, + "Numpad3" => KeyCode::Numpad3, + "Numpad4" => KeyCode::Numpad4, + "Numpad5" => KeyCode::Numpad5, + "Numpad6" => KeyCode::Numpad6, + "Numpad7" => KeyCode::Numpad7, + "Numpad8" => KeyCode::Numpad8, + "Numpad9" => KeyCode::Numpad9, + "NumpadAdd" => KeyCode::NumpadAdd, + "NumpadBackspace" => KeyCode::NumpadBackspace, + "NumpadClear" => KeyCode::NumpadClear, + "NumpadClearEntry" => KeyCode::NumpadClearEntry, + "NumpadComma" => KeyCode::NumpadComma, + "NumpadDecimal" => KeyCode::NumpadDecimal, + "NumpadDivide" => KeyCode::NumpadDivide, + "NumpadEnter" => KeyCode::NumpadEnter, + "NumpadEqual" => KeyCode::NumpadEqual, + "NumpadHash" => KeyCode::NumpadHash, + "NumpadMemoryAdd" => KeyCode::NumpadMemoryAdd, + "NumpadMemoryClear" => KeyCode::NumpadMemoryClear, + "NumpadMemoryRecall" => KeyCode::NumpadMemoryRecall, + "NumpadMemoryStore" => KeyCode::NumpadMemoryStore, + "NumpadMemorySubtract" => KeyCode::NumpadMemorySubtract, + "NumpadMultiply" => KeyCode::NumpadMultiply, + "NumpadParenLeft" => KeyCode::NumpadParenLeft, + "NumpadParenRight" => KeyCode::NumpadParenRight, + "NumpadStar" => KeyCode::NumpadStar, + "NumpadSubtract" => KeyCode::NumpadSubtract, + "Escape" => KeyCode::Escape, + "Fn" => KeyCode::Fn, + "FnLock" => KeyCode::FnLock, + "PrintScreen" => KeyCode::PrintScreen, + "ScrollLock" => KeyCode::ScrollLock, + "Pause" => KeyCode::Pause, + "BrowserBack" => KeyCode::BrowserBack, + "BrowserFavorites" => KeyCode::BrowserFavorites, + "BrowserForward" => KeyCode::BrowserForward, + "BrowserHome" => KeyCode::BrowserHome, + "BrowserRefresh" => KeyCode::BrowserRefresh, + "BrowserSearch" => KeyCode::BrowserSearch, + "BrowserStop" => KeyCode::BrowserStop, + "Eject" => KeyCode::Eject, + "LaunchApp1" => KeyCode::LaunchApp1, + "LaunchApp2" => KeyCode::LaunchApp2, + "LaunchMail" => KeyCode::LaunchMail, + "MediaPlayPause" => KeyCode::MediaPlayPause, + "MediaSelect" => KeyCode::MediaSelect, + "MediaStop" => KeyCode::MediaStop, + "MediaTrackNext" => KeyCode::MediaTrackNext, + "MediaTrackPrevious" => KeyCode::MediaTrackPrevious, + "Power" => KeyCode::Power, + "Sleep" => KeyCode::Sleep, + "AudioVolumeDown" => KeyCode::AudioVolumeDown, + "AudioVolumeMute" => KeyCode::AudioVolumeMute, + "AudioVolumeUp" => KeyCode::AudioVolumeUp, + "WakeUp" => KeyCode::WakeUp, + "Hyper" => KeyCode::Hyper, + "Turbo" => KeyCode::Turbo, + "Abort" => KeyCode::Abort, + "Resume" => KeyCode::Resume, + "Suspend" => KeyCode::Suspend, + "Again" => KeyCode::Again, + "Copy" => KeyCode::Copy, + "Cut" => KeyCode::Cut, + "Find" => KeyCode::Find, + "Open" => KeyCode::Open, + "Paste" => KeyCode::Paste, + "Props" => KeyCode::Props, + "Select" => KeyCode::Select, + "Undo" => KeyCode::Undo, + "Hiragana" => KeyCode::Hiragana, + "Katakana" => KeyCode::Katakana, + "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, + _ => return PhysicalKey::Unidentified(NativeKeyCode::Unidentified), + }) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/main_thread.rs b/third_party/winit-0.30.13/src/platform_impl/web/main_thread.rs new file mode 100644 index 0000000..59a2ac5 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/main_thread.rs @@ -0,0 +1,96 @@ +use std::fmt::{self, Debug, Formatter}; +use std::marker::PhantomData; +use std::mem; +use std::sync::OnceLock; + +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; + +use super::r#async::{self, Sender}; + +thread_local! { + static MAIN_THREAD: bool = { + #[wasm_bindgen] + extern "C" { + #[derive(Clone)] + type Global; + + #[wasm_bindgen(method, getter, js_name = Window)] + fn window(this: &Global) -> JsValue; + } + + let global: Global = js_sys::global().unchecked_into(); + !global.window().is_undefined() + }; +} + +#[derive(Clone, Copy, Debug)] +pub struct MainThreadMarker(PhantomData<*const ()>); + +impl MainThreadMarker { + pub fn new() -> Option { + MAIN_THREAD.with(|is| is.then_some(Self(PhantomData))) + } +} + +pub struct MainThreadSafe(Option); + +impl MainThreadSafe { + pub fn new(_: MainThreadMarker, value: T) -> Self { + DROP_HANDLER.get_or_init(|| { + let (sender, receiver) = r#async::channel(); + wasm_bindgen_futures::spawn_local( + async move { while receiver.next().await.is_ok() {} }, + ); + + sender + }); + + Self(Some(value)) + } + + pub fn into_inner(mut self, _: MainThreadMarker) -> T { + self.0.take().expect("already taken or dropped") + } + + pub fn get(&self, _: MainThreadMarker) -> &T { + self.0.as_ref().expect("already taken or dropped") + } +} + +impl Debug for MainThreadSafe { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if MainThreadMarker::new().is_some() { + f.debug_tuple("MainThreadSafe").field(&self.0).finish() + } else { + f.debug_struct("MainThreadSafe").finish_non_exhaustive() + } + } +} + +impl Drop for MainThreadSafe { + fn drop(&mut self) { + if let Some(value) = self.0.take() { + if mem::needs_drop::() && MainThreadMarker::new().is_none() { + DROP_HANDLER + .get() + .expect("drop handler not initialized when setting canvas") + .send(DropBox(Box::new(value))) + .expect("sender dropped in main thread") + } + } + } +} + +unsafe impl Send for MainThreadSafe {} +unsafe impl Sync for MainThreadSafe {} + +static DROP_HANDLER: OnceLock> = OnceLock::new(); + +struct DropBox(#[allow(dead_code)] Box); + +unsafe impl Send for DropBox {} +unsafe impl Sync for DropBox {} + +trait Any {} +impl Any for T {} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/mod.rs b/third_party/winit-0.30.13/src/platform_impl/web/mod.rs new file mode 100644 index 0000000..969d8bb --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/mod.rs @@ -0,0 +1,50 @@ +// Brief introduction to the internals of the web backend: +// The web backend used to support both wasm-bindgen and stdweb as methods of binding to the +// environment. Because they are both supporting the same underlying APIs, the actual web bindings +// are cordoned off into backend abstractions, which present the thinnest unifying layer possible. +// +// When adding support for new events or interactions with the browser, first consult trusted +// documentation (such as MDN) to ensure it is well-standardised and supported across many browsers. +// Once you have decided on the relevant web APIs, add support to both backends. +// +// The backend is used by the rest of the module to implement Winit's business logic, which forms +// the rest of the code. 'device', 'error', 'monitor', and 'window' define web-specific structures +// for winit's cross-platform structures. They are all relatively simple translations. +// +// The event_loop module handles listening for and processing events. 'Proxy' implements +// EventLoopProxy and 'WindowTarget' implements ActiveEventLoop. WindowTarget also handles +// registering the event handlers. The 'Execution' struct in the 'runner' module handles taking +// incoming events (from the registered handlers) and ensuring they are passed to the user in a +// compliant way. + +// TODO: FP, remove when is fixed. +#![allow(clippy::empty_docs)] + +mod r#async; +mod cursor; +mod device; +mod error; +mod event_loop; +mod keyboard; +mod main_thread; +mod monitor; +mod web_sys; +mod window; + +pub use self::device::DeviceId; +pub use self::error::OsError; +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub use self::monitor::{MonitorHandle, VideoModeHandle}; +pub use self::window::{PlatformSpecificWindowAttributes, Window, WindowId}; + +pub(crate) use self::keyboard::KeyEventExtra; +use self::web_sys as backend; +pub(crate) use crate::icon::NoIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; +pub(crate) use cursor::{ + CustomCursor as PlatformCustomCursor, CustomCursorFuture, + CustomCursorSource as PlatformCustomCursorSource, +}; diff --git a/third_party/winit-0.30.13/src/platform_impl/web/monitor.rs b/third_party/winit-0.30.13/src/platform_impl/web/monitor.rs new file mode 100644 index 0000000..1870284 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/monitor.rs @@ -0,0 +1,53 @@ +use std::iter::Empty; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MonitorHandle; + +impl MonitorHandle { + pub fn scale_factor(&self) -> f64 { + unreachable!() + } + + pub fn position(&self) -> PhysicalPosition { + unreachable!() + } + + pub fn name(&self) -> Option { + unreachable!() + } + + pub fn refresh_rate_millihertz(&self) -> Option { + unreachable!() + } + + pub fn size(&self) -> PhysicalSize { + unreachable!() + } + + pub fn video_modes(&self) -> Empty { + unreachable!() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct VideoModeHandle; + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + unreachable!(); + } + + pub fn bit_depth(&self) -> u16 { + unreachable!(); + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + unreachable!(); + } + + pub fn monitor(&self) -> MonitorHandle { + unreachable!(); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/animation_frame.rs b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/animation_frame.rs new file mode 100644 index 0000000..17aa8f6 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/animation_frame.rs @@ -0,0 +1,60 @@ +use std::cell::Cell; +use std::rc::Rc; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; + +pub struct AnimationFrameHandler { + window: web_sys::Window, + closure: Closure, + handle: Rc>>, +} + +impl AnimationFrameHandler { + pub fn new(window: web_sys::Window) -> Self { + let handle = Rc::new(Cell::new(None)); + let closure = Closure::new({ + let handle = handle.clone(); + move || handle.set(None) + }); + + Self { window, closure, handle } + } + + pub fn on_animation_frame(&mut self, mut f: F) + where + F: 'static + FnMut(), + { + let handle = self.handle.clone(); + self.closure = Closure::new(move || { + handle.set(None); + f(); + }) + } + + pub fn request(&self) { + if let Some(handle) = self.handle.take() { + self.window.cancel_animation_frame(handle).expect("Failed to cancel animation frame"); + } + + let handle = self + .window + .request_animation_frame(self.closure.as_ref().unchecked_ref()) + .expect("Failed to request animation frame"); + + self.handle.set(Some(handle)); + } + + pub fn cancel(&mut self) { + if let Some(handle) = self.handle.take() { + self.window.cancel_animation_frame(handle).expect("Failed to cancel animation frame"); + } + } +} + +impl Drop for AnimationFrameHandler { + fn drop(&mut self) { + if let Some(handle) = self.handle.take() { + self.window.cancel_animation_frame(handle).expect("Failed to cancel animation frame"); + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/canvas.rs b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/canvas.rs new file mode 100644 index 0000000..84dde9f --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/canvas.rs @@ -0,0 +1,565 @@ +use std::cell::Cell; +use std::ops::Deref; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; + +use smol_str::SmolStr; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; +use web_sys::{ + CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, + PointerEvent, WheelEvent, +}; + +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; +use crate::error::OsError as RootOE; +use crate::event::{Force, InnerSizeWriter, MouseButton, MouseScrollDelta}; +use crate::keyboard::{Key, KeyLocation, ModifiersState, PhysicalKey}; +use crate::platform_impl::OsError; +use crate::window::{WindowAttributes, WindowId as RootWindowId}; + +use super::super::cursor::CursorHandler; +use super::super::main_thread::MainThreadMarker; +use super::super::WindowId; +use super::animation_frame::AnimationFrameHandler; +use super::event_handle::EventListenerHandle; +use super::intersection_handle::IntersectionObserverHandle; +use super::media_query_handle::MediaQueryListHandle; +use super::pointer::PointerHandler; +use super::{event, fullscreen, ButtonsState, ResizeScaleHandle}; + +#[allow(dead_code)] +pub struct Canvas { + common: Common, + id: WindowId, + pub has_focus: Rc>, + pub prevent_default: Rc>, + pub is_intersecting: Option, + on_touch_start: Option>, + on_focus: Option>, + on_blur: Option>, + on_keyboard_release: Option>, + on_keyboard_press: Option>, + on_mouse_wheel: Option>, + on_dark_mode: Option, + pointer_handler: PointerHandler, + on_resize_scale: Option, + on_intersect: Option, + animation_frame_handler: AnimationFrameHandler, + on_touch_end: Option>, + on_context_menu: Option>, + pub cursor: CursorHandler, +} + +pub struct Common { + pub window: web_sys::Window, + pub document: Document, + /// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure + /// the DPI factor is maintained. Note: this is read-only because we use a pointer to this + /// for [`WindowHandle`][rwh_06::WindowHandle]. + raw: Rc, + style: Style, + old_size: Rc>>, + current_size: Rc>>, +} + +#[derive(Clone, Debug)] +pub struct Style { + read: CssStyleDeclaration, + write: CssStyleDeclaration, +} + +impl Canvas { + pub(crate) fn create( + main_thread: MainThreadMarker, + id: WindowId, + window: web_sys::Window, + document: Document, + attr: &mut WindowAttributes, + ) -> Result { + let canvas = match attr.platform_specific.canvas.take().map(|canvas| { + Arc::try_unwrap(canvas) + .map(|canvas| canvas.into_inner(main_thread)) + .unwrap_or_else(|canvas| canvas.get(main_thread).clone()) + }) { + Some(canvas) => canvas, + None => document + .create_element("canvas") + .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))? + .unchecked_into(), + }; + + if attr.platform_specific.append && !document.contains(Some(&canvas)) { + document + .body() + .expect("Failed to get body from document") + .append_child(&canvas) + .expect("Failed to append canvas to body"); + } + + // A tabindex is needed in order to capture local keyboard events. + // A "0" value means that the element should be focusable in + // sequential keyboard navigation, but its order is defined by the + // document's source order. + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex + if attr.platform_specific.focusable { + canvas + .set_attribute("tabindex", "0") + .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; + } + + let style = Style::new(&window, &canvas); + + let cursor = CursorHandler::new(main_thread, canvas.clone(), style.clone()); + + let common = Common { + window: window.clone(), + document: document.clone(), + raw: Rc::new(canvas.clone()), + style, + old_size: Rc::default(), + current_size: Rc::default(), + }; + + if let Some(size) = attr.inner_size { + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_size(&common.document, &common.raw, &common.style, size); + } + + if let Some(size) = attr.min_inner_size { + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_min_size(&common.document, &common.raw, &common.style, Some(size)); + } + + if let Some(size) = attr.max_inner_size { + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_max_size(&common.document, &common.raw, &common.style, Some(size)); + } + + if let Some(position) = attr.position { + let position = position.to_logical(super::scale_factor(&common.window)); + super::set_canvas_position(&common.document, &common.raw, &common.style, position); + } + + if attr.fullscreen.is_some() { + fullscreen::request_fullscreen(&document, &canvas); + } + + if attr.active { + let _ = common.raw.focus(); + } + + Ok(Canvas { + common, + id, + has_focus: Rc::new(Cell::new(false)), + prevent_default: Rc::new(Cell::new(attr.platform_specific.prevent_default)), + is_intersecting: None, + on_touch_start: None, + on_blur: None, + on_focus: None, + on_keyboard_release: None, + on_keyboard_press: None, + on_mouse_wheel: None, + on_dark_mode: None, + pointer_handler: PointerHandler::new(), + on_resize_scale: None, + on_intersect: None, + animation_frame_handler: AnimationFrameHandler::new(window), + on_touch_end: None, + on_context_menu: None, + cursor, + }) + } + + pub fn set_cursor_lock(&self, lock: bool) -> Result<(), RootOE> { + if lock { + self.raw().request_pointer_lock(); + } else { + self.common.document.exit_pointer_lock(); + } + Ok(()) + } + + pub fn set_attribute(&self, attribute: &str, value: &str) { + self.common + .raw + .set_attribute(attribute, value) + .unwrap_or_else(|err| panic!("error: {err:?}\nSet attribute: {attribute}")) + } + + pub fn position(&self) -> LogicalPosition { + let bounds = self.common.raw.get_bounding_client_rect(); + let mut position = LogicalPosition { x: bounds.x(), y: bounds.y() }; + + if self.document().contains(Some(self.raw())) && self.style().get("display") != "none" { + position.x += super::style_size_property(self.style(), "border-left-width") + + super::style_size_property(self.style(), "padding-left"); + position.y += super::style_size_property(self.style(), "border-top-width") + + super::style_size_property(self.style(), "padding-top"); + } + + position + } + + #[inline] + pub fn old_size(&self) -> PhysicalSize { + self.common.old_size.get() + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + self.common.current_size.get() + } + + #[inline] + pub fn set_old_size(&self, size: PhysicalSize) { + self.common.old_size.set(size) + } + + #[inline] + pub fn set_current_size(&self, size: PhysicalSize) { + self.common.current_size.set(size) + } + + #[inline] + pub fn window(&self) -> &web_sys::Window { + &self.common.window + } + + #[inline] + pub fn document(&self) -> &Document { + &self.common.document + } + + #[inline] + pub fn raw(&self) -> &HtmlCanvasElement { + &self.common.raw + } + + #[inline] + pub fn style(&self) -> &Style { + &self.common.style + } + + pub fn on_touch_start(&mut self) { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_touch_start = Some(self.common.add_event("touchstart", move |event: Event| { + if prevent_default.get() { + event.prevent_default(); + } + })); + } + + pub fn on_blur(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_blur = Some(self.common.add_event("blur", move |_: FocusEvent| { + handler(); + })); + } + + pub fn on_focus(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_focus = Some(self.common.add_event("focus", move |_: FocusEvent| { + handler(); + })); + } + + pub fn on_keyboard_release(&mut self, mut handler: F) + where + F: 'static + FnMut(PhysicalKey, Key, Option, KeyLocation, bool, ModifiersState), + { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_keyboard_release = + Some(self.common.add_event("keyup", move |event: KeyboardEvent| { + if prevent_default.get() { + event.prevent_default(); + } + let key = event::key(&event); + let modifiers = event::keyboard_modifiers(&event); + handler( + event::key_code(&event), + key, + event::key_text(&event), + event::key_location(&event), + event.repeat(), + modifiers, + ); + })); + } + + pub fn on_keyboard_press(&mut self, mut handler: F) + where + F: 'static + FnMut(PhysicalKey, Key, Option, KeyLocation, bool, ModifiersState), + { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_keyboard_press = + Some(self.common.add_event("keydown", move |event: KeyboardEvent| { + if prevent_default.get() { + event.prevent_default(); + } + let key = event::key(&event); + let modifiers = event::keyboard_modifiers(&event); + handler( + event::key_code(&event), + key, + event::key_text(&event), + event::key_location(&event), + event.repeat(), + modifiers, + ); + })); + } + + pub fn on_cursor_leave(&mut self, handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.pointer_handler.on_cursor_leave(&self.common, handler) + } + + pub fn on_cursor_enter(&mut self, handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.pointer_handler.on_cursor_enter(&self.common, handler) + } + + pub fn on_mouse_release(&mut self, mouse_handler: M, touch_handler: T) + where + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + self.pointer_handler.on_mouse_release(&self.common, mouse_handler, touch_handler) + } + + pub fn on_mouse_press(&mut self, mouse_handler: M, touch_handler: T) + where + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + self.pointer_handler.on_mouse_press( + &self.common, + mouse_handler, + touch_handler, + Rc::clone(&self.prevent_default), + ) + } + + pub fn on_cursor_move(&mut self, mouse_handler: M, touch_handler: T, button_handler: B) + where + M: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator>), + T: 'static + + FnMut(ModifiersState, i32, &mut dyn Iterator, Force)>), + B: 'static + FnMut(ModifiersState, i32, PhysicalPosition, ButtonsState, MouseButton), + { + self.pointer_handler.on_cursor_move( + &self.common, + mouse_handler, + touch_handler, + button_handler, + Rc::clone(&self.prevent_default), + ) + } + + pub fn on_touch_cancel(&mut self, handler: F) + where + F: 'static + FnMut(i32, PhysicalPosition, Force), + { + self.pointer_handler.on_touch_cancel(&self.common, handler) + } + + pub fn on_mouse_wheel(&mut self, mut handler: F) + where + F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState), + { + let window = self.common.window.clone(); + let prevent_default = Rc::clone(&self.prevent_default); + self.on_mouse_wheel = Some(self.common.add_event("wheel", move |event: WheelEvent| { + if prevent_default.get() { + event.prevent_default(); + } + + if let Some(delta) = event::mouse_scroll_delta(&window, &event) { + let modifiers = event::mouse_modifiers(&event); + handler(0, delta, modifiers); + } + })); + } + + pub fn on_dark_mode(&mut self, mut handler: F) + where + F: 'static + FnMut(bool), + { + self.on_dark_mode = Some(MediaQueryListHandle::new( + &self.common.window, + "(prefers-color-scheme: dark)", + move |mql| handler(mql.matches()), + )); + } + + pub(crate) fn on_resize_scale(&mut self, scale_handler: S, size_handler: R) + where + S: 'static + Fn(PhysicalSize, f64), + R: 'static + Fn(PhysicalSize), + { + self.on_resize_scale = Some(ResizeScaleHandle::new( + self.window().clone(), + self.document().clone(), + self.raw().clone(), + self.style().clone(), + scale_handler, + size_handler, + )); + } + + pub(crate) fn on_intersection(&mut self, handler: F) + where + F: 'static + FnMut(bool), + { + self.on_intersect = Some(IntersectionObserverHandle::new(self.raw(), handler)); + } + + pub(crate) fn on_animation_frame(&mut self, f: F) + where + F: 'static + FnMut(), + { + self.animation_frame_handler.on_animation_frame(f) + } + + pub(crate) fn on_context_menu(&mut self) { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_context_menu = + Some(self.common.add_event("contextmenu", move |event: PointerEvent| { + if prevent_default.get() { + event.prevent_default(); + } + })); + } + + pub fn request_fullscreen(&self) { + fullscreen::request_fullscreen(self.document(), self.raw()); + } + + pub fn exit_fullscreen(&self) { + fullscreen::exit_fullscreen(self.document(), self.raw()); + } + + pub fn is_fullscreen(&self) -> bool { + fullscreen::is_fullscreen(self.document(), self.raw()) + } + + pub fn request_animation_frame(&self) { + self.animation_frame_handler.request(); + } + + pub(crate) fn handle_scale_change( + &self, + runner: &super::super::event_loop::runner::Shared, + event_handler: impl FnOnce(crate::event::Event<()>), + current_size: PhysicalSize, + scale: f64, + ) { + // First, we send the `ScaleFactorChanged` event: + self.set_current_size(current_size); + let new_size = { + let new_size = Arc::new(Mutex::new(current_size)); + event_handler(crate::event::Event::WindowEvent { + window_id: RootWindowId(self.id), + event: crate::event::WindowEvent::ScaleFactorChanged { + scale_factor: scale, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_size)), + }, + }); + + let new_size = *new_size.lock().unwrap(); + new_size + }; + + if current_size != new_size { + // Then we resize the canvas to the new size, a new + // `Resized` event will be sent by the `ResizeObserver`: + let new_size = new_size.to_logical(scale); + super::set_canvas_size(self.document(), self.raw(), self.style(), new_size); + + // Set the size might not trigger the event because the calculation is inaccurate. + self.on_resize_scale + .as_ref() + .expect("expected Window to still be active") + .notify_resize(); + } else if self.old_size() != new_size { + // Then we at least send a resized event. + self.set_old_size(new_size); + runner.send_event(crate::event::Event::WindowEvent { + window_id: RootWindowId(self.id), + event: crate::event::WindowEvent::Resized(new_size), + }) + } + } + + pub fn remove_listeners(&mut self) { + self.on_touch_start = None; + self.on_focus = None; + self.on_blur = None; + self.on_keyboard_release = None; + self.on_keyboard_press = None; + self.on_mouse_wheel = None; + self.on_dark_mode = None; + self.pointer_handler.remove_listeners(); + self.on_resize_scale = None; + self.on_intersect = None; + self.animation_frame_handler.cancel(); + self.on_touch_end = None; + self.on_context_menu = None; + } +} + +impl Common { + pub fn add_event( + &self, + event_name: &'static str, + handler: F, + ) -> EventListenerHandle + where + E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, + F: 'static + FnMut(E), + { + EventListenerHandle::new(self.raw.deref().clone(), event_name, Closure::new(handler)) + } + + pub fn raw(&self) -> &HtmlCanvasElement { + &self.raw + } +} + +impl Style { + fn new(window: &web_sys::Window, canvas: &HtmlCanvasElement) -> Self { + #[allow(clippy::disallowed_methods)] + let read = window + .get_computed_style(canvas) + .expect("Failed to obtain computed style") + // this can't fail: we aren't using a pseudo-element + .expect("Invalid pseudo-element"); + + #[allow(clippy::disallowed_methods)] + let write = canvas.style(); + + Self { read, write } + } + + pub(crate) fn get(&self, property: &str) -> String { + self.read.get_property_value(property).expect("Invalid property") + } + + pub(crate) fn remove(&self, property: &str) { + self.write.remove_property(property).expect("Property is read only"); + } + + pub(crate) fn set(&self, property: &str, value: &str) { + self.write.set_property(property, value).expect("Property is read only"); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/event.rs b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/event.rs new file mode 100644 index 0000000..ddd1257 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/event.rs @@ -0,0 +1,270 @@ +use crate::event::{MouseButton, MouseScrollDelta}; +use crate::keyboard::{Key, KeyLocation, ModifiersState, NamedKey, PhysicalKey}; + +use dpi::{LogicalPosition, PhysicalPosition, Position}; +use smol_str::SmolStr; +use std::cell::OnceCell; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{KeyboardEvent, MouseEvent, PointerEvent, WheelEvent}; + +use super::Engine; + +bitflags::bitflags! { + // https://www.w3.org/TR/pointerevents3/#the-buttons-property + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct ButtonsState: u16 { + const LEFT = 0b00001; + const RIGHT = 0b00010; + const MIDDLE = 0b00100; + const BACK = 0b01000; + const FORWARD = 0b10000; + } +} + +impl From for MouseButton { + fn from(value: ButtonsState) -> Self { + match value { + ButtonsState::LEFT => MouseButton::Left, + ButtonsState::RIGHT => MouseButton::Right, + ButtonsState::MIDDLE => MouseButton::Middle, + ButtonsState::BACK => MouseButton::Back, + ButtonsState::FORWARD => MouseButton::Forward, + _ => MouseButton::Other(value.bits()), + } + } +} + +impl From for ButtonsState { + fn from(value: MouseButton) -> Self { + match value { + MouseButton::Left => ButtonsState::LEFT, + MouseButton::Right => ButtonsState::RIGHT, + MouseButton::Middle => ButtonsState::MIDDLE, + MouseButton::Back => ButtonsState::BACK, + MouseButton::Forward => ButtonsState::FORWARD, + MouseButton::Other(value) => ButtonsState::from_bits_retain(value), + } + } +} + +pub fn mouse_buttons(event: &MouseEvent) -> ButtonsState { + ButtonsState::from_bits_retain(event.buttons()) +} + +pub fn mouse_button(event: &MouseEvent) -> Option { + // https://www.w3.org/TR/pointerevents3/#the-button-property + match event.button() { + -1 => None, + 0 => Some(MouseButton::Left), + 1 => Some(MouseButton::Middle), + 2 => Some(MouseButton::Right), + 3 => Some(MouseButton::Back), + 4 => Some(MouseButton::Forward), + i => { + Some(MouseButton::Other(i.try_into().expect("unexpected negative mouse button value"))) + }, + } +} + +impl MouseButton { + pub fn to_id(self) -> u32 { + match self { + MouseButton::Left => 0, + MouseButton::Right => 1, + MouseButton::Middle => 2, + MouseButton::Back => 3, + MouseButton::Forward => 4, + MouseButton::Other(value) => value.into(), + } + } +} + +pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { + #[wasm_bindgen] + extern "C" { + type MouseEventExt; + + #[wasm_bindgen(method, getter, js_name = offsetX)] + fn offset_x(this: &MouseEventExt) -> f64; + + #[wasm_bindgen(method, getter, js_name = offsetY)] + fn offset_y(this: &MouseEventExt) -> f64; + } + + let event: &MouseEventExt = event.unchecked_ref(); + + LogicalPosition { x: event.offset_x(), y: event.offset_y() } +} + +// TODO: Remove this when Firefox supports correct movement values in coalesced events and browsers +// have agreed on what coordinate space `movementX/Y` is using. +// See . +// See . +pub enum MouseDelta { + Chromium, + Gecko { old_position: LogicalPosition, old_delta: LogicalPosition }, + Other, +} + +impl MouseDelta { + pub fn init(window: &web_sys::Window, event: &PointerEvent) -> Self { + match super::engine(window) { + Some(Engine::Chromium) => Self::Chromium, + // Firefox has wrong movement values in coalesced events. + Some(Engine::Gecko) if has_coalesced_events_support(event) => Self::Gecko { + old_position: mouse_position(event), + old_delta: LogicalPosition::new( + event.movement_x() as f64, + event.movement_y() as f64, + ), + }, + _ => Self::Other, + } + } + + pub fn delta(&mut self, event: &MouseEvent) -> Position { + match self { + MouseDelta::Chromium => { + PhysicalPosition::new(event.movement_x(), event.movement_y()).into() + }, + MouseDelta::Gecko { old_position, old_delta } => { + let new_position = mouse_position(event); + let x = new_position.x - old_position.x + old_delta.x; + let y = new_position.y - old_position.y + old_delta.y; + *old_position = new_position; + *old_delta = LogicalPosition::new(0., 0.); + LogicalPosition::new(x, y).into() + }, + MouseDelta::Other => { + LogicalPosition::new(event.movement_x(), event.movement_y()).into() + }, + } + } +} + +pub fn mouse_scroll_delta( + window: &web_sys::Window, + event: &WheelEvent, +) -> Option { + let x = -event.delta_x(); + let y = -event.delta_y(); + + match event.delta_mode() { + WheelEvent::DOM_DELTA_LINE => Some(MouseScrollDelta::LineDelta(x as f32, y as f32)), + WheelEvent::DOM_DELTA_PIXEL => { + let delta = LogicalPosition::new(x, y).to_physical(super::scale_factor(window)); + Some(MouseScrollDelta::PixelDelta(delta)) + }, + _ => None, + } +} + +pub fn key_code(event: &KeyboardEvent) -> PhysicalKey { + let code = event.code(); + PhysicalKey::from_key_code_attribute_value(&code) +} + +pub fn key(event: &KeyboardEvent) -> Key { + Key::from_key_attribute_value(&event.key()) +} + +pub fn key_text(event: &KeyboardEvent) -> Option { + let key = event.key(); + let key = Key::from_key_attribute_value(&key); + match &key { + Key::Character(text) => Some(text.clone()), + Key::Named(NamedKey::Tab) => Some(SmolStr::new("\t")), + Key::Named(NamedKey::Enter) => Some(SmolStr::new("\r")), + Key::Named(NamedKey::Space) => Some(SmolStr::new(" ")), + _ => None, + } + .map(SmolStr::new) +} + +pub fn key_location(event: &KeyboardEvent) -> KeyLocation { + match event.location() { + KeyboardEvent::DOM_KEY_LOCATION_LEFT => KeyLocation::Left, + KeyboardEvent::DOM_KEY_LOCATION_RIGHT => KeyLocation::Right, + KeyboardEvent::DOM_KEY_LOCATION_NUMPAD => KeyLocation::Numpad, + KeyboardEvent::DOM_KEY_LOCATION_STANDARD => KeyLocation::Standard, + location => { + tracing::warn!("Unexpected key location: {location}"); + KeyLocation::Standard + }, + } +} + +pub fn keyboard_modifiers(event: &KeyboardEvent) -> ModifiersState { + let mut state = ModifiersState::empty(); + + if event.shift_key() { + state |= ModifiersState::SHIFT; + } + if event.ctrl_key() { + state |= ModifiersState::CONTROL; + } + if event.alt_key() { + state |= ModifiersState::ALT; + } + if event.meta_key() { + state |= ModifiersState::SUPER; + } + + state +} + +pub fn mouse_modifiers(event: &MouseEvent) -> ModifiersState { + let mut state = ModifiersState::empty(); + + if event.shift_key() { + state |= ModifiersState::SHIFT; + } + if event.ctrl_key() { + state |= ModifiersState::CONTROL; + } + if event.alt_key() { + state |= ModifiersState::ALT; + } + if event.meta_key() { + state |= ModifiersState::SUPER; + } + + state +} + +pub fn pointer_move_event(event: PointerEvent) -> impl Iterator { + // make a single iterator depending on the availability of coalesced events + if has_coalesced_events_support(&event) { + None.into_iter().chain( + Some(event.get_coalesced_events().into_iter().map(PointerEvent::unchecked_from_js)) + .into_iter() + .flatten(), + ) + } else { + Some(event).into_iter().chain(None.into_iter().flatten()) + } +} + +// TODO: Remove when Safari supports `getCoalescedEvents`. +// See . +pub fn has_coalesced_events_support(event: &PointerEvent) -> bool { + thread_local! { + static COALESCED_EVENTS_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + COALESCED_EVENTS_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type PointerCoalescedEventsSupport; + + #[wasm_bindgen(method, getter, js_name = getCoalescedEvents)] + fn has_get_coalesced_events(this: &PointerCoalescedEventsSupport) -> JsValue; + } + + let support: &PointerCoalescedEventsSupport = event.unchecked_ref(); + !support.has_get_coalesced_events().is_undefined() + }) + }) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/event_handle.rs b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/event_handle.rs new file mode 100644 index 0000000..4474af3 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/event_handle.rs @@ -0,0 +1,38 @@ +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::EventTarget; + +pub struct EventListenerHandle { + target: EventTarget, + event_type: &'static str, + listener: Closure, +} + +impl EventListenerHandle { + pub fn new(target: U, event_type: &'static str, listener: Closure) -> Self + where + U: Into, + { + let target = target.into(); + target + .add_event_listener_with_callback(event_type, listener.as_ref().unchecked_ref()) + .expect("Failed to add event listener"); + EventListenerHandle { target, event_type, listener } + } +} + +impl Drop for EventListenerHandle { + fn drop(&mut self) { + self.target + .remove_event_listener_with_callback( + self.event_type, + self.listener.as_ref().unchecked_ref(), + ) + .unwrap_or_else(|e| { + web_sys::console::error_2( + &format!("Error removing event listener {}", self.event_type).into(), + &e, + ) + }); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/fullscreen.rs b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/fullscreen.rs new file mode 100644 index 0000000..867d34a --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/fullscreen.rs @@ -0,0 +1,103 @@ +use std::cell::OnceCell; + +use js_sys::Promise; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{Document, Element, HtmlCanvasElement}; + +pub fn request_fullscreen(document: &Document, canvas: &HtmlCanvasElement) { + if is_fullscreen(document, canvas) { + return; + } + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(extends = HtmlCanvasElement)] + type RequestFullscreen; + + #[wasm_bindgen(method, js_name = requestFullscreen)] + fn request_fullscreen(this: &RequestFullscreen) -> Promise; + + #[wasm_bindgen(method, js_name = webkitRequestFullscreen)] + fn webkit_request_fullscreen(this: &RequestFullscreen); + } + + let canvas: &RequestFullscreen = canvas.unchecked_ref(); + + if has_fullscreen_api_support(canvas) { + thread_local! { + static REJECT_HANDLER: Closure = Closure::new(|_| ()); + } + REJECT_HANDLER.with(|handler| { + let _ = canvas.request_fullscreen().catch(handler); + }); + } else { + canvas.webkit_request_fullscreen(); + } +} + +pub fn is_fullscreen(document: &Document, canvas: &HtmlCanvasElement) -> bool { + #[wasm_bindgen] + extern "C" { + type FullscreenElement; + + #[wasm_bindgen(method, getter, js_name = webkitFullscreenElement)] + fn webkit_fullscreen_element(this: &FullscreenElement) -> Option; + } + + let element = if has_fullscreen_api_support(canvas) { + #[allow(clippy::disallowed_methods)] + document.fullscreen_element() + } else { + let document: &FullscreenElement = document.unchecked_ref(); + document.webkit_fullscreen_element() + }; + + match element { + Some(element) => { + let canvas: &Element = canvas; + canvas == &element + }, + None => false, + } +} + +pub fn exit_fullscreen(document: &Document, canvas: &HtmlCanvasElement) { + #[wasm_bindgen] + extern "C" { + type ExitFullscreen; + + #[wasm_bindgen(method, js_name = webkitExitFullscreen)] + fn webkit_exit_fullscreen(this: &ExitFullscreen); + } + + if has_fullscreen_api_support(canvas) { + #[allow(clippy::disallowed_methods)] + document.exit_fullscreen() + } else { + let document: &ExitFullscreen = document.unchecked_ref(); + document.webkit_exit_fullscreen() + } +} + +fn has_fullscreen_api_support(canvas: &HtmlCanvasElement) -> bool { + thread_local! { + static FULLSCREEN_API_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + FULLSCREEN_API_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type CanvasFullScreenApiSupport; + + #[wasm_bindgen(method, getter, js_name = requestFullscreen)] + fn has_request_fullscreen(this: &CanvasFullScreenApiSupport) -> JsValue; + } + + let support: &CanvasFullScreenApiSupport = canvas.unchecked_ref(); + !support.has_request_fullscreen().is_undefined() + }) + }) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/intersection_handle.rs b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/intersection_handle.rs new file mode 100644 index 0000000..9f63e84 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/intersection_handle.rs @@ -0,0 +1,33 @@ +use js_sys::Array; +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::{Element, IntersectionObserver, IntersectionObserverEntry}; + +pub(super) struct IntersectionObserverHandle { + observer: IntersectionObserver, + _closure: Closure, +} + +impl IntersectionObserverHandle { + pub fn new(element: &Element, mut callback: F) -> Self + where + F: 'static + FnMut(bool), + { + let closure = Closure::new(move |entries: Array| { + let entry: IntersectionObserverEntry = entries.get(0).unchecked_into(); + callback(entry.is_intersecting()); + }); + let observer = IntersectionObserver::new(closure.as_ref().unchecked_ref()) + // we don't provide any `options` + .expect("Invalid `options`"); + observer.observe(element); + + Self { observer, _closure: closure } + } +} + +impl Drop for IntersectionObserverHandle { + fn drop(&mut self) { + self.observer.disconnect() + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/media_query_handle.rs b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/media_query_handle.rs new file mode 100644 index 0000000..766157a --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/media_query_handle.rs @@ -0,0 +1,48 @@ +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::MediaQueryList; + +pub(super) struct MediaQueryListHandle { + mql: MediaQueryList, + closure: Closure, +} + +impl MediaQueryListHandle { + pub fn new(window: &web_sys::Window, media_query: &str, mut listener: F) -> Self + where + F: 'static + FnMut(&MediaQueryList), + { + let mql = window + .match_media(media_query) + .expect("Failed to parse media query") + .expect("Found empty media query"); + + let closure = Closure::new({ + let mql = mql.clone(); + move || listener(&mql) + }); + // TODO: Replace obsolete `addListener()` with `addEventListener()` and use + // `MediaQueryListEvent` instead of cloning the `MediaQueryList`. + // Requires Safari v14. + mql.add_listener_with_opt_callback(Some(closure.as_ref().unchecked_ref())) + .expect("Invalid listener"); + + Self { mql, closure } + } + + pub fn mql(&self) -> &MediaQueryList { + &self.mql + } +} + +impl Drop for MediaQueryListHandle { + fn drop(&mut self) { + remove_listener(&self.mql, &self.closure); + } +} + +fn remove_listener(mql: &MediaQueryList, listener: &Closure) { + mql.remove_listener_with_opt_callback(Some(listener.as_ref().unchecked_ref())).unwrap_or_else( + |e| web_sys::console::error_2(&"Error removing media query listener".into(), &e), + ); +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/mod.rs b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/mod.rs new file mode 100644 index 0000000..08962b4 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/mod.rs @@ -0,0 +1,233 @@ +mod animation_frame; +mod canvas; +pub mod event; +mod event_handle; +mod fullscreen; +mod intersection_handle; +mod media_query_handle; +mod pointer; +mod resize_scaling; +mod schedule; + +use std::sync::OnceLock; + +pub use self::canvas::{Canvas, Style}; +pub use self::event::ButtonsState; +pub use self::event_handle::EventListenerHandle; +pub use self::resize_scaling::ResizeScaleHandle; +pub use self::schedule::Schedule; + +use crate::dpi::{LogicalPosition, LogicalSize}; +use js_sys::Array; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use web_sys::{ + Document, HtmlCanvasElement, Navigator, PageTransitionEvent, VisibilityState, Window, +}; + +pub fn throw(msg: &str) { + wasm_bindgen::throw_str(msg); +} + +pub struct PageTransitionEventHandle { + _show_listener: event_handle::EventListenerHandle, + _hide_listener: event_handle::EventListenerHandle, +} + +pub fn on_page_transition( + window: web_sys::Window, + show_handler: impl FnMut(PageTransitionEvent) + 'static, + hide_handler: impl FnMut(PageTransitionEvent) + 'static, +) -> PageTransitionEventHandle { + let show_closure = Closure::new(show_handler); + let hide_closure = Closure::new(hide_handler); + + let show_listener = + event_handle::EventListenerHandle::new(window.clone(), "pageshow", show_closure); + let hide_listener = event_handle::EventListenerHandle::new(window, "pagehide", hide_closure); + PageTransitionEventHandle { _show_listener: show_listener, _hide_listener: hide_listener } +} + +pub fn scale_factor(window: &web_sys::Window) -> f64 { + window.device_pixel_ratio() +} + +fn fix_canvas_size(style: &Style, mut size: LogicalSize) -> LogicalSize { + if style.get("box-sizing") == "border-box" { + size.width += style_size_property(style, "border-left-width") + + style_size_property(style, "border-right-width") + + style_size_property(style, "padding-left") + + style_size_property(style, "padding-right"); + size.height += style_size_property(style, "border-top-width") + + style_size_property(style, "border-bottom-width") + + style_size_property(style, "padding-top") + + style_size_property(style, "padding-bottom"); + } + + size +} + +pub fn set_canvas_size( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + new_size: LogicalSize, +) { + if !document.contains(Some(raw)) || style.get("display") == "none" { + return; + } + + let new_size = fix_canvas_size(style, new_size); + + style.set("width", &format!("{}px", new_size.width)); + style.set("height", &format!("{}px", new_size.height)); +} + +pub fn set_canvas_min_size( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + dimensions: Option>, +) { + if let Some(dimensions) = dimensions { + if !document.contains(Some(raw)) || style.get("display") == "none" { + return; + } + + let new_size = fix_canvas_size(style, dimensions); + + style.set("min-width", &format!("{}px", new_size.width)); + style.set("min-height", &format!("{}px", new_size.height)); + } else { + style.remove("min-width"); + style.remove("min-height"); + } +} + +pub fn set_canvas_max_size( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + dimensions: Option>, +) { + if let Some(dimensions) = dimensions { + if !document.contains(Some(raw)) || style.get("display") == "none" { + return; + } + + let new_size = fix_canvas_size(style, dimensions); + + style.set("max-width", &format!("{}px", new_size.width)); + style.set("max-height", &format!("{}px", new_size.height)); + } else { + style.remove("max-width"); + style.remove("max-height"); + } +} + +pub fn set_canvas_position( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + mut position: LogicalPosition, +) { + if document.contains(Some(raw)) && style.get("display") != "none" { + position.x -= style_size_property(style, "margin-left") + + style_size_property(style, "border-left-width") + + style_size_property(style, "padding-left"); + position.y -= style_size_property(style, "margin-top") + + style_size_property(style, "border-top-width") + + style_size_property(style, "padding-top"); + } + + style.set("position", "fixed"); + style.set("left", &format!("{}px", position.x)); + style.set("top", &format!("{}px", position.y)); +} + +/// This function will panic if the element is not inserted in the DOM +/// or is not a CSS property that represents a size in pixel. +pub fn style_size_property(style: &Style, property: &str) -> f64 { + let prop = style.get(property); + prop.strip_suffix("px") + .expect("Element was not inserted into the DOM or is not a size in pixel") + .parse() + .expect("CSS property is not a size in pixel") +} + +pub fn is_dark_mode(window: &web_sys::Window) -> Option { + window.match_media("(prefers-color-scheme: dark)").ok().flatten().map(|media| media.matches()) +} + +pub fn is_visible(document: &Document) -> bool { + document.visibility_state() == VisibilityState::Visible +} + +pub type RawCanvasType = HtmlCanvasElement; + +#[derive(Clone, Copy)] +pub enum Engine { + Chromium, + Gecko, + WebKit, +} + +pub fn engine(window: &Window) -> Option { + static ENGINE: OnceLock> = OnceLock::new(); + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(extends = Navigator)] + type NavigatorExt; + + #[wasm_bindgen(method, getter, js_name = userAgentData)] + fn user_agent_data(this: &NavigatorExt) -> Option; + + type NavigatorUaData; + + #[wasm_bindgen(method, getter)] + fn brands(this: &NavigatorUaData) -> Array; + + type NavigatorUaBrandVersion; + + #[wasm_bindgen(method, getter)] + fn brand(this: &NavigatorUaBrandVersion) -> String; + } + + *ENGINE.get_or_init(|| { + let navigator: NavigatorExt = window.navigator().unchecked_into(); + + if let Some(data) = navigator.user_agent_data() { + for brand in data + .brands() + .iter() + .map(NavigatorUaBrandVersion::unchecked_from_js) + .map(|brand| brand.brand()) + { + match brand.as_str() { + "Chromium" => return Some(Engine::Chromium), + // TODO: verify when Firefox actually implements it. + "Gecko" => return Some(Engine::Gecko), + // TODO: verify when Safari actually implements it. + "WebKit" => return Some(Engine::WebKit), + _ => (), + } + } + + None + } else { + let data = navigator.user_agent().ok()?; + + if data.contains("Chrome/") { + Some(Engine::Chromium) + } else if data.contains("Gecko/") { + Some(Engine::Gecko) + } else if data.contains("AppleWebKit/") { + Some(Engine::WebKit) + } else { + None + } + } + }) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/pointer.rs b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/pointer.rs new file mode 100644 index 0000000..3ee168b --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/pointer.rs @@ -0,0 +1,244 @@ +use std::cell::Cell; +use std::rc::Rc; + +use super::canvas::Common; +use super::event; +use super::event_handle::EventListenerHandle; +use crate::dpi::PhysicalPosition; +use crate::event::{Force, MouseButton}; +use crate::keyboard::ModifiersState; + +use event::ButtonsState; +use web_sys::PointerEvent; + +#[allow(dead_code)] +pub(super) struct PointerHandler { + on_cursor_leave: Option>, + on_cursor_enter: Option>, + on_cursor_move: Option>, + on_pointer_press: Option>, + on_pointer_release: Option>, + on_touch_cancel: Option>, +} + +impl PointerHandler { + pub fn new() -> Self { + Self { + on_cursor_leave: None, + on_cursor_enter: None, + on_cursor_move: None, + on_pointer_press: None, + on_pointer_release: None, + on_touch_cancel: None, + } + } + + pub fn on_cursor_leave(&mut self, canvas_common: &Common, mut handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.on_cursor_leave = + Some(canvas_common.add_event("pointerout", move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + // touch events are handled separately + // handling them here would produce duplicate mouse events, inconsistent with + // other platforms. + let pointer_id = (event.pointer_type() != "touch").then(|| event.pointer_id()); + + handler(modifiers, pointer_id); + })); + } + + pub fn on_cursor_enter(&mut self, canvas_common: &Common, mut handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.on_cursor_enter = + Some(canvas_common.add_event("pointerover", move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + // touch events are handled separately + // handling them here would produce duplicate mouse events, inconsistent with + // other platforms. + let pointer_id = (event.pointer_type() != "touch").then(|| event.pointer_id()); + + handler(modifiers, pointer_id); + })); + } + + pub fn on_mouse_release( + &mut self, + canvas_common: &Common, + mut mouse_handler: M, + mut touch_handler: T, + ) where + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + let window = canvas_common.window.clone(); + self.on_pointer_release = + Some(canvas_common.add_event("pointerup", move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + match event.pointer_type().as_str() { + "touch" => touch_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + Force::Normalized(event.pressure() as f64), + ), + _ => mouse_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + event::mouse_button(&event).expect("no mouse button released"), + ), + } + })); + } + + pub fn on_mouse_press( + &mut self, + canvas_common: &Common, + mut mouse_handler: M, + mut touch_handler: T, + prevent_default: Rc>, + ) where + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + let window = canvas_common.window.clone(); + let canvas = canvas_common.raw().clone(); + self.on_pointer_press = + Some(canvas_common.add_event("pointerdown", move |event: PointerEvent| { + if prevent_default.get() { + // prevent text selection + event.prevent_default(); + // but still focus element + let _ = canvas.focus(); + } + + let modifiers = event::mouse_modifiers(&event); + let pointer_type = &event.pointer_type(); + + match pointer_type.as_str() { + "touch" => { + touch_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + Force::Normalized(event.pressure() as f64), + ); + }, + _ => { + mouse_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + event::mouse_button(&event).expect("no mouse button pressed"), + ); + + if pointer_type == "mouse" { + // Error is swallowed here since the error would occur every time the + // mouse is clicked when the cursor is + // grabbed, and there is probably not a + // situation where this could fail, that we + // care if it fails. + let _e = canvas.set_pointer_capture(event.pointer_id()); + } + }, + } + })); + } + + pub fn on_cursor_move( + &mut self, + canvas_common: &Common, + mut mouse_handler: M, + mut touch_handler: T, + mut button_handler: B, + prevent_default: Rc>, + ) where + M: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator>), + T: 'static + + FnMut(ModifiersState, i32, &mut dyn Iterator, Force)>), + B: 'static + FnMut(ModifiersState, i32, PhysicalPosition, ButtonsState, MouseButton), + { + let window = canvas_common.window.clone(); + let canvas = canvas_common.raw().clone(); + self.on_cursor_move = + Some(canvas_common.add_event("pointermove", move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + let id = event.pointer_id(); + + // chorded button event + if let Some(button) = event::mouse_button(&event) { + if prevent_default.get() { + // prevent text selection + event.prevent_default(); + // but still focus element + let _ = canvas.focus(); + } + + button_handler( + modifiers, + id, + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + event::mouse_buttons(&event), + button, + ); + + return; + } + + // pointer move event + let scale = super::scale_factor(&window); + match event.pointer_type().as_str() { + "touch" => touch_handler( + modifiers, + id, + &mut event::pointer_move_event(event).map(|event| { + ( + event::mouse_position(&event).to_physical(scale), + Force::Normalized(event.pressure() as f64), + ) + }), + ), + _ => mouse_handler( + modifiers, + id, + &mut event::pointer_move_event(event) + .map(|event| event::mouse_position(&event).to_physical(scale)), + ), + }; + })); + } + + pub fn on_touch_cancel(&mut self, canvas_common: &Common, mut handler: F) + where + F: 'static + FnMut(i32, PhysicalPosition, Force), + { + let window = canvas_common.window.clone(); + self.on_touch_cancel = + Some(canvas_common.add_event("pointercancel", move |event: PointerEvent| { + if event.pointer_type() == "touch" { + handler( + event.pointer_id(), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + Force::Normalized(event.pressure() as f64), + ); + } + })); + } + + pub fn remove_listeners(&mut self) { + self.on_cursor_leave = None; + self.on_cursor_enter = None; + self.on_cursor_move = None; + self.on_pointer_press = None; + self.on_pointer_release = None; + self.on_touch_cancel = None; + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/resize_scaling.rs b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/resize_scaling.rs new file mode 100644 index 0000000..4d10b3a --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/resize_scaling.rs @@ -0,0 +1,302 @@ +use js_sys::{Array, Object}; +use tracing::warn; +use wasm_bindgen::prelude::{wasm_bindgen, Closure}; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{ + Document, HtmlCanvasElement, MediaQueryList, ResizeObserver, ResizeObserverBoxOptions, + ResizeObserverEntry, ResizeObserverOptions, ResizeObserverSize, Window, +}; + +use crate::dpi::{LogicalSize, PhysicalSize}; + +use super::super::backend; +use super::canvas::Style; +use super::media_query_handle::MediaQueryListHandle; + +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +pub struct ResizeScaleHandle(Rc); + +impl ResizeScaleHandle { + pub(crate) fn new( + window: Window, + document: Document, + canvas: HtmlCanvasElement, + style: Style, + scale_handler: S, + resize_handler: R, + ) -> Self + where + S: 'static + Fn(PhysicalSize, f64), + R: 'static + Fn(PhysicalSize), + { + Self(ResizeScaleInternal::new( + window, + document, + canvas, + style, + scale_handler, + resize_handler, + )) + } + + pub(crate) fn notify_resize(&self) { + self.0.notify() + } +} + +/// This is a helper type to help manage the `MediaQueryList` used for detecting +/// changes of the `devicePixelRatio`. +struct ResizeScaleInternal { + window: Window, + document: Document, + canvas: HtmlCanvasElement, + style: Style, + mql: RefCell, + observer: ResizeObserver, + _observer_closure: Closure, + scale_handler: Box, f64)>, + resize_handler: Box)>, + notify_scale: Cell, +} + +impl ResizeScaleInternal { + fn new( + window: Window, + document: Document, + canvas: HtmlCanvasElement, + style: Style, + scale_handler: S, + resize_handler: R, + ) -> Rc + where + S: 'static + Fn(PhysicalSize, f64), + R: 'static + Fn(PhysicalSize), + { + Rc::::new_cyclic(|weak_self| { + let mql = Self::create_mql(&window, { + let weak_self = weak_self.clone(); + move |mql| { + if let Some(rc_self) = weak_self.upgrade() { + Self::handle_scale(rc_self, mql); + } + } + }); + + let weak_self = weak_self.clone(); + let observer_closure = Closure::new(move |entries: Array, _| { + if let Some(this) = weak_self.upgrade() { + let size = this.process_entry(entries); + + if this.notify_scale.replace(false) { + let scale = backend::scale_factor(&this.window); + (this.scale_handler)(size, scale) + } else { + (this.resize_handler)(size) + } + } + }); + let observer = Self::create_observer(&canvas, observer_closure.as_ref()); + + Self { + window, + document, + canvas, + style, + mql: RefCell::new(mql), + observer, + _observer_closure: observer_closure, + scale_handler: Box::new(scale_handler), + resize_handler: Box::new(resize_handler), + notify_scale: Cell::new(false), + } + }) + } + + fn create_mql(window: &Window, closure: F) -> MediaQueryListHandle + where + F: 'static + FnMut(&MediaQueryList), + { + let current_scale = super::scale_factor(window); + // TODO: Remove `-webkit-device-pixel-ratio`. Requires Safari v16. + let media_query = format!( + "(resolution: {current_scale}dppx), + (-webkit-device-pixel-ratio: {current_scale})", + ); + let mql = MediaQueryListHandle::new(window, &media_query, closure); + debug_assert!( + mql.mql().matches(), + "created media query doesn't match, {current_scale} != {}", + super::scale_factor(window) + ); + mql + } + + fn create_observer(canvas: &HtmlCanvasElement, closure: &JsValue) -> ResizeObserver { + let observer = ResizeObserver::new(closure.as_ref().unchecked_ref()) + .expect("Failed to create `ResizeObserver`"); + + // Safari doesn't support `devicePixelContentBoxSize` + if has_device_pixel_support() { + let options = ResizeObserverOptions::new(); + options.set_box(ResizeObserverBoxOptions::DevicePixelContentBox); + observer.observe_with_options(canvas, &options); + } else { + observer.observe(canvas); + } + + observer + } + + fn notify(&self) { + if !self.document.contains(Some(&self.canvas)) || self.style.get("display") == "none" { + let size = PhysicalSize::new(0, 0); + + if self.notify_scale.replace(false) { + let scale = backend::scale_factor(&self.window); + (self.scale_handler)(size, scale) + } else { + (self.resize_handler)(size) + } + + return; + } + + // Safari doesn't support `devicePixelContentBoxSize` + if has_device_pixel_support() { + self.observer.unobserve(&self.canvas); + self.observer.observe(&self.canvas); + + return; + } + + let mut size = LogicalSize::new( + backend::style_size_property(&self.style, "width"), + backend::style_size_property(&self.style, "height"), + ); + + if self.style.get("box-sizing") == "border-box" { + size.width -= backend::style_size_property(&self.style, "border-left-width") + + backend::style_size_property(&self.style, "border-right-width") + + backend::style_size_property(&self.style, "padding-left") + + backend::style_size_property(&self.style, "padding-right"); + size.height -= backend::style_size_property(&self.style, "border-top-width") + + backend::style_size_property(&self.style, "border-bottom-width") + + backend::style_size_property(&self.style, "padding-top") + + backend::style_size_property(&self.style, "padding-bottom"); + } + + let size = size.to_physical(backend::scale_factor(&self.window)); + + if self.notify_scale.replace(false) { + let scale = backend::scale_factor(&self.window); + (self.scale_handler)(size, scale) + } else { + (self.resize_handler)(size) + } + } + + fn handle_scale(self: Rc, mql: &MediaQueryList) { + let weak_self = Rc::downgrade(&self); + let scale = super::scale_factor(&self.window); + + // TODO: confirm/reproduce this problem, see: + // . + // This should never happen, but if it does then apparently the scale factor didn't change. + if mql.matches() { + warn!( + "media query tracking scale factor was triggered without a change:\nMedia Query: \ + {}\nCurrent Scale: {scale}", + mql.media(), + ); + return; + } + + let new_mql = Self::create_mql(&self.window, move |mql| { + if let Some(rc_self) = weak_self.upgrade() { + Self::handle_scale(rc_self, mql); + } + }); + self.mql.replace(new_mql); + + self.notify_scale.set(true); + self.notify(); + } + + fn process_entry(&self, entries: Array) -> PhysicalSize { + let entry: ResizeObserverEntry = entries.get(0).unchecked_into(); + + // Safari doesn't support `devicePixelContentBoxSize` + if !has_device_pixel_support() { + let rect = entry.content_rect(); + + return LogicalSize::new(rect.width(), rect.height()) + .to_physical(backend::scale_factor(&self.window)); + } + + let entry: ResizeObserverSize = + entry.device_pixel_content_box_size().get(0).unchecked_into(); + + let writing_mode = self.style.get("writing-mode"); + + // means the canvas is not inserted into the DOM + if writing_mode.is_empty() { + debug_assert_eq!(entry.inline_size(), 0.); + debug_assert_eq!(entry.block_size(), 0.); + + return PhysicalSize::new(0, 0); + } + + let horizontal = match writing_mode.as_str() { + _ if writing_mode.starts_with("horizontal") => true, + _ if writing_mode.starts_with("vertical") | writing_mode.starts_with("sideways") => { + false + }, + // deprecated values + "lr" | "lr-tb" | "rl" => true, + "tb" | "tb-lr" | "tb-rl" => false, + _ => { + warn!("unrecognized `writing-mode`, assuming horizontal"); + true + }, + }; + + if horizontal { + PhysicalSize::new(entry.inline_size() as u32, entry.block_size() as u32) + } else { + PhysicalSize::new(entry.block_size() as u32, entry.inline_size() as u32) + } + } +} + +impl Drop for ResizeScaleInternal { + fn drop(&mut self) { + self.observer.disconnect(); + } +} + +// TODO: Remove when Safari supports `devicePixelContentBoxSize`. +// See . +pub fn has_device_pixel_support() -> bool { + thread_local! { + static DEVICE_PIXEL_SUPPORT: bool = { + #[wasm_bindgen] + extern "C" { + type ResizeObserverEntryExt; + + #[wasm_bindgen(js_class = ResizeObserverEntry, static_method_of = ResizeObserverEntryExt, getter)] + fn prototype() -> Object; + } + + let prototype = ResizeObserverEntryExt::prototype(); + let descriptor = Object::get_own_property_descriptor( + &prototype, + &JsValue::from_str("devicePixelContentBoxSize"), + ); + !descriptor.is_undefined() + }; + } + + DEVICE_PIXEL_SUPPORT.with(|support| *support) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/schedule.rs b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/schedule.rs new file mode 100644 index 0000000..eb97068 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/schedule.rs @@ -0,0 +1,342 @@ +use js_sys::{Array, Function, Object, Promise, Reflect}; +use std::cell::OnceCell; +use std::time::Duration; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{ + AbortController, AbortSignal, Blob, BlobPropertyBag, MessageChannel, MessagePort, Url, Worker, +}; + +use crate::platform::web::{PollStrategy, WaitUntilStrategy}; + +#[derive(Debug)] +pub struct Schedule { + _closure: Closure, + inner: Inner, +} + +#[derive(Debug)] +enum Inner { + Scheduler { + controller: AbortController, + }, + IdleCallback { + window: web_sys::Window, + handle: u32, + }, + Timeout { + window: web_sys::Window, + handle: i32, + port: MessagePort, + _timeout_closure: Closure, + }, + Worker(MessagePort), +} + +impl Schedule { + pub fn new(strategy: PollStrategy, window: &web_sys::Window, f: F) -> Schedule + where + F: 'static + FnMut(), + { + if strategy == PollStrategy::Scheduler && has_scheduler_support(window) { + Self::new_scheduler(window, f, None) + } else if strategy == PollStrategy::IdleCallback && has_idle_callback_support(window) { + Self::new_idle_callback(window.clone(), f) + } else { + Self::new_timeout(window.clone(), f, None) + } + } + + pub fn new_with_duration( + strategy: WaitUntilStrategy, + window: &web_sys::Window, + f: F, + duration: Duration, + ) -> Schedule + where + F: 'static + FnMut(), + { + match strategy { + WaitUntilStrategy::Scheduler => { + if has_scheduler_support(window) { + Self::new_scheduler(window, f, Some(duration)) + } else { + Self::new_timeout(window.clone(), f, Some(duration)) + } + }, + WaitUntilStrategy::Worker => Self::new_worker(f, duration), + } + } + + fn new_scheduler(window: &web_sys::Window, f: F, duration: Option) -> Schedule + where + F: 'static + FnMut(), + { + let window: &WindowSupportExt = window.unchecked_ref(); + let scheduler = window.scheduler(); + + let closure = Closure::new(f); + let mut options = SchedulerPostTaskOptions::new(); + let controller = AbortController::new().expect("Failed to create `AbortController`"); + options.signal(&controller.signal()); + + if let Some(duration) = duration { + // `Duration::as_millis()` always rounds down (because of truncation), we want to round + // up instead. This makes sure that the we never wake up **before** the given time. + let duration = duration + .as_secs() + .checked_mul(1000) + .and_then(|secs| secs.checked_add(duration_millis_ceil(duration).into())) + .unwrap_or(u64::MAX); + + options.delay(duration as f64); + } + + thread_local! { + static REJECT_HANDLER: Closure = Closure::new(|_| ()); + } + REJECT_HANDLER.with(|handler| { + let _ = scheduler + .post_task_with_options(closure.as_ref().unchecked_ref(), &options) + .catch(handler); + }); + + Schedule { _closure: closure, inner: Inner::Scheduler { controller } } + } + + fn new_idle_callback(window: web_sys::Window, f: F) -> Schedule + where + F: 'static + FnMut(), + { + let closure = Closure::new(f); + let handle = window + .request_idle_callback(closure.as_ref().unchecked_ref()) + .expect("Failed to request idle callback"); + + Schedule { _closure: closure, inner: Inner::IdleCallback { window, handle } } + } + + fn new_timeout(window: web_sys::Window, f: F, duration: Option) -> Schedule + where + F: 'static + FnMut(), + { + let channel = MessageChannel::new().unwrap(); + let closure = Closure::new(f); + let port_1 = channel.port1(); + port_1.set_onmessage(Some(closure.as_ref().unchecked_ref())); + port_1.start(); + + let port_2 = channel.port2(); + let timeout_closure = Closure::new(move || { + port_2.post_message(&JsValue::UNDEFINED).expect("Failed to send message") + }); + let handle = if let Some(duration) = duration { + // `Duration::as_millis()` always rounds down (because of truncation), we want to round + // up instead. This makes sure that the we never wake up **before** the given time. + let duration = duration + .as_secs() + .try_into() + .ok() + .and_then(|secs: i32| secs.checked_mul(1000)) + .and_then(|secs: i32| { + let millis: i32 = duration_millis_ceil(duration) + .try_into() + .expect("millis are somehow bigger then 1K"); + secs.checked_add(millis) + }) + .unwrap_or(i32::MAX); + + window.set_timeout_with_callback_and_timeout_and_arguments_0( + timeout_closure.as_ref().unchecked_ref(), + duration, + ) + } else { + window.set_timeout_with_callback(timeout_closure.as_ref().unchecked_ref()) + } + .expect("Failed to set timeout"); + + Schedule { + _closure: closure, + inner: Inner::Timeout { + window, + handle, + port: port_1, + _timeout_closure: timeout_closure, + }, + } + } + + fn new_worker(f: F, duration: Duration) -> Schedule + where + F: 'static + FnMut(), + { + thread_local! { + static URL: ScriptUrl = ScriptUrl::new(include_str!("worker.min.js")); + static WORKER: Worker = URL.with(|url| Worker::new(&url.0)).expect("`new Worker()` is not expected to fail with a local script"); + } + + let channel = MessageChannel::new().unwrap(); + let closure = Closure::new(f); + let port_1 = channel.port1(); + port_1.set_onmessage(Some(closure.as_ref().unchecked_ref())); + port_1.start(); + + // `Duration::as_millis()` always rounds down (because of truncation), we want to round + // up instead. This makes sure that the we never wake up **before** the given time. + let duration = duration + .as_secs() + .try_into() + .ok() + .and_then(|secs: u32| secs.checked_mul(1000)) + .and_then(|secs| secs.checked_add(duration_millis_ceil(duration))) + .unwrap_or(u32::MAX); + + WORKER + .with(|worker| { + let port_2 = channel.port2(); + worker.post_message_with_transfer( + &Array::of2(&port_2, &duration.into()), + &Array::of1(&port_2).into(), + ) + }) + .expect("`Worker.postMessage()` is not expected to fail"); + + Schedule { _closure: closure, inner: Inner::Worker(port_1) } + } +} + +impl Drop for Schedule { + fn drop(&mut self) { + match &self.inner { + Inner::Scheduler { controller, .. } => controller.abort(), + Inner::IdleCallback { window, handle, .. } => window.cancel_idle_callback(*handle), + Inner::Timeout { window, handle, port, .. } => { + window.clear_timeout_with_handle(*handle); + port.close(); + port.set_onmessage(None); + }, + Inner::Worker(port) => { + port.close(); + port.set_onmessage(None); + }, + } + } +} + +// TODO: Replace with `u32::div_ceil()` when we hit Rust v1.73. +fn duration_millis_ceil(duration: Duration) -> u32 { + let micros = duration.subsec_micros(); + + // From . + let d = micros / 1000; + let r = micros % 1000; + if r > 0 && 1000 > 0 { + d + 1 + } else { + d + } +} + +fn has_scheduler_support(window: &web_sys::Window) -> bool { + thread_local! { + static SCHEDULER_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + SCHEDULER_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type SchedulerSupport; + + #[wasm_bindgen(method, getter, js_name = scheduler)] + fn has_scheduler(this: &SchedulerSupport) -> JsValue; + } + + let support: &SchedulerSupport = window.unchecked_ref(); + + !support.has_scheduler().is_undefined() + }) + }) +} + +fn has_idle_callback_support(window: &web_sys::Window) -> bool { + thread_local! { + static IDLE_CALLBACK_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + IDLE_CALLBACK_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type IdleCallbackSupport; + + #[wasm_bindgen(method, getter, js_name = requestIdleCallback)] + fn has_request_idle_callback(this: &IdleCallbackSupport) -> JsValue; + } + + let support: &IdleCallbackSupport = window.unchecked_ref(); + !support.has_request_idle_callback().is_undefined() + }) + }) +} + +struct ScriptUrl(String); + +impl ScriptUrl { + fn new(script: &str) -> Self { + let sequence = Array::of1(&script.into()); + let property = BlobPropertyBag::new(); + property.set_type("text/javascript"); + let blob = Blob::new_with_str_sequence_and_options(&sequence, &property) + .expect("`new Blob()` should never throw"); + + let url = Url::create_object_url_with_blob(&blob) + .expect("`URL.createObjectURL()` should never throw"); + + Self(url) + } +} + +impl Drop for ScriptUrl { + fn drop(&mut self) { + Url::revoke_object_url(&self.0).expect("`URL.revokeObjectURL()` should never throw"); + } +} + +#[wasm_bindgen] +extern "C" { + type WindowSupportExt; + + #[wasm_bindgen(method, getter)] + fn scheduler(this: &WindowSupportExt) -> Scheduler; + + type Scheduler; + + #[wasm_bindgen(method, js_name = postTask)] + fn post_task_with_options( + this: &Scheduler, + callback: &Function, + options: &SchedulerPostTaskOptions, + ) -> Promise; + + type SchedulerPostTaskOptions; +} + +impl SchedulerPostTaskOptions { + fn new() -> Self { + Object::new().unchecked_into() + } + + fn delay(&mut self, val: f64) -> &mut Self { + let r = Reflect::set(self, &JsValue::from("delay"), &val.into()); + debug_assert!(r.is_ok(), "Failed to set `delay` property"); + self + } + + fn signal(&mut self, val: &AbortSignal) -> &mut Self { + let r = Reflect::set(self, &JsValue::from("signal"), &val.into()); + debug_assert!(r.is_ok(), "Failed to set `signal` property"); + self + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/worker.js b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/worker.js new file mode 100644 index 0000000..5a8411e --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/worker.js @@ -0,0 +1,10 @@ +onmessage = event => { + const [port, timeout] = event.data + const f = () => port.postMessage(undefined) + + if ('scheduler' in this) { + scheduler.postTask(f, { delay: timeout }) + } else { + setTimeout(f, timeout) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/web/web_sys/worker.min.js b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/worker.min.js new file mode 100644 index 0000000..fd394a7 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/web_sys/worker.min.js @@ -0,0 +1 @@ +onmessage=e=>{let[s,t]=e.data,a=()=>s.postMessage(void 0);"scheduler"in this?scheduler.postTask(a,{delay:t}):setTimeout(a,t)}; diff --git a/third_party/winit-0.30.13/src/platform_impl/web/window.rs b/third_party/winit-0.30.13/src/platform_impl/web/window.rs new file mode 100644 index 0000000..29ca730 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/web/window.rs @@ -0,0 +1,477 @@ +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; +use crate::icon::Icon; +use crate::window::{ + Cursor, CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowId as RootWI, WindowLevel, +}; + +use super::main_thread::{MainThreadMarker, MainThreadSafe}; +use super::monitor::MonitorHandle; +use super::r#async::Dispatcher; +use super::{backend, ActiveEventLoop, Fullscreen}; +use web_sys::HtmlCanvasElement; + +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +use std::sync::Arc; + +pub struct Window { + inner: Dispatcher, +} + +pub struct Inner { + id: WindowId, + pub window: web_sys::Window, + canvas: Rc>, + destroy_fn: Option>, +} + +impl Window { + pub(crate) fn new( + target: &ActiveEventLoop, + mut attr: WindowAttributes, + ) -> Result { + let id = target.generate_id(); + + let window = target.runner.window(); + let document = target.runner.document(); + let canvas = backend::Canvas::create( + target.runner.main_thread(), + id, + window.clone(), + document.clone(), + &mut attr, + )?; + let canvas = Rc::new(RefCell::new(canvas)); + + target.register(&canvas, id); + + let runner = target.runner.clone(); + let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id))); + + let inner = Inner { id, window: window.clone(), canvas, destroy_fn: Some(destroy_fn) }; + + inner.set_title(&attr.title); + inner.set_maximized(attr.maximized); + inner.set_visible(attr.visible); + inner.set_window_icon(attr.window_icon); + inner.set_cursor(attr.cursor); + + let canvas = Rc::downgrade(&inner.canvas); + let (dispatcher, runner) = Dispatcher::new(target.runner.main_thread(), inner).unwrap(); + target.runner.add_canvas(RootWI(id), canvas, runner); + + Ok(Window { inner: dispatcher }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Inner) + Send + 'static) { + self.inner.dispatch(f) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Inner) -> R + Send) -> R { + self.inner.queue(f) + } + + pub fn canvas(&self) -> Option { + self.inner.value().map(|inner| inner.canvas.borrow().raw().clone()) + } + + pub(crate) fn prevent_default(&self) -> bool { + self.inner.queue(|inner| inner.canvas.borrow().prevent_default.get()) + } + + pub(crate) fn set_prevent_default(&self, prevent_default: bool) { + self.inner.dispatch(move |inner| inner.canvas.borrow().prevent_default.set(prevent_default)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + self.inner + .value() + .map(|inner| { + let canvas = inner.canvas.borrow(); + // SAFETY: This will only work if the reference to `HtmlCanvasElement` stays valid. + let canvas: &wasm_bindgen::JsValue = canvas.raw(); + let window_handle = + rwh_06::WebCanvasWindowHandle::new(std::ptr::NonNull::from(canvas).cast()); + rwh_06::RawWindowHandle::WebCanvas(window_handle) + }) + .ok_or(rwh_06::HandleError::Unavailable) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Web(rwh_06::WebDisplayHandle::new())) + } +} + +impl Inner { + pub fn set_title(&self, title: &str) { + self.canvas.borrow().set_attribute("alt", title) + } + + pub fn set_transparent(&self, _transparent: bool) {} + + pub fn set_blur(&self, _blur: bool) {} + + pub fn set_visible(&self, _visible: bool) { + // Intentionally a no-op + } + + #[inline] + pub fn is_visible(&self) -> Option { + None + } + + pub fn request_redraw(&self) { + self.canvas.borrow().request_animation_frame(); + } + + pub fn pre_present_notify(&self) {} + + pub fn outer_position(&self) -> Result, NotSupportedError> { + Ok(self.canvas.borrow().position().to_physical(self.scale_factor())) + } + + pub fn inner_position(&self) -> Result, NotSupportedError> { + // Note: the canvas element has no window decorations, so this is equal to `outer_position`. + self.outer_position() + } + + pub fn set_outer_position(&self, position: Position) { + let canvas = self.canvas.borrow(); + let position = position.to_logical::(self.scale_factor()); + + backend::set_canvas_position(canvas.document(), canvas.raw(), canvas.style(), position) + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + self.canvas.borrow().inner_size() + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + // Note: the canvas element has no window decorations, so this is equal to `inner_size`. + self.inner_size() + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let size = size.to_logical(self.scale_factor()); + let canvas = self.canvas.borrow(); + backend::set_canvas_size(canvas.document(), canvas.raw(), canvas.style(), size); + None + } + + #[inline] + pub fn set_min_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor())); + let canvas = self.canvas.borrow(); + backend::set_canvas_min_size(canvas.document(), canvas.raw(), canvas.style(), dimensions) + } + + #[inline] + pub fn set_max_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor())); + let canvas = self.canvas.borrow(); + backend::set_canvas_max_size(canvas.document(), canvas.raw(), canvas.style(), dimensions) + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + None + } + + #[inline] + pub fn set_resize_increments(&self, _increments: Option) { + // Intentionally a no-op: users can't resize canvas elements + } + + #[inline] + pub fn set_resizable(&self, _resizable: bool) { + // Intentionally a no-op: users can't resize canvas elements + } + + pub fn is_resizable(&self) -> bool { + true + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {} + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + WindowButtons::all() + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + super::backend::scale_factor(&self.window) + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + self.canvas.borrow_mut().cursor.set_cursor(cursor) + } + + #[inline] + pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let lock = match mode { + CursorGrabMode::None => false, + CursorGrabMode::Locked => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + }, + }; + + self.canvas.borrow().set_cursor_lock(lock).map_err(ExternalError::Os) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + self.canvas.borrow_mut().cursor.set_cursor_visible(visible) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + #[inline] + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn set_minimized(&self, _minimized: bool) { + // Intentionally a no-op, as canvases cannot be 'minimized' + } + + #[inline] + pub fn is_minimized(&self) -> Option { + // Canvas cannot be 'minimized' + Some(false) + } + + #[inline] + pub fn set_maximized(&self, _maximized: bool) { + // Intentionally a no-op, as canvases cannot be 'maximized' + } + + #[inline] + pub fn is_maximized(&self) -> bool { + // Canvas cannot be 'maximized' + false + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + if self.canvas.borrow().is_fullscreen() { + Some(Fullscreen::Borderless(None)) + } else { + None + } + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + let canvas = &self.canvas.borrow(); + + if fullscreen.is_some() { + canvas.request_fullscreen(); + } else { + canvas.exit_fullscreen() + } + } + + #[inline] + pub fn set_decorations(&self, _decorations: bool) { + // Intentionally a no-op, no canvas decorations + } + + pub fn is_decorated(&self) -> bool { + true + } + + #[inline] + pub fn set_window_level(&self, _level: WindowLevel) { + // Intentionally a no-op, no window ordering + } + + #[inline] + pub fn set_window_icon(&self, _window_icon: Option) { + // Currently an intentional no-op + } + + #[inline] + pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) { + // Currently a no-op as it does not seem there is good support for this on web + } + + #[inline] + pub fn set_ime_allowed(&self, _allowed: bool) { + // Currently not implemented + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) { + // Currently not implemented + } + + #[inline] + pub fn focus_window(&self) { + let _ = self.canvas.borrow().raw().focus(); + } + + #[inline] + pub fn request_user_attention(&self, _request_type: Option) { + // Currently an intentional no-op + } + + #[inline] + pub fn current_monitor(&self) -> Option { + None + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + VecDeque::new() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + None + } + + #[inline] + pub fn id(&self) -> WindowId { + self.id + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::WebHandle::empty(); + window_handle.id = self.id.0; + rwh_04::RawWindowHandle::Web(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::WebWindowHandle::empty(); + window_handle.id = self.id.0; + rwh_05::RawWindowHandle::Web(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Web(rwh_05::WebDisplayHandle::empty()) + } + + #[inline] + pub fn set_theme(&self, _theme: Option) {} + + #[inline] + pub fn theme(&self) -> Option { + backend::is_dark_mode(&self.window).map(|is_dark_mode| { + if is_dark_mode { + Theme::Dark + } else { + Theme::Light + } + }) + } + + pub fn set_content_protected(&self, _protected: bool) {} + + #[inline] + pub fn has_focus(&self) -> bool { + self.canvas.borrow().has_focus.get() + } + + pub fn title(&self) -> String { + String::new() + } + + pub fn reset_dead_keys(&self) { + // Not supported + } +} + +impl Drop for Inner { + fn drop(&mut self) { + if let Some(destroy_fn) = self.destroy_fn.take() { + destroy_fn(); + } + } +} +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(pub(crate) u32); + +impl WindowId { + pub const fn dummy() -> Self { + Self(0) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as u32) + } +} + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + pub(crate) canvas: Option>>, + pub(crate) prevent_default: bool, + pub(crate) focusable: bool, + pub(crate) append: bool, +} + +impl PlatformSpecificWindowAttributes { + pub(crate) fn set_canvas(&mut self, canvas: Option) { + let Some(canvas) = canvas else { + self.canvas = None; + return; + }; + + let main_thread = MainThreadMarker::new() + .expect("received a `HtmlCanvasElement` outside the window context"); + + self.canvas = Some(Arc::new(MainThreadSafe::new(main_thread, canvas))); + } +} + +impl Default for PlatformSpecificWindowAttributes { + fn default() -> Self { + Self { canvas: None, prevent_default: true, focusable: true, append: false } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/dark_mode.rs b/third_party/winit-0.30.13/src/platform_impl/windows/dark_mode.rs new file mode 100644 index 0000000..366e44c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/dark_mode.rs @@ -0,0 +1,170 @@ +/// 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 std::{ffi::c_void, ptr}; + +use crate::utils::Lazy; +use windows_sys::core::PCSTR; +use windows_sys::Win32::Foundation::{BOOL, HWND, NTSTATUS, S_OK}; +use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}; +use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW; +use windows_sys::Win32::UI::Accessibility::{HCF_HIGHCONTRASTON, HIGHCONTRASTA}; +use windows_sys::Win32::UI::Controls::SetWindowTheme; +use windows_sys::Win32::UI::WindowsAndMessaging::{SystemParametersInfoA, SPI_GETHIGHCONTRAST}; + +use crate::window::Theme; + +use super::util; + +static WIN10_BUILD_VERSION: Lazy> = Lazy::new(|| { + type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> NTSTATUS; + let handle = get_function!("ntdll.dll", RtlGetVersion); + + if let Some(rtl_get_version) = handle { + unsafe { + let mut vi = OSVERSIONINFOW { + dwOSVersionInfoSize: 0, + dwMajorVersion: 0, + dwMinorVersion: 0, + dwBuildNumber: 0, + dwPlatformId: 0, + szCSDVersion: [0; 128], + }; + + let status = (rtl_get_version)(&mut vi); + + if status >= 0 && vi.dwMajorVersion == 10 && vi.dwMinorVersion == 0 { + Some(vi.dwBuildNumber) + } else { + None + } + } + } else { + None + } +}); + +static DARK_MODE_SUPPORTED: Lazy = Lazy::new(|| { + // We won't try to do anything for windows versions < 17763 + // (Windows 10 October 2018 update) + match *WIN10_BUILD_VERSION { + Some(v) => v >= 17763, + None => false, + } +}); + +static DARK_THEME_NAME: Lazy> = Lazy::new(|| util::encode_wide("DarkMode_Explorer")); +static LIGHT_THEME_NAME: Lazy> = Lazy::new(|| util::encode_wide("")); + +/// Attempt to set a theme on a window, if necessary. +/// Returns the theme that was picked +pub fn try_theme(hwnd: HWND, preferred_theme: Option) -> Theme { + if *DARK_MODE_SUPPORTED { + let is_dark_mode = match preferred_theme { + Some(theme) => theme == Theme::Dark, + None => should_use_dark_mode(), + }; + + let theme = if is_dark_mode { Theme::Dark } else { Theme::Light }; + let theme_name = match theme { + Theme::Dark => DARK_THEME_NAME.as_ptr(), + Theme::Light => LIGHT_THEME_NAME.as_ptr(), + }; + + let status = unsafe { SetWindowTheme(hwnd, theme_name, ptr::null()) }; + + if status == S_OK && set_dark_mode_for_window(hwnd, is_dark_mode) { + return theme; + } + } + + Theme::Light +} + +fn set_dark_mode_for_window(hwnd: HWND, is_dark_mode: bool) -> bool { + // Uses Windows undocumented API SetWindowCompositionAttribute, + // as seen in win32-darkmode example linked at top of file. + + type SetWindowCompositionAttribute = + unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL; + + #[allow(clippy::upper_case_acronyms)] + type WINDOWCOMPOSITIONATTRIB = u32; + const WCA_USEDARKMODECOLORS: WINDOWCOMPOSITIONATTRIB = 26; + + #[allow(non_snake_case)] + #[allow(clippy::upper_case_acronyms)] + #[repr(C)] + struct WINDOWCOMPOSITIONATTRIBDATA { + Attrib: WINDOWCOMPOSITIONATTRIB, + pvData: *mut c_void, + cbData: usize, + } + + static SET_WINDOW_COMPOSITION_ATTRIBUTE: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetWindowCompositionAttribute)); + + if let Some(set_window_composition_attribute) = *SET_WINDOW_COMPOSITION_ATTRIBUTE { + unsafe { + // SetWindowCompositionAttribute needs a bigbool (i32), not bool. + let mut is_dark_mode_bigbool = BOOL::from(is_dark_mode); + + let mut data = WINDOWCOMPOSITIONATTRIBDATA { + Attrib: WCA_USEDARKMODECOLORS, + pvData: &mut is_dark_mode_bigbool as *mut _ as _, + cbData: std::mem::size_of_val(&is_dark_mode_bigbool) as _, + }; + + let status = set_window_composition_attribute(hwnd, &mut data); + + status != false.into() + } + } else { + false + } +} + +pub fn should_use_dark_mode() -> bool { + should_apps_use_dark_mode() && !is_high_contrast() +} + +fn should_apps_use_dark_mode() -> bool { + type ShouldAppsUseDarkMode = unsafe extern "system" fn() -> bool; + static SHOULD_APPS_USE_DARK_MODE: Lazy> = Lazy::new(|| unsafe { + const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: PCSTR = 132 as PCSTR; + + // We won't try to do anything for windows versions < 17763 + // (Windows 10 October 2018 update) + if !*DARK_MODE_SUPPORTED { + return None; + } + + let module = LoadLibraryA("uxtheme.dll\0".as_ptr().cast()); + + if module == 0 { + return None; + } + + let handle = GetProcAddress(module, UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL); + + handle.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 { + let mut hc = HIGHCONTRASTA { cbSize: 0, dwFlags: 0, lpszDefaultScheme: ptr::null_mut() }; + + let ok = unsafe { + SystemParametersInfoA( + SPI_GETHIGHCONTRAST, + std::mem::size_of_val(&hc) as _, + &mut hc as *mut _ as _, + 0, + ) + }; + + ok != false.into() && util::has_flag(hc.dwFlags, HCF_HIGHCONTRASTON) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/definitions.rs b/third_party/winit-0.30.13/src/platform_impl/windows/definitions.rs new file mode 100644 index 0000000..c015ffc --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/definitions.rs @@ -0,0 +1,148 @@ +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] + +use std::ffi::c_void; + +use windows_sys::core::{IUnknown, GUID, HRESULT}; +use windows_sys::Win32::Foundation::{BOOL, HWND, POINTL}; +use windows_sys::Win32::System::Com::{ + IAdviseSink, IDataObject, IEnumFORMATETC, IEnumSTATDATA, FORMATETC, STGMEDIUM, +}; + +#[repr(C)] +pub struct IUnknownVtbl { + pub QueryInterface: unsafe extern "system" fn( + This: *mut IUnknown, + riid: *const GUID, + ppvObject: *mut *mut c_void, + ) -> HRESULT, + pub AddRef: unsafe extern "system" fn(This: *mut IUnknown) -> u32, + pub Release: unsafe extern "system" fn(This: *mut IUnknown) -> u32, +} + +#[repr(C)] +pub struct IDataObjectVtbl { + pub parent: IUnknownVtbl, + pub GetData: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetcIn: *const FORMATETC, + pmedium: *mut STGMEDIUM, + ) -> HRESULT, + pub GetDataHere: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetc: *const FORMATETC, + pmedium: *mut STGMEDIUM, + ) -> HRESULT, + QueryGetData: + unsafe extern "system" fn(This: *mut IDataObject, pformatetc: *const FORMATETC) -> HRESULT, + pub GetCanonicalFormatEtc: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetcIn: *const FORMATETC, + pformatetcOut: *mut FORMATETC, + ) -> HRESULT, + pub SetData: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetc: *const FORMATETC, + pformatetcOut: *const FORMATETC, + fRelease: BOOL, + ) -> HRESULT, + pub EnumFormatEtc: unsafe extern "system" fn( + This: *mut IDataObject, + dwDirection: u32, + ppenumFormatEtc: *mut *mut IEnumFORMATETC, + ) -> HRESULT, + pub DAdvise: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetc: *const FORMATETC, + advf: u32, + pAdvSInk: *const IAdviseSink, + pdwConnection: *mut u32, + ) -> HRESULT, + pub DUnadvise: unsafe extern "system" fn(This: *mut IDataObject, dwConnection: u32) -> HRESULT, + pub EnumDAdvise: unsafe extern "system" fn( + This: *mut IDataObject, + ppenumAdvise: *const *const IEnumSTATDATA, + ) -> HRESULT, +} + +#[repr(C)] +pub struct IDropTargetVtbl { + pub parent: IUnknownVtbl, + pub DragEnter: unsafe extern "system" fn( + This: *mut IDropTarget, + pDataObj: *const IDataObject, + grfKeyState: u32, + pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT, + pub DragOver: unsafe extern "system" fn( + This: *mut IDropTarget, + grfKeyState: u32, + pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT, + pub DragLeave: unsafe extern "system" fn(This: *mut IDropTarget) -> HRESULT, + pub Drop: unsafe extern "system" fn( + This: *mut IDropTarget, + pDataObj: *const IDataObject, + grfKeyState: u32, + pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT, +} + +#[repr(C)] +pub struct IDropTarget { + pub lpVtbl: *const IDropTargetVtbl, +} + +#[repr(C)] +pub struct ITaskbarListVtbl { + pub parent: IUnknownVtbl, + pub HrInit: unsafe extern "system" fn(This: *mut ITaskbarList) -> HRESULT, + pub AddTab: unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, + pub DeleteTab: unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, + pub ActivateTab: unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, + pub SetActiveAlt: unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, +} + +#[repr(C)] +pub struct ITaskbarList { + pub lpVtbl: *const ITaskbarListVtbl, +} + +#[repr(C)] +pub struct ITaskbarList2Vtbl { + pub parent: ITaskbarListVtbl, + pub MarkFullscreenWindow: unsafe extern "system" fn( + This: *mut ITaskbarList2, + hwnd: HWND, + fFullscreen: BOOL, + ) -> HRESULT, +} + +#[repr(C)] +pub struct ITaskbarList2 { + pub lpVtbl: *const ITaskbarList2Vtbl, +} + +pub const CLSID_TaskbarList: GUID = GUID { + data1: 0x56fdf344, + data2: 0xfd6d, + data3: 0x11d0, + data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90], +}; + +pub const IID_ITaskbarList: GUID = GUID { + data1: 0x56fdf342, + data2: 0xfd6d, + data3: 0x11d0, + data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90], +}; + +pub const IID_ITaskbarList2: GUID = GUID { + data1: 0x602d4995, + data2: 0xb13a, + data3: 0x429b, + data4: [0xa6, 0x6e, 0x19, 0x35, 0xe4, 0x4f, 0x43, 0x17], +}; diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/dpi.rs b/third_party/winit-0.30.13/src/platform_impl/windows/dpi.rs new file mode 100644 index 0000000..5880069 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/dpi.rs @@ -0,0 +1,112 @@ +#![allow(non_snake_case, unused_unsafe)] + +use std::sync::Once; + +use windows_sys::Win32::Foundation::{HWND, S_OK}; +use windows_sys::Win32::Graphics::Gdi::{ + GetDC, GetDeviceCaps, MonitorFromWindow, HMONITOR, LOGPIXELSX, MONITOR_DEFAULTTONEAREST, +}; +use windows_sys::Win32::UI::HiDpi::{ + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, + MDT_EFFECTIVE_DPI, PROCESS_PER_MONITOR_DPI_AWARE, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::IsProcessDPIAware; + +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) + == false.into() + { + // V2 only works with Windows 10 Creators Update (1703). Try using the older + // V1 if we can't set V2. + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); + } + } else if let Some(SetProcessDpiAwareness) = *SET_PROCESS_DPI_AWARENESS { + // We are on Windows 8.1 or later. + SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); + } else if let Some(SetProcessDPIAware) = *SET_PROCESS_DPI_AWARE { + // We are on Vista or later. + SetProcessDPIAware(); + } + } + }); +} + +pub fn enable_non_client_dpi_scaling(hwnd: HWND) { + unsafe { + if let Some(EnableNonClientDpiScaling) = *ENABLE_NON_CLIENT_DPI_SCALING { + 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) == S_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 const BASE_DPI: u32 = 96; +pub fn dpi_to_scale_factor(dpi: u32) -> f64 { + dpi as f64 / BASE_DPI as f64 +} + +pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { + let hdc = unsafe { GetDC(hwnd) }; + if hdc == 0 { + panic!("[winit] `GetDC` returned null!"); + } + if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { + // We are on Windows 10 Anniversary Update (1607) or later. + match unsafe { GetDpiForWindow(hwnd) } { + 0 => BASE_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 = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + if monitor == 0 { + return BASE_DPI; + } + + let mut dpi_x = 0; + let mut dpi_y = 0; + if unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) } == S_OK { + dpi_x + } else { + BASE_DPI + } + } else { + // We are on Vista or later. + if unsafe { IsProcessDPIAware() } != false.into() { + // If the process is DPI aware, then scaling must be handled by the application using + // this DPI value. + unsafe { GetDeviceCaps(hdc, LOGPIXELSX as i32) 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. + BASE_DPI + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/drop_handler.rs b/third_party/winit-0.30.13/src/platform_impl/windows/drop_handler.rs new file mode 100644 index 0000000..38cb5c5 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/drop_handler.rs @@ -0,0 +1,237 @@ +use std::ffi::{c_void, OsString}; +use std::os::windows::ffi::OsStringExt; +use std::path::PathBuf; +use std::ptr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use windows_sys::core::{IUnknown, GUID, HRESULT}; +use windows_sys::Win32::Foundation::{DV_E_FORMATETC, HWND, POINTL, S_OK}; +use windows_sys::Win32::System::Com::{IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL}; +use windows_sys::Win32::System::Ole::{CF_HDROP, DROPEFFECT_COPY, DROPEFFECT_NONE}; +use windows_sys::Win32::UI::Shell::{DragFinish, DragQueryFileW, HDROP}; + +use tracing::debug; + +use crate::platform_impl::platform::definitions::{ + IDataObjectVtbl, IDropTarget, IDropTargetVtbl, IUnknownVtbl, +}; +use crate::platform_impl::platform::WindowId; + +use crate::event::Event; +use crate::window::WindowId as RootWindowId; + +#[repr(C)] +pub struct FileDropHandlerData { + pub interface: IDropTarget, + refcount: AtomicUsize, + window: HWND, + send_event: Box)>, + cursor_effect: u32, + hovered_is_valid: bool, /* If the currently hovered item is not valid there must not be any + * `HoveredFileCancelled` emitted */ +} + +pub struct FileDropHandler { + pub data: *mut FileDropHandlerData, +} + +#[allow(non_snake_case)] +impl FileDropHandler { + pub fn new(window: HWND, send_event: Box)>) -> FileDropHandler { + let data = Box::new(FileDropHandlerData { + interface: IDropTarget { lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl }, + refcount: AtomicUsize::new(1), + window, + send_event, + cursor_effect: DROPEFFECT_NONE, + hovered_is_valid: false, + }); + FileDropHandler { data: Box::into_raw(data) } + } + + // Implement IUnknown + pub unsafe extern "system" fn QueryInterface( + _this: *mut IUnknown, + _riid: *const GUID, + _ppvObject: *mut *mut c_void, + ) -> HRESULT { + // This function doesn't appear to be required for an `IDropTarget`. + // An implementation would be nice however. + unimplemented!(); + } + + pub unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { + let drop_handler_data = unsafe { Self::from_interface(this) }; + let count = drop_handler_data.refcount.fetch_add(1, Ordering::Release) + 1; + count as u32 + } + + pub unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { + let drop_handler = unsafe { Self::from_interface(this) }; + let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; + if count == 0 { + // Destroy the underlying data + drop(unsafe { Box::from_raw(drop_handler as *mut FileDropHandlerData) }); + } + count as u32 + } + + pub unsafe extern "system" fn DragEnter( + this: *mut IDropTarget, + pDataObj: *const IDataObject, + _grfKeyState: u32, + _pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT { + use crate::event::WindowEvent::HoveredFile; + let drop_handler = unsafe { Self::from_interface(this) }; + let hdrop = unsafe { + Self::iterate_filenames(pDataObj, |filename| { + drop_handler.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(drop_handler.window)), + event: HoveredFile(filename), + }); + }) + }; + drop_handler.hovered_is_valid = hdrop.is_some(); + drop_handler.cursor_effect = + if drop_handler.hovered_is_valid { DROPEFFECT_COPY } else { DROPEFFECT_NONE }; + unsafe { + *pdwEffect = drop_handler.cursor_effect; + } + + S_OK + } + + pub unsafe extern "system" fn DragOver( + this: *mut IDropTarget, + _grfKeyState: u32, + _pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT { + let drop_handler = unsafe { Self::from_interface(this) }; + unsafe { + *pdwEffect = drop_handler.cursor_effect; + } + + S_OK + } + + pub unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT { + use crate::event::WindowEvent::HoveredFileCancelled; + let drop_handler = unsafe { Self::from_interface(this) }; + if drop_handler.hovered_is_valid { + drop_handler.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(drop_handler.window)), + event: HoveredFileCancelled, + }); + } + + S_OK + } + + pub unsafe extern "system" fn Drop( + this: *mut IDropTarget, + pDataObj: *const IDataObject, + _grfKeyState: u32, + _pt: *const POINTL, + _pdwEffect: *mut u32, + ) -> HRESULT { + use crate::event::WindowEvent::DroppedFile; + let drop_handler = unsafe { Self::from_interface(this) }; + let hdrop = unsafe { + Self::iterate_filenames(pDataObj, |filename| { + drop_handler.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(drop_handler.window)), + event: DroppedFile(filename), + }); + }) + }; + if let Some(hdrop) = hdrop { + unsafe { DragFinish(hdrop) }; + } + + S_OK + } + + unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut FileDropHandlerData { + unsafe { &mut *(this as *mut _) } + } + + unsafe fn iterate_filenames(data_obj: *const IDataObject, callback: F) -> Option + where + F: Fn(PathBuf), + { + let drop_format = FORMATETC { + cfFormat: CF_HDROP, + ptd: ptr::null_mut(), + dwAspect: DVASPECT_CONTENT, + lindex: -1, + tymed: TYMED_HGLOBAL as u32, + }; + + let mut medium = unsafe { std::mem::zeroed() }; + let get_data_fn = unsafe { (*(*data_obj).cast::()).GetData }; + let get_data_result = unsafe { get_data_fn(data_obj as *mut _, &drop_format, &mut medium) }; + if get_data_result >= 0 { + let hdrop = unsafe { medium.u.hGlobal as HDROP }; + + // The second parameter (0xFFFFFFFF) instructs the function to return the item count + let item_count = unsafe { DragQueryFileW(hdrop, 0xffffffff, ptr::null_mut(), 0) }; + + 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 = + unsafe { DragQueryFileW(hdrop, i, ptr::null_mut(), 0) 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); + unsafe { + DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as u32); + path_buf.set_len(str_len); + } + + callback(OsString::from_wide(&path_buf[0..character_count]).into()); + } + + Some(hdrop) + } else if get_data_result == 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. + debug!("Error occurred while processing dropped/hovered item: item is not a file."); + None + } else { + debug!("Unexpected error occurred while processing dropped/hovered item."); + None + } + } +} + +impl FileDropHandlerData { + fn send_event(&self, event: Event<()>) { + (self.send_event)(event); + } +} + +impl Drop for FileDropHandler { + fn drop(&mut self) { + unsafe { + FileDropHandler::Release(self.data as *mut IUnknown); + } + } +} + +static DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl { + parent: IUnknownVtbl { + QueryInterface: FileDropHandler::QueryInterface, + AddRef: FileDropHandler::AddRef, + Release: FileDropHandler::Release, + }, + DragEnter: FileDropHandler::DragEnter, + DragOver: FileDropHandler::DragOver, + DragLeave: FileDropHandler::DragLeave, + Drop: FileDropHandler::Drop, +}; diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/event_loop.rs b/third_party/winit-0.30.13/src/platform_impl/windows/event_loop.rs new file mode 100644 index 0000000..6d60c1c --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/event_loop.rs @@ -0,0 +1,2676 @@ +#![allow(non_snake_case)] + +mod runner; + +use std::cell::Cell; +use std::collections::VecDeque; +use std::ffi::c_void; +use std::marker::PhantomData; +use std::os::windows::io::{AsRawHandle as _, FromRawHandle as _, OwnedHandle, RawHandle}; +use std::rc::Rc; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::time::{Duration, Instant}; +use std::{mem, panic, ptr}; + +use crate::utils::Lazy; + +use windows_sys::Win32::Devices::HumanInterfaceDevice::MOUSE_MOVE_RELATIVE; +use windows_sys::Win32::Foundation::{ + GetLastError, FALSE, HANDLE, HWND, LPARAM, LRESULT, POINT, RECT, WAIT_FAILED, WPARAM, +}; +use windows_sys::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MonitorFromRect, MonitorFromWindow, RedrawWindow, ScreenToClient, + ValidateRect, MONITORINFO, MONITOR_DEFAULTTONULL, RDW_INTERNALPAINT, SC_SCREENSAVE, +}; +use windows_sys::Win32::System::Ole::RevokeDragDrop; +use windows_sys::Win32::System::Threading::{ + CreateWaitableTimerExW, GetCurrentThreadId, SetWaitableTimer, + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, INFINITE, TIMER_ALL_ACCESS, +}; +use windows_sys::Win32::UI::Controls::{HOVER_DEFAULT, WM_MOUSELEAVE}; +use windows_sys::Win32::UI::Input::Ime::{GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE, TRACKMOUSEEVENT, +}; +use windows_sys::Win32::UI::Input::Pointer::{ + POINTER_FLAG_DOWN, POINTER_FLAG_UP, POINTER_FLAG_UPDATE, +}; +use windows_sys::Win32::UI::Input::Touch::{ + CloseTouchInputHandle, GetTouchInputInfo, TOUCHEVENTF_DOWN, TOUCHEVENTF_MOVE, TOUCHEVENTF_UP, + TOUCHINPUT, +}; +use windows_sys::Win32::UI::Input::{RAWINPUT, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetClientRect, GetCursorPos, + GetMenu, LoadCursorW, MsgWaitForMultipleObjectsEx, PeekMessageW, PostMessageW, + RegisterClassExW, RegisterWindowMessageA, SetCursor, SetWindowPos, TranslateMessage, + CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, GWL_USERDATA, HTCAPTION, HTCLIENT, + MINMAXINFO, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, NCCALCSIZE_PARAMS, PM_REMOVE, PT_PEN, + PT_TOUCH, QS_ALLINPUT, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE, + SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, + WMSZ_BOTTOM, WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, + WMSZ_TOPRIGHT, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, + WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, + WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, + WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, + WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, + WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, + WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, + WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, + WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, + WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, +}; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::error::EventLoopError; +use crate::event::{ + DeviceEvent, Event, Force, Ime, InnerSizeWriter, RawKeyEvent, Touch, TouchPhase, WindowEvent, +}; +use crate::event_loop::{ActiveEventLoop as RootAEL, ControlFlow, DeviceEvents, EventLoopClosed}; +use crate::keyboard::ModifiersState; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::platform::dark_mode::try_theme; +use crate::platform_impl::platform::dpi::{become_dpi_aware, dpi_to_scale_factor}; +use crate::platform_impl::platform::drop_handler::FileDropHandler; +use crate::platform_impl::platform::icon::WinCursor; +use crate::platform_impl::platform::ime::ImeContext; +use crate::platform_impl::platform::keyboard::KeyEventBuilder; +use crate::platform_impl::platform::keyboard_layout::LAYOUT_CACHE; +use crate::platform_impl::platform::monitor::{self, MonitorHandle}; +use crate::platform_impl::platform::window::InitData; +use crate::platform_impl::platform::window_state::{ + CursorFlags, ImeState, WindowFlags, WindowState, +}; +use crate::platform_impl::platform::{ + raw_input, util, wrap_device_id, Fullscreen, WindowId, DEVICE_ID, +}; +use crate::window::{ + CustomCursor as RootCustomCursor, CustomCursorSource, Theme, WindowId as RootWindowId, +}; +use runner::{EventLoopRunner, EventLoopRunnerShared}; + +use super::window::set_skip_taskbar; +use super::SelectedCursor; + +/// some backends like macos uses an uninhabited `Never` type, +/// on windows, `UserEvent`s are also dispatched through the +/// WNDPROC callback, and due to the re-entrant nature of the +/// callback, recursively delivered events must be queued in a +/// buffer, the current implementation put this queue in +/// `EventLoopRunner`, which is shared between the event pumping +/// loop and the callback. because it's hard to decide from the +/// outside whether a event needs to be buffered, I decided not +/// use `Event` for the shared runner state, but use unit +/// as a placeholder so user events can be buffered as usual, +/// the real `UserEvent` is pulled from the mpsc channel directly +/// when the placeholder event is delivered to the event handler +pub(crate) struct UserEventPlaceholder; + +// here below, the generic `EventLoopRunnerShared` is replaced with +// `EventLoopRunnerShared` so we can get rid +// of the generic parameter T in types which don't depend on T. +// this is the approach which requires minimum changes to current +// backend implementation. it should be considered transitional +// and should be refactored and cleaned up eventually, I hope. + +pub(crate) struct WindowData { + pub window_state: Arc>, + pub event_loop_runner: EventLoopRunnerShared, + pub key_event_builder: KeyEventBuilder, + pub _file_drop_handler: Option, + pub userdata_removed: Cell, + pub recurse_depth: Cell, +} + +impl WindowData { + fn send_event(&self, event: Event) { + self.event_loop_runner.send_event(event); + } + + fn window_state_lock(&self) -> MutexGuard<'_, WindowState> { + self.window_state.lock().unwrap() + } +} + +struct ThreadMsgTargetData { + event_loop_runner: EventLoopRunnerShared, +} + +impl ThreadMsgTargetData { + fn send_event(&self, event: Event) { + self.event_loop_runner.send_event(event); + } +} + +/// The result of a subclass procedure (the message handling callback) +#[derive(Clone, Copy)] +pub(crate) enum ProcResult { + DefWindowProc(WPARAM), + Value(isize), +} + +pub struct EventLoop { + user_event_sender: Sender, + user_event_receiver: Receiver, + window_target: RootAEL, + msg_hook: Option bool + 'static>>, + // It is a timer used on timed waits. + // It is created lazily in case if we have `ControlFlow::WaitUntil`. + // Keep it as a field to avoid recreating it on every `ControlFlow::WaitUntil`. + high_resolution_timer: Option, +} + +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) any_thread: bool, + pub(crate) dpi_aware: bool, + pub(crate) msg_hook: Option bool + 'static>>, +} + +impl Default for PlatformSpecificEventLoopAttributes { + fn default() -> Self { + Self { any_thread: false, dpi_aware: true, msg_hook: None } + } +} + +pub struct ActiveEventLoop { + thread_id: u32, + thread_msg_target: HWND, + pub(crate) runner_shared: EventLoopRunnerShared, +} + +impl EventLoop { + pub(crate) fn new( + attributes: &mut PlatformSpecificEventLoopAttributes, + ) -> Result { + 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(); + + let runner_shared = Rc::new(EventLoopRunner::new(thread_msg_target)); + + let (user_event_sender, user_event_receiver) = mpsc::channel(); + insert_event_target_window_data(thread_msg_target, runner_shared.clone()); + raw_input::register_all_mice_and_keyboards_for_raw_input( + thread_msg_target, + Default::default(), + ); + + Ok(EventLoop { + user_event_sender, + user_event_receiver, + window_target: RootAEL { + p: ActiveEventLoop { thread_id, thread_msg_target, runner_shared }, + _marker: PhantomData, + }, + msg_hook: attributes.msg_hook.take(), + high_resolution_timer: None, + }) + } + + pub fn window_target(&self) -> &RootAEL { + &self.window_target + } + + pub fn run(mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootAEL), + { + self.run_on_demand(event_handler) + } + + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootAEL), + { + { + let runner = &self.window_target.p.runner_shared; + + let event_loop_windows_ref = &self.window_target; + let user_event_receiver = &self.user_event_receiver; + // # Safety + // We make sure to call runner.clear_event_handler() before + // returning + unsafe { + runner.set_event_handler(move |event| { + // the shared `EventLoopRunner` is not parameterized + // `EventLoopProxy::send_event()` calls `PostMessage` + // to wakeup and dispatch a placeholder `UserEvent`, + // when we received the placeholder event here, the + // real UserEvent(T) should already be put in the + // mpsc channel and ready to be pulled + let event = match event.map_nonuser_event() { + Ok(non_user_event) => non_user_event, + Err(_user_event_placeholder) => Event::UserEvent( + user_event_receiver + .try_recv() + .expect("user event signaled but not received"), + ), + }; + event_handler(event, event_loop_windows_ref) + }); + } + } + + let exit_code = loop { + self.wait_for_messages(None); + // wait_for_messages calls user application before and after waiting + // so it may have decided to exit. + if let Some(code) = self.exit_code() { + break code; + } + + self.dispatch_peeked_messages(); + + if let Some(code) = self.exit_code() { + break code; + } + }; + + let runner = &self.window_target.p.runner_shared; + runner.loop_destroyed(); + + // # Safety + // We assume that this will effectively call `runner.clear_event_handler()` + // to meet the safety requirements for calling `runner.set_event_handler()` above. + runner.reset_runner(); + + if exit_code == 0 { + Ok(()) + } else { + Err(EventLoopError::ExitFailure(exit_code)) + } + } + + pub fn pump_events(&mut self, timeout: Option, mut event_handler: F) -> PumpStatus + where + F: FnMut(Event, &RootAEL), + { + { + let runner = &self.window_target.p.runner_shared; + let event_loop_windows_ref = &self.window_target; + let user_event_receiver = &self.user_event_receiver; + + // # Safety + // We make sure to call runner.clear_event_handler() before + // returning + // + // Note: we're currently assuming nothing can panic and unwind + // to leave the runner in an unsound state with an associated + // event handler. + unsafe { + runner.set_event_handler(move |event| { + let event = match event.map_nonuser_event() { + Ok(non_user_event) => non_user_event, + Err(_user_event_placeholder) => Event::UserEvent( + user_event_receiver + .recv() + .expect("user event signaled but not received"), + ), + }; + event_handler(event, event_loop_windows_ref) + }); + runner.wakeup(); + } + } + + if self.exit_code().is_none() { + self.wait_for_messages(timeout); + } + // wait_for_messages calls user application before and after waiting + // so it may have decided to exit. + if self.exit_code().is_none() { + self.dispatch_peeked_messages(); + } + + let runner = &self.window_target.p.runner_shared; + + let status = if let Some(code) = runner.exit_code() { + runner.loop_destroyed(); + + // Immediately reset the internal state for the loop to allow + // the loop to be run more than once. + runner.reset_runner(); + PumpStatus::Exit(code) + } else { + runner.prepare_wait(); + PumpStatus::Continue + }; + + // We wait until we've checked for an exit status before clearing the + // application callback, in case we need to dispatch a LoopExiting event + // + // # Safety + // This pairs up with our call to `runner.set_event_handler` and ensures + // the application's callback can't be held beyond its lifetime. + runner.clear_event_handler(); + + status + } + + /// Waits until new event messages arrive to be peeked. + /// Doesn't peek messages itself. + /// + /// Parameter timeout is optional. This method would wait for the smaller timeout + /// between the argument and a timeout from control flow. + fn wait_for_messages(&mut self, timeout: Option) { + let runner = &self.window_target.p.runner_shared; + + // We aim to be consistent with the MacOS backend which has a RunLoop + // observer that will dispatch AboutToWait when about to wait for + // events, and NewEvents after the RunLoop wakes up. + // + // We emulate similar behaviour by treating `MsgWaitForMultipleObjectsEx` as our wait + // point and wake up point (when it returns) and we drain all other + // pending messages via `PeekMessage` until we come back to "wait" via + // `MsgWaitForMultipleObjectsEx`. + // + runner.prepare_wait(); + wait_for_messages_impl(&mut self.high_resolution_timer, runner.control_flow(), timeout); + // Before we potentially exit, make sure to consistently emit an event for the wake up + runner.wakeup(); + } + + /// Dispatch all queued messages via `PeekMessageW` + fn dispatch_peeked_messages(&mut self) { + let runner = &self.window_target.p.runner_shared; + + // We generally want to continue dispatching all pending messages + // but we also allow dispatching to be interrupted as a means to + // ensure the `pump_events` won't indefinitely block an external + // event loop if there are too many pending events. This interrupt + // flag will be set after dispatching `RedrawRequested` events. + runner.interrupt_msg_dispatch.set(false); + + // # Safety + // The Windows API has no documented requirement for bitwise + // initializing a `MSG` struct (it can be uninitialized memory for the C + // API) and there's no API to construct or initialize a `MSG`. This + // is the simplest way avoid uninitialized memory in Rust + let mut msg: MSG = unsafe { mem::zeroed() }; + + loop { + unsafe { + if PeekMessageW(&mut msg, 0, 0, 0, PM_REMOVE) == false.into() { + break; + } + + let handled = if let Some(callback) = self.msg_hook.as_deref_mut() { + callback(&mut msg as *mut _ as *mut _) + } else { + false + }; + if !handled { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + + if let Err(payload) = runner.take_panic_error() { + runner.reset_runner(); + panic::resume_unwind(payload); + } + + if let Some(_code) = runner.exit_code() { + break; + } + + if runner.interrupt_msg_dispatch.get() { + break; + } + } + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + target_window: self.window_target.p.thread_msg_target, + event_send: self.user_event_sender.clone(), + } + } + + fn exit_code(&self) -> Option { + self.window_target.p.exit_code() + } +} + +impl ActiveEventLoop { + #[inline(always)] + pub(crate) fn create_thread_executor(&self) -> EventLoopThreadExecutor { + EventLoopThreadExecutor { thread_id: self.thread_id, target_window: self.thread_msg_target } + } + + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + let inner = match WinCursor::new(&source.inner.0) { + Ok(cursor) => cursor, + Err(err) => { + tracing::warn!("Failed to create custom cursor: {err}"); + WinCursor::Failed + }, + }; + + RootCustomCursor { inner } + } + + // 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(monitor) + } + + #[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 listen_device_events(&self, allowed: DeviceEvents) { + raw_input::register_all_mice_and_keyboards_for_raw_input(self.thread_msg_target, allowed); + } + + pub fn system_theme(&self) -> Option { + Some(if super::dark_mode::should_use_dark_mode() { Theme::Dark } else { Theme::Light }) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.runner_shared.set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.runner_shared.control_flow() + } + + pub(crate) fn exit(&self) { + self.runner_shared.set_exit_code(0) + } + + pub(crate) fn exiting(&self) -> bool { + self.runner_shared.exit_code().is_some() + } + + pub(crate) fn clear_exit(&self) { + self.runner_shared.clear_exit(); + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } + + fn exit_code(&self) -> Option { + self.runner_shared.exit_code() + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::WindowsDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::WindowsDisplayHandle::new().into()) + } +} + +/// Returns the id of the main thread. +/// +/// Windows has no real API to check if the current executing thread is the "main thread", unlike +/// macOS. +/// +/// Windows will let us look up the current thread's id, but there's no API that lets us check what +/// the id of the main thread is. We would somehow need to get the main thread's id before a +/// developer could spin off any other threads inside of the main entrypoint in order to emulate the +/// capabilities of other platforms. +/// +/// We can get the id of the main thread by using CRT initialization. CRT initialization can be used +/// to setup global state within a program. The OS will call a list of function pointers which +/// assign values to a static variable. To have get a hold of the main thread id, we need to place +/// our function pointer inside of the `.CRT$XCU` section so it is called before the main +/// entrypoint. +/// +/// Full details of CRT initialization can be found here: +/// +fn main_thread_id() -> u32 { + static mut MAIN_THREAD_ID: u32 = 0; + + // Function pointer used in CRT initialization section to set the above static field's value. + + // Mark as used so this is not removable. + #[used] + #[allow(non_upper_case_globals)] + // Place the function pointer inside of CRT initialization section so it is loaded before + // main entrypoint. + // + // See: https://doc.rust-lang.org/stable/reference/abi.html#the-link_section-attribute + #[link_section = ".CRT$XCU"] + static INIT_MAIN_THREAD_ID: unsafe extern "C" fn() = { + unsafe extern "C" fn initer() { + unsafe { + MAIN_THREAD_ID = GetCurrentThreadId(); + } + } + initer + }; + + unsafe { MAIN_THREAD_ID } +} + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout)))) +} + +// 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| { + if dur.subsec_nanos() % 1_000_000 > 0 { + ms.checked_add(1) + } else { + Some(ms) + } + }, + ) + .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 { + DestroyWindow(self.window_target.p.thread_msg_target); + } + } +} + +/// Set upper limit for waiting time to avoid overflows. +/// I chose 50 days as a limit because it is used in dur2timeout. +const FIFTY_DAYS: Duration = Duration::from_secs(50_u64 * 24 * 60 * 60); +/// Waitable timers use 100 ns intervals to indicate due time. +/// +/// And there is no point waiting using other ways for such small timings +/// because they are even less precise (can overshoot by few ms). +const MIN_WAIT: Duration = Duration::from_nanos(100); + +fn create_high_resolution_timer() -> Option { + unsafe { + let handle: HANDLE = CreateWaitableTimerExW( + ptr::null(), + ptr::null(), + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, + TIMER_ALL_ACCESS, + ); + // CREATE_WAITABLE_TIMER_HIGH_RESOLUTION is supported only after + // Win10 1803 but it is already default option for rustc + // (std uses it to implement `std::thread::sleep`). + if handle == 0 { + None + } else { + Some(OwnedHandle::from_raw_handle(handle as *mut c_void)) + } + } +} + +/// This function should not return error if parameters are valid +/// but there is no guarantee about that at MSDN docs +/// so we return result of GetLastError if fail. +/// +/// ## Safety +/// +/// timer must be a valid timer handle created by [create_high_resolution_timer]. +/// timeout divided by 100 nanoseconds must be more than 0 and less than i64::MAX. +unsafe fn set_high_resolution_timer(timer: RawHandle, timeout: Duration) -> Result<(), u32> { + const INTERVAL_NS: u32 = MIN_WAIT.subsec_nanos(); + const INTERVALS_IN_SEC: u64 = (Duration::from_secs(1).as_nanos() / INTERVAL_NS as u128) as u64; + let intervals_to_wait: u64 = + timeout.as_secs() * INTERVALS_IN_SEC + u64::from(timeout.subsec_nanos() / INTERVAL_NS); + debug_assert!(intervals_to_wait < i64::MAX as u64, "Must be called with smaller duration",); + // Use negative time to indicate relative time. + let due_time: i64 = -(intervals_to_wait as i64); + unsafe { + let set_result = SetWaitableTimer(timer as HANDLE, &due_time, 0, None, ptr::null(), FALSE); + if set_result != FALSE { + Ok(()) + } else { + Err(GetLastError()) + } + } +} + +/// Implementation detail of [EventLoop::wait_for_messages]. +/// +/// Does actual system-level waiting and doesn't process any messages itself, +/// including winits internal notifications about waiting and new messages arrival. +fn wait_for_messages_impl( + high_resolution_timer: &mut Option, + control_flow: ControlFlow, + timeout: Option, +) { + let timeout = { + let control_flow_timeout = match control_flow { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + let start = Instant::now(); + Some(wait_deadline.saturating_duration_since(start)) + }, + }; + let timeout = min_timeout(timeout, control_flow_timeout); + if timeout == Some(Duration::ZERO) { + // Do not wait if we don't have time. + return; + } + // Now we decided to wait so need to do some clamping + // to avoid problems with overflow and calling WinAPI with invalid parameters. + timeout + .map(|t| t.min(FIFTY_DAYS)) + // If timeout is less than minimally supported by Windows, + // increase it to that minimum. Who want less than microsecond delays anyway? + .map(|t| t.max(MIN_WAIT)) + }; + + if timeout.is_some() && high_resolution_timer.is_none() { + *high_resolution_timer = create_high_resolution_timer(); + } + + let high_resolution_timer: Option = + high_resolution_timer.as_ref().map(OwnedHandle::as_raw_handle); + + let use_timer: bool; + if let (Some(handle), Some(timeout)) = (high_resolution_timer, timeout) { + let res = unsafe { + // Safety: handle can be Some only if we succeeded in creating high resolution + // timer. We properly clamped timeout so it can be used as argument + // to timer. + set_high_resolution_timer(handle, timeout) + }; + if let Err(error_code) = res { + // We successfully got timer but failed to set it? + // Should be some bug in our code. + tracing::trace!("Failed to set high resolution timer: last error {}", error_code); + use_timer = false; + } else { + use_timer = true; + } + } else { + use_timer = false; + } + + unsafe { + // Either: + // 1. User wants to wait indefinitely if timeout is not set. + // 2. We failed to get and set high resolution timer and we need something instead of it. + let wait_duration_ms = timeout.map(dur2timeout).unwrap_or(INFINITE); + + let (num_handles, raw_handles) = + if use_timer { (1, [high_resolution_timer.unwrap()]) } else { (0, [ptr::null_mut()]) }; + + // We must use `QS_ALLINPUT` to wake on accessibility messages. + let result = MsgWaitForMultipleObjectsEx( + num_handles, + raw_handles.as_ptr() as *const _, + wait_duration_ms, + QS_ALLINPUT, + MWMO_INPUTAVAILABLE, + ); + if result == WAIT_FAILED { + // Well, nothing smart to do in such case. + // Treat it as spurious wake up. + tracing::warn!("Failed to MsgWaitForMultipleObjectsEx: error code {}", GetLastError(),); + } + } +} + +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 boxed2: ThreadExecFn = Box::new(Box::new(function)); + + let raw = Box::into_raw(boxed2); + + let res = PostMessageW(self.target_window, EXEC_MSG_ID.get(), raw as usize, 0); + assert!(res != false.into(), "PostMessage failed; is the messages queue full?"); + } + } + } +} + +type ThreadExecFn = Box>; + +pub struct EventLoopProxy { + target_window: HWND, + event_send: Sender, +} +unsafe impl Send 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> { + self.event_send + .send(event) + .map(|result| { + unsafe { PostMessageW(self.target_window, USER_EVENT_MSG_ID.get(), 0, 0) }; + result + }) + .map_err(|e| EventLoopClosed(e.0)) + } +} + +/// A lazily-initialized window message ID. +pub struct LazyMessageId { + /// The ID. + id: AtomicU32, + + /// The name of the message. + name: &'static str, +} + +/// An invalid custom window ID. +const INVALID_ID: u32 = 0x0; + +impl LazyMessageId { + /// Create a new `LazyId`. + const fn new(name: &'static str) -> Self { + Self { id: AtomicU32::new(INVALID_ID), name } + } + + /// Get the message ID. + pub fn get(&self) -> u32 { + // Load the ID. + let id = self.id.load(Ordering::Relaxed); + + if id != INVALID_ID { + return id; + } + + // Register the message. + // SAFETY: We are sure that the pointer is a valid C string ending with '\0'. + assert!(self.name.ends_with('\0')); + let new_id = unsafe { RegisterWindowMessageA(self.name.as_ptr()) }; + + assert_ne!( + new_id, + 0, + "RegisterWindowMessageA returned zero for '{}': {}", + self.name, + std::io::Error::last_os_error() + ); + + // Store the new ID. Since `RegisterWindowMessageA` returns the same value for any given + // string, the target value will always either be a). `INVALID_ID` or b). the + // correct ID. Therefore a compare-and-swap operation here (or really any + // consideration) is never necessary. + self.id.store(new_id, Ordering::Relaxed); + + new_id + } +} + +// Message sent by the `EventLoopProxy` when we want to wake up the thread. +// WPARAM and LPARAM are unused. +static USER_EVENT_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::WakeupMsg\0"); +// 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 EXEC_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::ExecMsg\0"); +// Message sent by a `Window` when it wants to be destroyed by the main thread. +// WPARAM and LPARAM are unused. +pub(crate) static DESTROY_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::DestroyMsg\0"); +// 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(crate) static SET_RETAIN_STATE_ON_SIZE_MSG_ID: LazyMessageId = + LazyMessageId::new("Winit::SetRetainMaximized\0"); +static THREAD_EVENT_TARGET_WINDOW_CLASS: Lazy> = + Lazy::new(|| util::encode_wide("Winit Thread Event Target")); +/// When the taskbar is created, it registers a message with the "TaskbarCreated" string and then +/// broadcasts this message to all top-level windows +pub(crate) static TASKBAR_CREATED: LazyMessageId = LazyMessageId::new("TaskbarCreated\0"); + +fn create_event_target_window() -> HWND { + use windows_sys::Win32::UI::WindowsAndMessaging::{CS_HREDRAW, CS_VREDRAW}; + unsafe { + let class = WNDCLASSEXW { + cbSize: mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(thread_event_target_callback), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: util::get_instance_handle(), + hIcon: 0, + hCursor: 0, // must be null in order for cursor state to work properly + hbrBackground: 0, + lpszMenuName: ptr::null(), + lpszClassName: THREAD_EVENT_TARGET_WINDOW_CLASS.as_ptr(), + hIconSm: 0, + }; + + RegisterClassExW(&class); + } + + unsafe { + // 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. + let window = CreateWindowExW( + WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW, + THREAD_EVENT_TARGET_WINDOW_CLASS.as_ptr(), + ptr::null(), + WS_OVERLAPPED, + 0, + 0, + 0, + 0, + 0, + 0, + util::get_instance_handle(), + ptr::null(), + ); + + super::set_window_long( + 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) as isize, + ); + window + } +} + +fn insert_event_target_window_data( + thread_msg_target: HWND, + event_loop_runner: EventLoopRunnerShared, +) { + let userdata = ThreadMsgTargetData { event_loop_runner }; + let input_ptr = Box::into_raw(Box::new(userdata)); + + unsafe { super::set_window_long(thread_msg_target, GWL_USERDATA, input_ptr as isize) }; +} + +/// 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; + unsafe { 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: 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); + unsafe { ReleaseCapture() }; + } +} + +fn normalize_pointer_pressure(pressure: u32) -> Option { + match pressure { + 1..=1024 => Some(Force::Normalized(pressure as f64 / 1024.0)), + _ => None, + } +} + +/// Emit a `ModifiersChanged` event whenever modifiers have changed. +/// Returns the current modifier state +fn update_modifiers(window: HWND, userdata: &WindowData) { + use crate::event::WindowEvent::ModifiersChanged; + + let modifiers = { + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + layouts.get_agnostic_mods() + }; + + let mut window_state = userdata.window_state.lock().unwrap(); + if window_state.modifiers_state != modifiers { + window_state.modifiers_state = modifiers; + + // Drop lock + drop(window_state); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ModifiersChanged(modifiers.into()), + }); + } +} + +unsafe fn gain_active_focus(window: HWND, userdata: &WindowData) { + use crate::event::WindowEvent::Focused; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Focused(true), + }); +} + +unsafe fn lose_active_focus(window: HWND, userdata: &WindowData) { + use crate::event::WindowEvent::{Focused, ModifiersChanged}; + + userdata.window_state_lock().modifiers_state = ModifiersState::empty(); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ModifiersChanged(ModifiersState::empty().into()), + }); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + 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 +pub(super) unsafe extern "system" fn public_window_callback( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + let userdata = unsafe { super::get_window_long(window, GWL_USERDATA) }; + + let userdata_ptr = match (userdata, msg) { + (0, WM_NCCREATE) => { + let createstruct = unsafe { &mut *(lparam as *mut CREATESTRUCTW) }; + let initdata = unsafe { &mut *(createstruct.lpCreateParams as *mut InitData<'_>) }; + + let result = match unsafe { initdata.on_nccreate(window) } { + Some(userdata) => unsafe { + super::set_window_long(window, GWL_USERDATA, userdata as _); + DefWindowProcW(window, msg, wparam, lparam) + }, + None => -1, // failed to create the window + }; + + return result; + }, + // Getting here should quite frankly be impossible, + // but we'll make window creation fail here just in case. + (0, WM_CREATE) => return -1, + (_, WM_CREATE) => unsafe { + let createstruct = &mut *(lparam as *mut CREATESTRUCTW); + let initdata = createstruct.lpCreateParams; + let initdata = &mut *(initdata as *mut InitData<'_>); + + initdata.on_create(); + return DefWindowProcW(window, msg, wparam, lparam); + }, + (0, _) => return unsafe { DefWindowProcW(window, msg, wparam, lparam) }, + _ => userdata as *mut WindowData, + }; + + let (result, userdata_removed, recurse_depth) = { + let userdata = unsafe { &*(userdata_ptr) }; + + userdata.recurse_depth.set(userdata.recurse_depth.get() + 1); + + let result = unsafe { public_window_callback_inner(window, msg, wparam, lparam, userdata) }; + + let userdata_removed = userdata.userdata_removed.get(); + let recurse_depth = userdata.recurse_depth.get() - 1; + userdata.recurse_depth.set(recurse_depth); + + (result, userdata_removed, recurse_depth) + }; + + if userdata_removed && recurse_depth == 0 { + drop(unsafe { Box::from_raw(userdata_ptr) }); + } + + result +} + +unsafe fn public_window_callback_inner( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + userdata: &WindowData, +) -> LRESULT { + let mut result = ProcResult::DefWindowProc(wparam); + + // Send new modifiers before sending key events. + let mods_changed_callback = || match msg { + WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP => { + update_modifiers(window, userdata); + result = ProcResult::Value(0); + }, + _ => (), + }; + userdata + .event_loop_runner + .catch_unwind(mods_changed_callback) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + + let keyboard_callback = || { + use crate::event::WindowEvent::KeyboardInput; + let events = + userdata.key_event_builder.process_message(window, msg, wparam, lparam, &mut result); + for event in events { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: KeyboardInput { + device_id: DEVICE_ID, + event: event.event, + is_synthetic: event.is_synthetic, + }, + }); + } + }; + userdata + .event_loop_runner + .catch_unwind(keyboard_callback) + .unwrap_or_else(|| result = ProcResult::Value(-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 indentation wouldn't change and + // the git blame and history would be preserved. + let callback = || match msg { + WM_NCCALCSIZE => { + let window_flags = userdata.window_state_lock().window_flags; + if wparam == 0 || window_flags.contains(WindowFlags::MARKER_DECORATIONS) { + result = ProcResult::DefWindowProc(wparam); + return; + } + + let params = unsafe { &mut *(lparam as *mut NCCALCSIZE_PARAMS) }; + + if util::is_maximized(window) { + // Limit the window size when maximized to the current monitor. + // Otherwise it would include the non-existent decorations. + // + // Use `MonitorFromRect` instead of `MonitorFromWindow` to select + // the correct monitor here. + // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/2549 + let monitor = unsafe { MonitorFromRect(¶ms.rgrc[0], MONITOR_DEFAULTTONULL) }; + if let Ok(monitor_info) = monitor::get_monitor_info(monitor) { + params.rgrc[0] = monitor_info.monitorInfo.rcWork; + } + } else if window_flags.contains(WindowFlags::MARKER_UNDECORATED_SHADOW) { + // Extend the client area to cover the whole non-client area. + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize#remarks + // + // HACK(msiglreith): To add the drop shadow we slightly tweak the non-client area. + // This leads to a small black 1px border on the top. Adding a margin manually + // on all 4 borders would result in the caption getting drawn by the DWM. + // + // Another option would be to allow the DWM to paint inside the client area. + // Unfortunately this results in janky resize behavior, where the compositor is + // ahead of the window surface. Currently, there seems no option to achieve this + // with the Windows API. + params.rgrc[0].top += 1; + params.rgrc[0].bottom += 1; + } + + result = ProcResult::Value(0); + }, + + WM_ENTERSIZEMOVE => { + userdata + .window_state_lock() + .set_window_flags_in_place(|f| f.insert(WindowFlags::MARKER_IN_SIZE_MOVE)); + result = ProcResult::Value(0); + }, + + WM_EXITSIZEMOVE => { + let mut state = userdata.window_state_lock(); + if state.dragging { + state.dragging = false; + unsafe { PostMessageW(window, WM_LBUTTONUP, 0, lparam) }; + } + + state.set_window_flags_in_place(|f| f.remove(WindowFlags::MARKER_IN_SIZE_MOVE)); + result = ProcResult::Value(0); + }, + + WM_NCLBUTTONDOWN => { + if wparam == HTCAPTION as _ { + // Prevent the user event loop from pausing when left clicking the title bar. + // + // When the user interacts with the title bar, Windows enters the modal event + // loop. Currently, a left click causes a pause for about 500ms. Sending a dummy + // mouse-move event seems to cancel the modal loop early, preventing the pause. + // The application will never see this dummy event. + // + // The mouse coordinates are encoded into the lparam value, however the WM_MOUSEMOVE + // event is not using the same coordinate system of the WM_NCLBUTTONDOWN event. + // One uses client-area coordinates and the other is screen-coordinates. In any + // case, passing the lparam as-is with the dummy event does not seem the cancel + // the modal loop. + // + // However, passing in a value of 0 has been observed to always cancel the pause. + // + // Other notes: + // + // For some unknown reason, the cursor will blink when clicking the title bar. + // Cancelling the modal loop early causes the blink to happen *immediately*. + // Otherwise, the blank happens *after* the pause. + // + // When right-click the title bar, the system window menu is presented to the user, + // and the modal event loop begins. This dummy event does *not* prevent the freeze + // in the main event loop caused by that popup menu. + let lparam = 0; + unsafe { PostMessageW(window, WM_MOUSEMOVE, 0, lparam) }; + } + result = ProcResult::DefWindowProc(wparam); + }, + + WM_CLOSE => { + use crate::event::WindowEvent::CloseRequested; + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CloseRequested, + }); + result = ProcResult::Value(0); + }, + + WM_DESTROY => { + use crate::event::WindowEvent::Destroyed; + unsafe { RevokeDragDrop(window) }; + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Destroyed, + }); + result = ProcResult::Value(0); + }, + + WM_NCDESTROY => { + unsafe { super::set_window_long(window, GWL_USERDATA, 0) }; + userdata.userdata_removed.set(true); + result = ProcResult::Value(0); + }, + + WM_PAINT => { + userdata.window_state_lock().redraw_requested = + userdata.event_loop_runner.should_buffer(); + + // We'll buffer only in response to `UpdateWindow`, if win32 decides to redraw the + // window outside the normal flow of the event loop. This way mark event as handled + // and request a normal redraw with `RedrawWindow`. + if !userdata.event_loop_runner.should_buffer() { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::RedrawRequested, + }); + } + + // NOTE: calling `RedrawWindow` during `WM_PAINT` does nothing, since to mark + // `WM_PAINT` as handled we should call the `DefWindowProcW`. Call it and check whether + // user asked for redraw during `RedrawRequested` event handling and request it again + // after marking `WM_PAINT` as handled. + result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) }); + if std::mem::take(&mut userdata.window_state_lock().redraw_requested) { + unsafe { RedrawWindow(window, ptr::null(), 0, RDW_INTERNALPAINT) }; + } + }, + WM_WINDOWPOSCHANGING => { + let mut window_state = userdata.window_state_lock(); + if let Some(ref mut fullscreen) = window_state.fullscreen { + let window_pos = unsafe { &mut *(lparam 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: u32 = SWP_NOMOVE | SWP_NOSIZE; + + let new_rect = if window_pos.flags & NOMOVE_OR_NOSIZE != 0 { + let cur_rect = util::WindowArea::Outer.get_rect(window).expect( + "Unexpected GetWindowRect failure; please report this error to \ + rust-windowing/winit", + ); + + 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 = unsafe { MonitorFromRect(&new_rect, MONITOR_DEFAULTTONULL) }; + match fullscreen { + Fullscreen::Borderless(ref mut fullscreen_monitor) => { + if new_monitor != 0 + && fullscreen_monitor + .as_ref() + .map(|monitor| new_monitor != monitor.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(MonitorHandle::new(new_monitor)); + } + }, + Fullscreen::Exclusive(ref video_mode) => { + let old_monitor = 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; + } + }, + } + } + } + + result = ProcResult::Value(0); + }, + + // WM_MOVE supplies client area positions, so we send Moved here instead. + WM_WINDOWPOSCHANGED => { + use crate::event::WindowEvent::Moved; + + let windowpos = lparam as *const WINDOWPOS; + if unsafe { (*windowpos).flags & SWP_NOMOVE != SWP_NOMOVE } { + let physical_position = + unsafe { PhysicalPosition::new((*windowpos).x, (*windowpos).y) }; + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Moved(physical_position), + }); + } + + // This is necessary for us to still get sent WM_SIZE. + result = ProcResult::DefWindowProc(wparam); + }, + + WM_SIZE => { + use crate::event::WindowEvent::Resized; + let w = super::loword(lparam as u32) as u32; + let h = super::hiword(lparam as u32) as u32; + + let physical_size = PhysicalSize::new(w, h); + let event = Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Resized(physical_size), + }; + + { + let mut w = userdata.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 == SIZE_MAXIMIZED as usize; + w.set_window_flags_in_place(|f| f.set(WindowFlags::MAXIMIZED, maximized)); + } + } + userdata.send_event(event); + result = ProcResult::Value(0); + }, + + WM_SIZING => { + /// Calculate the amount to add to round `value` to the nearest multiple of `increment`. + fn snap_to_nearest_increment_delta(value: i32, increment: i32) -> i32 { + let half_one = increment / 2; + let half_two = increment - half_one; + half_one - (value - half_two) % increment + } + + let scale_factor = userdata.window_state_lock().scale_factor; + let Some(inc) = userdata + .window_state_lock() + .resize_increments + .map(|inc| inc.to_physical(scale_factor)) + .filter(|inc| inc.width > 0 && inc.height > 0) + else { + result = ProcResult::Value(0); + return; + }; + + let side = wparam as u32; + // The desired new size of the window, decorations included. + let rect = unsafe { &mut *(lparam as *mut RECT) }; + + // We need to calculate the dimensions of the window decorations to get the true + // size of the window's contents + let adj_rect = userdata + .window_state_lock() + .window_flags + .adjust_rect(window, *rect) + .unwrap_or(*rect); + let deco_width = rect.left - adj_rect.left + adj_rect.right - rect.right; + let deco_height = rect.top - adj_rect.top + adj_rect.bottom - rect.bottom; + + let width = rect.right - rect.left - deco_width; + let height = rect.bottom - rect.top - deco_height; + + let mut width_delta = snap_to_nearest_increment_delta(width, inc.width); + let mut height_delta = snap_to_nearest_increment_delta(height, inc.height); + + // Windows won't bound check the value of `rect` after we're done here, so we + // have to check manually. If the width/height we snap to would go out of bounds, just + // set it equal to the min/max bound. + let min_size = + userdata.window_state_lock().min_size.map(|size| size.to_physical(scale_factor)); + let max_size = + userdata.window_state_lock().max_size.map(|size| size.to_physical(scale_factor)); + let final_width = width + width_delta; + let final_height = height + height_delta; + if let Some(min_size) = min_size { + if final_width < min_size.width { + width_delta += min_size.width - final_width; + } + if final_height < min_size.height { + height_delta += min_size.height - final_height; + } + } + if let Some(max_size) = max_size { + if final_width > max_size.width { + width_delta -= final_width - max_size.width; + } + if final_height > max_size.height { + height_delta -= final_height - max_size.height; + } + } + + match side { + WMSZ_LEFT | WMSZ_BOTTOMLEFT | WMSZ_TOPLEFT => { + rect.left -= width_delta; + }, + WMSZ_RIGHT | WMSZ_BOTTOMRIGHT | WMSZ_TOPRIGHT => { + rect.right += width_delta; + }, + _ => {}, + } + + match side { + WMSZ_TOP | WMSZ_TOPLEFT | WMSZ_TOPRIGHT => { + rect.top -= height_delta; + }, + WMSZ_BOTTOM | WMSZ_BOTTOMLEFT | WMSZ_BOTTOMRIGHT => { + rect.bottom += height_delta; + }, + _ => {}, + } + + result = ProcResult::DefWindowProc(wparam); + }, + + WM_MENUCHAR => { + result = ProcResult::Value((MNC_CLOSE << 16) as isize); + }, + + WM_IME_STARTCOMPOSITION => { + let ime_allowed = userdata.window_state_lock().ime_allowed; + if ime_allowed { + userdata.window_state_lock().ime_state = ImeState::Enabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Enabled), + }); + } + + result = ProcResult::DefWindowProc(wparam); + }, + + WM_IME_COMPOSITION => { + let ime_allowed_and_composing = { + let w = userdata.window_state_lock(); + w.ime_allowed && w.ime_state != ImeState::Disabled + }; + // Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so + // check whether composing. + if ime_allowed_and_composing { + let ime_context = unsafe { ImeContext::current(window) }; + + if lparam == 0 { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + } + + // Google Japanese Input and ATOK have both flags, so + // first, receive composing result if exist. + if (lparam as u32 & GCS_RESULTSTR) != 0 { + if let Some(text) = unsafe { ime_context.get_composed_text() } { + userdata.window_state_lock().ime_state = ImeState::Enabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Commit(text)), + }); + } + } + + // Next, receive preedit range for next composing if exist. + if (lparam as u32 & GCS_COMPSTR) != 0 { + if let Some((text, first, last)) = + unsafe { ime_context.get_composing_text_and_cursor() } + { + userdata.window_state_lock().ime_state = ImeState::Preedit; + let cursor_range = first.map(|f| (f, last.unwrap_or(f))); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(text, cursor_range)), + }); + } + } + } + + // Not calling DefWindowProc to hide composing text drawn by IME. + result = ProcResult::Value(0); + }, + + WM_IME_ENDCOMPOSITION => { + let ime_allowed_or_composing = { + let w = userdata.window_state_lock(); + w.ime_allowed || w.ime_state != ImeState::Disabled + }; + if ime_allowed_or_composing { + if userdata.window_state_lock().ime_state == ImeState::Preedit { + // Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so + // trying receiving composing result and commit if exists. + let ime_context = unsafe { ImeContext::current(window) }; + if let Some(text) = unsafe { ime_context.get_composed_text() } { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Commit(text)), + }); + } + } + + userdata.window_state_lock().ime_state = ImeState::Disabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Disabled), + }); + } + + result = ProcResult::DefWindowProc(wparam); + }, + + WM_IME_SETCONTEXT => { + // IME UI visibility flags are in lparam. + let lparam = lparam & !(ISC_SHOWUICOMPOSITIONWINDOW as isize); + result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) }); + }, + + // this is necessary for us to maintain minimize/restore state + WM_SYSCOMMAND => { + if wparam == SC_RESTORE as usize { + let mut w = userdata.window_state_lock(); + w.set_window_flags_in_place(|f| f.set(WindowFlags::MINIMIZED, false)); + } + if wparam == SC_MINIMIZE as usize { + let mut w = userdata.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 == SC_SCREENSAVE as usize { + let window_state = userdata.window_state_lock(); + if window_state.fullscreen.is_some() { + result = ProcResult::Value(0); + return; + } + } + + result = ProcResult::DefWindowProc(wparam); + }, + + WM_MOUSEMOVE => { + use crate::event::WindowEvent::{CursorEntered, CursorLeft, CursorMoved}; + + let x = super::get_x_lparam(lparam as u32) as i32; + let y = super::get_y_lparam(lparam as u32) as i32; + let position = PhysicalPosition::new(x as f64, y as f64); + + let cursor_moved; + { + let mut w = userdata.window_state_lock(); + let mouse_was_inside_window = + w.mouse.cursor_flags().contains(CursorFlags::IN_WINDOW); + + match get_pointer_move_kind(window, mouse_was_inside_window, x, y) { + PointerMoveKind::Enter => { + w.mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, true)) + .ok(); + + drop(w); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorEntered { device_id: DEVICE_ID }, + }); + + // Calling TrackMouseEvent in order to receive mouse leave events. + unsafe { + TrackMouseEvent(&mut TRACKMOUSEEVENT { + cbSize: mem::size_of::() as u32, + dwFlags: TME_LEAVE, + hwndTrack: window, + dwHoverTime: HOVER_DEFAULT, + }) + }; + }, + PointerMoveKind::Leave => { + w.mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, false)) + .ok(); + + drop(w); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorLeft { device_id: DEVICE_ID }, + }); + }, + PointerMoveKind::None => drop(w), + } + + // 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 = userdata.window_state_lock(); + cursor_moved = w.mouse.last_position != Some(position); + w.mouse.last_position = Some(position); + } + + if cursor_moved { + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorMoved { device_id: DEVICE_ID, position }, + }); + } + + result = ProcResult::Value(0); + }, + + WM_MOUSELEAVE => { + use crate::event::WindowEvent::CursorLeft; + { + let mut w = userdata.window_state_lock(); + w.mouse.set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, false)).ok(); + } + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorLeft { device_id: DEVICE_ID }, + }); + + result = ProcResult::Value(0); + }, + + WM_MOUSEWHEEL => { + use crate::event::MouseScrollDelta::LineDelta; + + let value = (wparam >> 16) as i16; + let value = value as f32 / WHEEL_DELTA as f32; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta: LineDelta(0.0, value), + phase: TouchPhase::Moved, + }, + }); + + result = ProcResult::Value(0); + }, + + WM_MOUSEHWHEEL => { + use crate::event::MouseScrollDelta::LineDelta; + + let value = (wparam >> 16) as i16; + let value = -value as f32 / WHEEL_DELTA as f32; // NOTE: inverted! See https://github.com/rust-windowing/winit/pull/2105/ + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta: LineDelta(value, 0.0), + phase: TouchPhase::Moved, + }, + }); + + result = ProcResult::Value(0); + }, + + WM_KEYDOWN | WM_SYSKEYDOWN => { + if msg == WM_SYSKEYDOWN { + result = ProcResult::DefWindowProc(wparam); + } + }, + + WM_KEYUP | WM_SYSKEYUP => { + if msg == WM_SYSKEYUP && unsafe { GetMenu(window) != 0 } { + // let Windows handle event if the window has a native menu, a modal event loop + // is started here on Alt key up. + result = ProcResult::DefWindowProc(wparam); + } + }, + + WM_LBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::Left; + use crate::event::WindowEvent::MouseInput; + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Pressed, button: Left }, + }); + result = ProcResult::Value(0); + }, + + WM_LBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::Left; + use crate::event::WindowEvent::MouseInput; + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Released, button: Left }, + }); + result = ProcResult::Value(0); + }, + + WM_RBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::Right; + use crate::event::WindowEvent::MouseInput; + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Pressed, button: Right }, + }); + result = ProcResult::Value(0); + }, + + WM_RBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::Right; + use crate::event::WindowEvent::MouseInput; + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Released, button: Right }, + }); + result = ProcResult::Value(0); + }, + + WM_MBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::Middle; + use crate::event::WindowEvent::MouseInput; + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Pressed, button: Middle }, + }); + result = ProcResult::Value(0); + }, + + WM_MBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::Middle; + use crate::event::WindowEvent::MouseInput; + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Released, button: Middle }, + }); + result = ProcResult::Value(0); + }, + + WM_XBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::{Back, Forward, Other}; + use crate::event::WindowEvent::MouseInput; + let xbutton = super::get_xbutton_wparam(wparam as u32); + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Pressed, + button: match xbutton { + 1 => Back, + 2 => Forward, + _ => Other(xbutton), + }, + }, + }); + result = ProcResult::Value(0); + }, + + WM_XBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::{Back, Forward, Other}; + use crate::event::WindowEvent::MouseInput; + let xbutton = super::get_xbutton_wparam(wparam as u32); + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Released, + button: match xbutton { + 1 => Back, + 2 => Forward, + _ => Other(xbutton), + }, + }, + }); + result = ProcResult::Value(0); + }, + + 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 != window { + userdata.window_state_lock().mouse.capture_count = 0; + } + result = ProcResult::Value(0); + }, + + WM_TOUCH => { + let pcount = super::loword(wparam as u32) as usize; + let mut inputs = Vec::with_capacity(pcount); + let htouch = lparam; + if unsafe { + GetTouchInputInfo( + htouch, + pcount as u32, + inputs.as_mut_ptr(), + mem::size_of::() as i32, + ) > 0 + } { + unsafe { inputs.set_len(pcount) }; + for input in &inputs { + let mut location = POINT { x: input.x / 100, y: input.y / 100 }; + + if unsafe { ScreenToClient(window, &mut location) } == false.into() { + 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); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Touch(Touch { + phase: if util::has_flag(input.dwFlags, TOUCHEVENTF_DOWN) { + TouchPhase::Started + } else if util::has_flag(input.dwFlags, TOUCHEVENTF_UP) { + TouchPhase::Ended + } else if util::has_flag(input.dwFlags, TOUCHEVENTF_MOVE) { + TouchPhase::Moved + } else { + continue; + }, + location, + force: None, // WM_TOUCH doesn't support pressure information + id: input.dwID as u64, + device_id: DEVICE_ID, + }), + }); + } + } + unsafe { CloseTouchInputHandle(htouch) }; + result = ProcResult::Value(0); + }, + + WM_POINTERDOWN | WM_POINTERUPDATE | WM_POINTERUP => { + if let ( + Some(GetPointerFrameInfoHistory), + Some(SkipPointerFrameMessages), + Some(GetPointerDeviceRects), + ) = ( + *util::GET_POINTER_FRAME_INFO_HISTORY, + *util::SKIP_POINTER_FRAME_MESSAGES, + *util::GET_POINTER_DEVICE_RECTS, + ) { + let pointer_id = super::loword(wparam as u32) as u32; + let mut entries_count = 0u32; + let mut pointers_count = 0u32; + if unsafe { + GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count, + &mut pointers_count, + ptr::null_mut(), + ) + } == false.into() + { + result = ProcResult::Value(0); + return; + } + + let pointer_info_count = (entries_count * pointers_count) as usize; + let mut pointer_infos = Vec::with_capacity(pointer_info_count); + if unsafe { + GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count, + &mut pointers_count, + pointer_infos.as_mut_ptr(), + ) + } == false.into() + { + result = ProcResult::Value(0); + return; + } + unsafe { 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 unsafe { + GetPointerDeviceRects( + pointer_info.sourceDevice, + device_rect.as_mut_ptr(), + display_rect.as_mut_ptr(), + ) + } == false.into() + { + continue; + } + + let device_rect = unsafe { device_rect.assume_init() }; + let display_rect = unsafe { 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 unsafe { ScreenToClient(window, &mut location) } == false.into() { + continue; + } + + let force = match pointer_info.pointerType { + PT_TOUCH => { + let mut touch_info = mem::MaybeUninit::uninit(); + util::GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| { + match unsafe { + GetPointerTouchInfo( + pointer_info.pointerId, + touch_info.as_mut_ptr(), + ) + } { + 0 => None, + _ => normalize_pointer_pressure(unsafe { + touch_info.assume_init().pressure + }), + } + }) + }, + PT_PEN => { + let mut pen_info = mem::MaybeUninit::uninit(); + util::GET_POINTER_PEN_INFO.and_then(|GetPointerPenInfo| { + match unsafe { + GetPointerPenInfo(pointer_info.pointerId, pen_info.as_mut_ptr()) + } { + 0 => None, + _ => normalize_pointer_pressure(unsafe { + pen_info.assume_init().pressure + }), + } + }) + }, + _ => None, + }; + + let x = location.x as f64 + x.fract(); + let y = location.y as f64 + y.fract(); + let location = PhysicalPosition::new(x, y); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Touch(Touch { + phase: if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_DOWN) { + TouchPhase::Started + } else if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_UP) { + TouchPhase::Ended + } else if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_UPDATE) + { + TouchPhase::Moved + } else { + continue; + }, + location, + force, + id: pointer_info.pointerId as u64, + device_id: DEVICE_ID, + }), + }); + } + + unsafe { SkipPointerFrameMessages(pointer_id) }; + } + result = ProcResult::Value(0); + }, + + WM_NCACTIVATE => { + let is_active = wparam != false.into(); + let active_focus_changed = userdata.window_state_lock().set_active(is_active); + if active_focus_changed { + if is_active { + unsafe { gain_active_focus(window, userdata) }; + } else { + unsafe { lose_active_focus(window, userdata) }; + } + } + result = ProcResult::DefWindowProc(wparam); + }, + + WM_SETFOCUS => { + let active_focus_changed = userdata.window_state_lock().set_focused(true); + if active_focus_changed { + unsafe { gain_active_focus(window, userdata) }; + } + result = ProcResult::Value(0); + }, + + WM_KILLFOCUS => { + let active_focus_changed = userdata.window_state_lock().set_focused(false); + if active_focus_changed { + unsafe { lose_active_focus(window, userdata) }; + } + result = ProcResult::Value(0); + }, + + WM_SETCURSOR => { + let set_cursor_to = { + let window_state = userdata.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 = super::loword(lparam as u32) as u32 == HTCLIENT; + if in_client_area { + Some(window_state.mouse.selected_cursor.clone()) + } else { + None + } + }; + + match set_cursor_to { + Some(selected_cursor) => { + let hcursor = match selected_cursor { + SelectedCursor::Named(cursor_icon) => unsafe { + LoadCursorW(0, util::to_windows_cursor(cursor_icon)) + }, + SelectedCursor::Custom(cursor) => cursor.as_raw_handle(), + }; + unsafe { SetCursor(hcursor) }; + result = ProcResult::Value(0); + }, + None => result = ProcResult::DefWindowProc(wparam), + } + }, + + WM_GETMINMAXINFO => { + let mmi = lparam as *mut MINMAXINFO; + + let window_state = userdata.window_state_lock(); + let window_flags = window_state.window_flags; + + if window_state.min_size.is_some() || window_state.max_size.is_some() { + if let Some(min_size) = window_state.min_size { + let min_size = min_size.to_physical(window_state.scale_factor); + let (width, height): (u32, u32) = + window_flags.adjust_size(window, min_size).into(); + unsafe { (*mmi).ptMinTrackSize = POINT { x: width as i32, y: height as i32 } }; + } + if let Some(max_size) = window_state.max_size { + let max_size = max_size.to_physical(window_state.scale_factor); + let (width, height): (u32, u32) = + window_flags.adjust_size(window, max_size).into(); + unsafe { (*mmi).ptMaxTrackSize = POINT { x: width as i32, y: height as i32 } }; + } + } + + result = ProcResult::Value(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. + 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 = super::loword(wparam as u32) as u32; + let new_scale_factor = dpi_to_scale_factor(new_dpi_x); + let old_scale_factor: f64; + + let (allow_resize, window_flags) = { + let mut window_state = userdata.window_state_lock(); + old_scale_factor = window_state.scale_factor; + window_state.scale_factor = new_scale_factor; + + if new_scale_factor == old_scale_factor { + result = ProcResult::Value(0); + return; + } + + let allow_resize = window_state.fullscreen.is_none() + && !window_state.window_flags().contains(WindowFlags::MAXIMIZED); + + (allow_resize, window_state.window_flags) + }; + + // New size as suggested by Windows. + let suggested_rect = unsafe { *(lparam 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 = + window_flags.adjust_rect(window, 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 = util::WindowArea::Inner + .get_rect(window) + .expect("failed to query (old) inner window area"); + 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 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, + }; + + let new_inner_size = Arc::new(Mutex::new(new_physical_inner_size)); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ScaleFactorChanged { + scale_factor: new_scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)), + }, + }); + + let new_physical_inner_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + + let dragging_window: bool; + + { + let window_state = userdata.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; + { + 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 = window_flags + .adjust_rect(window, 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 = unsafe { mem::zeroed() }; + unsafe { 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 = unsafe { MonitorFromWindow(window, MONITOR_DEFAULTTONULL) }; + let conservative_rect_monitor = + unsafe { MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL) }; + new_outer_rect = if conservative_rect_monitor == new_dpi_monitor { + conservative_rect + } else { + let get_monitor_rect = |monitor| { + let mut monitor_info = MONITORINFO { + cbSize: mem::size_of::() as _, + ..unsafe { mem::zeroed() } + }; + unsafe { 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. + #[allow(clippy::bool_to_int_with_if)] + 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 unsafe { MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL) } + == new_dpi_monitor + { + break; + } + } + + conservative_rect + }; + } + + unsafe { + SetWindowPos( + window, + 0, + 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(0); + }, + + WM_SETTINGCHANGE => { + use crate::event::WindowEvent::ThemeChanged; + + let preferred_theme = userdata.window_state_lock().preferred_theme; + + if preferred_theme.is_none() { + let new_theme = try_theme(window, preferred_theme); + let mut window_state = userdata.window_state_lock(); + + if window_state.current_theme != new_theme { + window_state.current_theme = new_theme; + drop(window_state); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ThemeChanged(new_theme), + }); + } + } + result = ProcResult::DefWindowProc(wparam); + }, + + _ => { + if msg == DESTROY_MSG_ID.get() { + unsafe { DestroyWindow(window) }; + result = ProcResult::Value(0); + } else if msg == SET_RETAIN_STATE_ON_SIZE_MSG_ID.get() { + let mut window_state = userdata.window_state_lock(); + window_state.set_window_flags_in_place(|f| { + f.set(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE, wparam != 0) + }); + result = ProcResult::Value(0); + } else if msg == TASKBAR_CREATED.get() { + let window_state = userdata.window_state_lock(); + unsafe { set_skip_taskbar(window, window_state.skip_taskbar) }; + result = ProcResult::DefWindowProc(wparam); + } else { + result = ProcResult::DefWindowProc(wparam); + } + }, + }; + + userdata + .event_loop_runner + .catch_unwind(callback) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + + match result { + ProcResult::DefWindowProc(wparam) => unsafe { DefWindowProcW(window, msg, wparam, lparam) }, + ProcResult::Value(val) => val, + } +} + +unsafe extern "system" fn thread_event_target_callback( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + let userdata_ptr = + unsafe { super::get_window_long(window, GWL_USERDATA) } as *mut ThreadMsgTargetData; + if userdata_ptr.is_null() { + // `userdata_ptr` will always be null for the first `WM_GETMINMAXINFO`, as well as + // `WM_NCCREATE` and `WM_CREATE`. + return unsafe { DefWindowProcW(window, msg, wparam, lparam) }; + } + let userdata = unsafe { Box::from_raw(userdata_ptr) }; + + if msg != WM_PAINT { + unsafe { RedrawWindow(window, ptr::null(), 0, RDW_INTERNALPAINT) }; + } + + let mut userdata_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 indentation wouldn't change and + // the git blame and history would be preserved. + let callback = || match msg { + WM_NCDESTROY => { + unsafe { super::set_window_long(window, GWL_USERDATA, 0) }; + userdata_removed = true; + 0 + }, + WM_PAINT => unsafe { + ValidateRect(window, ptr::null()); + // Default WM_PAINT behaviour. This makes sure modals and popups are shown immediately + // when opening them. + DefWindowProcW(window, msg, wparam, lparam) + }, + + WM_INPUT_DEVICE_CHANGE => { + let event = match wparam as u32 { + GIDC_ARRIVAL => DeviceEvent::Added, + GIDC_REMOVAL => DeviceEvent::Removed, + _ => unreachable!(), + }; + + userdata + .send_event(Event::DeviceEvent { device_id: wrap_device_id(lparam as u32), event }); + + 0 + }, + + WM_INPUT => { + if let Some(data) = raw_input::get_raw_input_data(lparam as _) { + unsafe { handle_raw_input(&userdata, data) }; + } + + unsafe { DefWindowProcW(window, msg, wparam, lparam) } + }, + + _ if msg == USER_EVENT_MSG_ID.get() => { + // synthesis a placeholder UserEvent, so that if the callback is + // re-entered it can be buffered for later delivery. the real + // user event is still in the mpsc channel and will be pulled + // once the placeholder event is delivered to the wrapper + // `event_handler` + userdata.send_event(Event::UserEvent(UserEventPlaceholder)); + 0 + }, + _ if msg == EXEC_MSG_ID.get() => { + let mut function: ThreadExecFn = unsafe { Box::from_raw(wparam as *mut _) }; + function(); + 0 + }, + _ => unsafe { DefWindowProcW(window, msg, wparam, lparam) }, + }; + + let result = userdata.event_loop_runner.catch_unwind(callback).unwrap_or(-1); + if userdata_removed { + drop(userdata); + } else { + Box::leak(userdata); + } + result +} + +unsafe fn handle_raw_input(userdata: &ThreadMsgTargetData, data: RAWINPUT) { + use crate::event::DeviceEvent::{Button, Key, Motion, MouseMotion, MouseWheel}; + use crate::event::ElementState::{Pressed, Released}; + use crate::event::MouseScrollDelta::LineDelta; + + let device_id = wrap_device_id(data.header.hDevice as _); + + if data.header.dwType == RIM_TYPEMOUSE { + let mouse = unsafe { data.data.mouse }; + + if util::has_flag(mouse.usFlags as u32, MOUSE_MOVE_RELATIVE) { + let x = mouse.lLastX as f64; + let y = mouse.lLastY as f64; + + if x != 0.0 { + userdata.send_event(Event::DeviceEvent { + device_id, + event: Motion { axis: 0, value: x }, + }); + } + + if y != 0.0 { + userdata.send_event(Event::DeviceEvent { + device_id, + event: Motion { axis: 1, value: y }, + }); + } + + if x != 0.0 || y != 0.0 { + userdata.send_event(Event::DeviceEvent { + device_id, + event: MouseMotion { delta: (x, y) }, + }); + } + } + + let button_flags = unsafe { mouse.Anonymous.Anonymous.usButtonFlags }; + if util::has_flag(button_flags as u32, RI_MOUSE_WHEEL) { + let button_data = unsafe { mouse.Anonymous.Anonymous.usButtonData } as i16; + let delta = button_data as f32 / WHEEL_DELTA as f32; + userdata.send_event(Event::DeviceEvent { + device_id, + event: MouseWheel { delta: LineDelta(0.0, delta) }, + }); + } + if util::has_flag(button_flags as u32, RI_MOUSE_HWHEEL) { + let button_data = unsafe { mouse.Anonymous.Anonymous.usButtonData } as i16; + let delta = -button_data as f32 / WHEEL_DELTA as f32; + userdata.send_event(Event::DeviceEvent { + device_id, + event: MouseWheel { delta: LineDelta(delta, 0.0) }, + }); + } + + let button_state = raw_input::get_raw_mouse_button_state(button_flags as u32); + for (button, state) in button_state.iter().enumerate() { + if let Some(state) = *state { + userdata.send_event(Event::DeviceEvent { + device_id, + event: Button { button: button as _, state }, + }); + } + } + } else if data.header.dwType == RIM_TYPEKEYBOARD { + let keyboard = unsafe { 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; + } + + if let Some(physical_key) = raw_input::get_keyboard_physical_key(keyboard) { + let state = if pressed { Pressed } else { Released }; + + userdata.send_event(Event::DeviceEvent { + device_id, + event: Key(RawKeyEvent { physical_key, state }), + }); + } + } +} + +enum PointerMoveKind { + /// Pointer entered to the window. + Enter, + /// Pointer leaved the window client area. + Leave, + /// Pointer is inside the window or `GetClientRect` failed. + None, +} + +fn get_pointer_move_kind( + window: HWND, + mouse_was_inside_window: bool, + x: i32, + y: i32, +) -> PointerMoveKind { + let rect: RECT = unsafe { + let mut rect: RECT = mem::zeroed(); + if GetClientRect(window, &mut rect) == false.into() { + return PointerMoveKind::None; // exit early if GetClientRect failed + } + rect + }; + + let x = (rect.left..rect.right).contains(&x); + let y = (rect.top..rect.bottom).contains(&y); + + if !mouse_was_inside_window && x && y { + PointerMoveKind::Enter + } else if mouse_was_inside_window && !(x && y) { + PointerMoveKind::Leave + } else { + PointerMoveKind::None + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/event_loop/runner.rs b/third_party/winit-0.30.13/src/platform_impl/windows/event_loop/runner.rs new file mode 100644 index 0000000..3243ec4 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/event_loop/runner.rs @@ -0,0 +1,406 @@ +use std::any::Any; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use std::time::Instant; +use std::{mem, panic}; + +use windows_sys::Win32::Foundation::HWND; + +use crate::dpi::PhysicalSize; +use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::platform_impl::platform::event_loop::{WindowData, GWL_USERDATA}; +use crate::platform_impl::platform::get_window_long; +use crate::window::WindowId; + +use super::ControlFlow; + +pub(crate) type EventLoopRunnerShared = Rc>; + +type EventHandler = Cell)>>>; + +pub(crate) struct EventLoopRunner { + // The event loop's win32 handles + pub(super) thread_msg_target: HWND, + + // Setting this will ensure pump_events will return to the external + // loop asap. E.g. set after each RedrawRequested to ensure pump_events + // can't stall an external loop beyond a frame + pub(super) interrupt_msg_dispatch: Cell, + + control_flow: Cell, + exit: Cell>, + runner_state: Cell, + last_events_cleared: Cell, + event_handler: EventHandler, + event_buffer: RefCell>>, + + 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)] +pub(crate) 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 `AboutToWait` hasn't. + HandlingMainEvents, + /// The event loop has been destroyed. No other events will be emitted. + Destroyed, +} + +enum BufferedEvent { + Event(Event), + ScaleFactorChanged(WindowId, f64, PhysicalSize), +} + +impl EventLoopRunner { + pub(crate) fn new(thread_msg_target: HWND) -> EventLoopRunner { + EventLoopRunner { + thread_msg_target, + interrupt_msg_dispatch: Cell::new(false), + runner_state: Cell::new(RunnerState::Uninitialized), + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), + panic_error: Cell::new(None), + last_events_cleared: Cell::new(Instant::now()), + event_handler: Cell::new(None), + event_buffer: RefCell::new(VecDeque::new()), + } + } + + /// Associate the application's event handler with the runner + /// + /// # Safety + /// This is ignoring the lifetime of the application handler (which may not + /// outlive the EventLoopRunner) and can lead to undefined behaviour if + /// the handler is not cleared before the end of real lifetime. + /// + /// All public APIs that take an event handler (`run`, `run_on_demand`, + /// `pump_events`) _must_ pair a call to `set_event_handler` with + /// a call to `clear_event_handler` before returning to avoid + /// undefined behaviour. + pub(crate) unsafe fn set_event_handler(&self, f: F) + where + F: FnMut(Event), + { + // Erase closure lifetime. + // SAFETY: Caller upholds that the lifetime of the closure is upheld. + let f = unsafe { + mem::transmute::)>, Box)>>(Box::new(f)) + }; + let old_event_handler = self.event_handler.replace(Some(f)); + assert!(old_event_handler.is_none()); + } + + pub(crate) fn clear_event_handler(&self) { + self.event_handler.set(None); + } + + pub(crate) fn reset_runner(&self) { + let EventLoopRunner { + thread_msg_target: _, + interrupt_msg_dispatch, + runner_state, + panic_error, + control_flow: _, + exit, + last_events_cleared: _, + event_handler, + event_buffer: _, + } = self; + interrupt_msg_dispatch.set(false); + runner_state.set(RunnerState::Uninitialized); + panic_error.set(None); + exit.set(None); + event_handler.set(None); + } +} + +/// State retrieval functions. +impl EventLoopRunner { + #[allow(unused)] + pub fn thread_msg_target(&self) -> HWND { + self.thread_msg_target + } + + pub fn take_panic_error(&self) -> Result<(), PanicError> { + match self.panic_error.take() { + Some(err) => Err(err), + None => Ok(()), + } + } + + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub fn exit_code(&self) -> Option { + self.exit.get() + } + + pub fn clear_exit(&self) { + self.exit.set(None); + } + + 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 + } + } +} + +/// Event dispatch functions. +impl EventLoopRunner { + pub(crate) fn prepare_wait(&self) { + self.move_state_to(RunnerState::Idle); + } + + pub(crate) fn wakeup(&self) { + self.move_state_to(RunnerState::HandlingMainEvents); + } + + pub(crate) fn send_event(&self, event: Event) { + if let Event::WindowEvent { event: WindowEvent::RedrawRequested, .. } = event { + self.call_event_handler(event); + // As a rule, to ensure that `pump_events` can't block an external event loop + // for too long, we always guarantee that `pump_events` will return control to + // the external loop asap after a `RedrawRequested` event is dispatched. + self.interrupt_msg_dispatch.set(true); + } 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.call_event_handler(event); + self.dispatch_buffered_events(); + } + } + + pub(crate) fn loop_destroyed(&self) { + self.move_state_to(RunnerState::Destroyed); + } + + fn call_event_handler(&self, event: Event) { + self.catch_unwind(|| { + let mut event_handler = self.event_handler.take().expect( + "either event handler is re-entrant (likely), or no event handler is registered \ + (very unlikely)", + ); + + event_handler(event); + + assert!(self.event_handler.replace(Some(event_handler)).is_none()); + }); + } + + 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`, `AboutToWait`, and + /// `LoopExiting`) as necessary to bring the internal `RunnerState` to the + /// new runner state. + /// + /// The state transitions are defined as follows: + /// + /// ```text + /// Uninitialized + /// | + /// V + /// Idle + /// ^ | + /// | V + /// HandlingMainEvents + /// | + /// V + /// Destroyed + /// ``` + /// + /// Attempting to transition back to `Uninitialized` will result in a panic. Attempting to + /// transition *from* `Destroyed` will also result 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. + fn move_state_to(&self, new_runner_state: RunnerState) { + use RunnerState::{Destroyed, HandlingMainEvents, Idle, Uninitialized}; + + match (self.runner_state.replace(new_runner_state), new_runner_state) { + (Uninitialized, Uninitialized) + | (Idle, Idle) + | (HandlingMainEvents, HandlingMainEvents) + | (Destroyed, Destroyed) => (), + + // State transitions that initialize the event loop. + (Uninitialized, HandlingMainEvents) => { + self.call_new_events(true); + }, + (Uninitialized, Idle) => { + self.call_new_events(true); + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + }, + (Uninitialized, Destroyed) => { + self.call_new_events(true); + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + self.call_event_handler(Event::LoopExiting); + }, + (_, Uninitialized) => panic!("cannot move state to Uninitialized"), + + // State transitions that start the event handling process. + (Idle, HandlingMainEvents) => { + self.call_new_events(false); + }, + (Idle, Destroyed) => { + self.call_event_handler(Event::LoopExiting); + }, + + (HandlingMainEvents, Idle) => { + // This is always the last event we dispatch before waiting for new events + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + }, + (HandlingMainEvents, Destroyed) => { + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + self.call_event_handler(Event::LoopExiting); + }, + + (Destroyed, _) => panic!("cannot move state from Destroyed"), + } + } + + fn call_new_events(&self, init: bool) { + let start_cause = match (init, self.control_flow(), self.exit.get()) { + (true, ..) => StartCause::Init, + (false, ControlFlow::Poll, None) => StartCause::Poll, + (false, _, Some(_)) | (false, ControlFlow::Wait, None) => StartCause::WaitCancelled { + requested_resume: None, + start: self.last_events_cleared.get(), + }, + (false, ControlFlow::WaitUntil(requested_resume), None) => { + 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)); + // NB: For consistency all platforms must emit a 'resumed' event even though Windows + // applications don't themselves have a formal suspend/resume lifecycle. + if init { + self.call_event_handler(Event::Resumed); + } + self.dispatch_buffered_events(); + } +} + +impl BufferedEvent { + pub fn from_event(event: Event) -> BufferedEvent { + match event { + Event::WindowEvent { + event: WindowEvent::ScaleFactorChanged { scale_factor, inner_size_writer }, + window_id, + } => BufferedEvent::ScaleFactorChanged( + window_id, + scale_factor, + *inner_size_writer.new_inner_size.upgrade().unwrap().lock().unwrap(), + ), + event => BufferedEvent::Event(event), + } + } + + pub fn dispatch_event(self, dispatch: impl FnOnce(Event)) { + match self { + Self::Event(event) => dispatch(event), + Self::ScaleFactorChanged(window_id, scale_factor, new_inner_size) => { + let user_new_inner_size = Arc::new(Mutex::new(new_inner_size)); + dispatch(Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &user_new_inner_size, + )), + }, + }); + let inner_size = *user_new_inner_size.lock().unwrap(); + + drop(user_new_inner_size); + + if inner_size != new_inner_size { + let window_flags = unsafe { + let userdata = + get_window_long(window_id.0.into(), GWL_USERDATA) as *mut WindowData; + (*userdata).window_state_lock().window_flags + }; + + window_flags.set_size((window_id.0).0, inner_size); + } + }, + } + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/icon.rs b/third_party/winit-0.30.13/src/platform_impl/windows/icon.rs new file mode 100644 index 0000000..4e5fe69 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/icon.rs @@ -0,0 +1,265 @@ +use std::ffi::c_void; +use std::path::Path; +use std::sync::Arc; +use std::{fmt, io, mem}; + +use cursor_icon::CursorIcon; +use windows_sys::core::PCWSTR; +use windows_sys::Win32::Foundation::HWND; +use windows_sys::Win32::Graphics::Gdi::{ + CreateBitmap, CreateCompatibleBitmap, DeleteObject, GetDC, ReleaseDC, SetBitmapBits, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateIcon, CreateIconIndirect, DestroyCursor, DestroyIcon, LoadImageW, SendMessageW, HCURSOR, + HICON, ICONINFO, ICON_BIG, ICON_SMALL, IMAGE_ICON, LR_DEFAULTSIZE, LR_LOADFROMFILE, WM_SETICON, +}; + +use crate::cursor::CursorImage; +use crate::dpi::PhysicalSize; +use crate::icon::*; + +use super::util; + +impl Pixel { + fn convert_to_bgra(&mut self) { + mem::swap(&mut self.r, &mut self.b); + } +} + +impl RgbaIcon { + fn into_windows_icon(self) -> Result { + let 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_ptr() as *mut Pixel, pixel_count) }; + for pixel in pixels { + and_mask.push(pixel.a.wrapping_sub(u8::MAX)); // invert alpha channel + pixel.convert_to_bgra(); + } + assert_eq!(and_mask.len(), pixel_count); + let handle = unsafe { + CreateIcon( + 0, + self.width as i32, + self.height as i32, + 1, + (PIXEL_SIZE * 8) as u8, + and_mask.as_ptr(), + rgba.as_ptr(), + ) + }; + if handle != 0 { + Ok(WinIcon::from_handle(handle)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } +} + +#[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 { + // 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 wide_path = util::encode_wide(path.as_ref()); + + let handle = unsafe { + LoadImageW( + 0, + wide_path.as_ptr(), + IMAGE_ICON, + width, + height, + LR_DEFAULTSIZE | LR_LOADFROMFILE, + ) + }; + if handle != 0 { + Ok(WinIcon::from_handle(handle as HICON)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } + + pub fn from_resource( + resource_id: u16, + size: Option>, + ) -> Result { + Self::from_resource_ptr(resource_id as PCWSTR, size) + } + + pub fn from_resource_name( + resource_name: &str, + size: Option>, + ) -> Result { + let wide_name = util::encode_wide(resource_name); + Self::from_resource_ptr(wide_name.as_ptr(), size) + } + + fn from_resource_ptr( + resource: PCWSTR, + 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( + util::get_instance_handle(), + resource, + IMAGE_ICON, + width, + height, + LR_DEFAULTSIZE, + ) + }; + if handle != 0 { + Ok(WinIcon::from_handle(handle as HICON)) + } else { + 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, icon_type as usize, self.as_raw_handle()); + } + } + + fn from_handle(handle: HICON) -> Self { + Self { inner: Arc::new(RaiiIcon { handle }) } + } +} + +impl Drop for RaiiIcon { + fn drop(&mut self) { + 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, icon_type as usize, 0); + } +} + +#[derive(Debug, Clone)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(Arc), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub enum WinCursor { + Cursor(Arc), + Failed, +} + +impl WinCursor { + pub(crate) fn new(image: &CursorImage) -> Result { + let mut bgra = image.rgba.clone(); + bgra.chunks_exact_mut(4).for_each(|chunk| chunk.swap(0, 2)); + + let w = image.width as i32; + let h = image.height as i32; + + unsafe { + let hdc_screen = GetDC(0); + if hdc_screen == 0 { + return Err(io::Error::last_os_error()); + } + let hbm_color = CreateCompatibleBitmap(hdc_screen, w, h); + ReleaseDC(0, hdc_screen); + if hbm_color == 0 { + return Err(io::Error::last_os_error()); + } + if SetBitmapBits(hbm_color, bgra.len() as u32, bgra.as_ptr() as *const c_void) == 0 { + DeleteObject(hbm_color); + return Err(io::Error::last_os_error()); + }; + + // Mask created according to https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createbitmap#parameters + let mask_bits: Vec = vec![0xff; ((((w + 15) >> 4) << 1) * h) as usize]; + let hbm_mask = CreateBitmap(w, h, 1, 1, mask_bits.as_ptr() as *const _); + if hbm_mask == 0 { + DeleteObject(hbm_color); + return Err(io::Error::last_os_error()); + } + + let icon_info = ICONINFO { + fIcon: 0, + xHotspot: image.hotspot_x as u32, + yHotspot: image.hotspot_y as u32, + hbmMask: hbm_mask, + hbmColor: hbm_color, + }; + + let handle = CreateIconIndirect(&icon_info as *const _); + DeleteObject(hbm_color); + DeleteObject(hbm_mask); + if handle == 0 { + return Err(io::Error::last_os_error()); + } + + Ok(Self::Cursor(Arc::new(RaiiCursor { handle }))) + } + } +} + +#[derive(Debug, Hash, Eq, PartialEq)] +pub struct RaiiCursor { + handle: HCURSOR, +} + +impl Drop for RaiiCursor { + fn drop(&mut self) { + unsafe { DestroyCursor(self.handle) }; + } +} + +impl RaiiCursor { + pub fn as_raw_handle(&self) -> HICON { + self.handle + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/ime.rs b/third_party/winit-0.30.13/src/platform_impl/windows/ime.rs new file mode 100644 index 0000000..eb65abf --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/ime.rs @@ -0,0 +1,162 @@ +use std::ffi::{c_void, OsString}; +use std::os::windows::prelude::OsStringExt; +use std::ptr::null_mut; + +use windows_sys::Win32::Foundation::{POINT, RECT}; +use windows_sys::Win32::Globalization::HIMC; +use windows_sys::Win32::UI::Input::Ime::{ + ImmAssociateContextEx, ImmGetCompositionStringW, ImmGetContext, ImmReleaseContext, + ImmSetCandidateWindow, ImmSetCompositionWindow, ATTR_TARGET_CONVERTED, + ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM, CFS_EXCLUDE, CFS_POINT, COMPOSITIONFORM, GCS_COMPATTR, + GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN, IACE_DEFAULT, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_IMMENABLED}; + +use crate::dpi::{Position, Size}; +use crate::platform::windows::HWND; + +pub struct ImeContext { + hwnd: HWND, + himc: HIMC, +} + +impl ImeContext { + pub unsafe fn current(hwnd: HWND) -> Self { + let himc = unsafe { ImmGetContext(hwnd) }; + ImeContext { hwnd, himc } + } + + pub unsafe fn get_composing_text_and_cursor( + &self, + ) -> Option<(String, Option, Option)> { + let text = unsafe { self.get_composition_string(GCS_COMPSTR) }?; + let attrs = unsafe { self.get_composition_data(GCS_COMPATTR) }.unwrap_or_default(); + + let mut first = None; + let mut last = None; + let mut boundary_before_char = 0; + let mut attr_idx = 0; + + for chr in text.chars() { + let Some(attr) = attrs.get(attr_idx).copied() else { + break; + }; + + let char_is_targeted = + attr as u32 == ATTR_TARGET_CONVERTED || attr as u32 == ATTR_TARGET_NOTCONVERTED; + + if first.is_none() && char_is_targeted { + first = Some(boundary_before_char); + } else if first.is_some() && last.is_none() && !char_is_targeted { + last = Some(boundary_before_char); + } + + boundary_before_char += chr.len_utf8(); + attr_idx += chr.len_utf16(); + } + + if first.is_some() && last.is_none() { + last = Some(text.len()); + } else if first.is_none() { + // IME haven't split words and select any clause yet, so trying to retrieve normal + // cursor. + let cursor = unsafe { self.get_composition_cursor(&text) }; + first = cursor; + last = cursor; + } + + Some((text, first, last)) + } + + pub unsafe fn get_composed_text(&self) -> Option { + unsafe { self.get_composition_string(GCS_RESULTSTR) } + } + + unsafe fn get_composition_cursor(&self, text: &str) -> Option { + let cursor = unsafe { ImmGetCompositionStringW(self.himc, GCS_CURSORPOS, null_mut(), 0) }; + (cursor >= 0).then(|| text.chars().take(cursor as _).map(|c| c.len_utf8()).sum()) + } + + unsafe fn get_composition_string(&self, gcs_mode: u32) -> Option { + let data = unsafe { self.get_composition_data(gcs_mode) }?; + let (prefix, shorts, suffix) = unsafe { data.align_to::() }; + if prefix.is_empty() && suffix.is_empty() { + OsString::from_wide(shorts).into_string().ok() + } else { + None + } + } + + unsafe fn get_composition_data(&self, gcs_mode: u32) -> Option> { + let size = match unsafe { ImmGetCompositionStringW(self.himc, gcs_mode, null_mut(), 0) } { + 0 => return Some(Vec::new()), + size if size < 0 => return None, + size => size, + }; + + let mut buf = Vec::::with_capacity(size as _); + let size = unsafe { + ImmGetCompositionStringW( + self.himc, + gcs_mode, + buf.as_mut_ptr() as *mut c_void, + size as _, + ) + }; + + if size < 0 { + None + } else { + unsafe { buf.set_len(size as _) }; + Some(buf) + } + } + + pub unsafe fn set_ime_cursor_area(&self, spot: Position, size: Size, scale_factor: f64) { + if !unsafe { ImeContext::system_has_ime() } { + return; + } + + let (x, y) = spot.to_physical::(scale_factor).into(); + let (width, height): (i32, i32) = size.to_physical::(scale_factor).into(); + let rc_area = RECT { left: x, top: y, right: x + width, bottom: y + height }; + let candidate_form = CANDIDATEFORM { + dwIndex: 0, + dwStyle: CFS_EXCLUDE, + ptCurrentPos: POINT { x, y }, + rcArea: rc_area, + }; + let composition_form = COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: POINT { x, y: y + height }, + rcArea: rc_area, + }; + + unsafe { + ImmSetCompositionWindow(self.himc, &composition_form); + ImmSetCandidateWindow(self.himc, &candidate_form); + } + } + + pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) { + if !unsafe { ImeContext::system_has_ime() } { + return; + } + + if allowed { + unsafe { ImmAssociateContextEx(hwnd, 0, IACE_DEFAULT) }; + } else { + unsafe { ImmAssociateContextEx(hwnd, 0, IACE_CHILDREN) }; + } + } + + unsafe fn system_has_ime() -> bool { + unsafe { GetSystemMetrics(SM_IMMENABLED) != 0 } + } +} + +impl Drop for ImeContext { + fn drop(&mut self) { + unsafe { ImmReleaseContext(self.hwnd, self.himc) }; + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/keyboard.rs b/third_party/winit-0.30.13/src/platform_impl/windows/keyboard.rs new file mode 100644 index 0000000..9c10949 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/keyboard.rs @@ -0,0 +1,1243 @@ +use std::char; +use std::ffi::OsString; +use std::mem::MaybeUninit; +use std::os::windows::ffi::OsStringExt; +use std::sync::atomic::AtomicU32; +use std::sync::atomic::Ordering::Relaxed; +use std::sync::{Mutex, MutexGuard}; + +use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM}; +use windows_sys::Win32::System::SystemServices::LANG_KOREAN; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + GetAsyncKeyState, GetKeyState, GetKeyboardLayout, GetKeyboardState, MapVirtualKeyExW, + MAPVK_VK_TO_VSC_EX, MAPVK_VSC_TO_VK_EX, VIRTUAL_KEY, VK_ABNT_C2, VK_ADD, VK_CAPITAL, VK_CLEAR, + VK_CONTROL, VK_DECIMAL, VK_DELETE, VK_DIVIDE, VK_DOWN, VK_END, VK_F4, VK_HOME, VK_INSERT, + VK_LCONTROL, VK_LEFT, VK_LMENU, VK_LSHIFT, VK_LWIN, VK_MENU, VK_MULTIPLY, VK_NEXT, VK_NUMLOCK, + VK_NUMPAD0, VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7, + VK_NUMPAD8, VK_NUMPAD9, VK_PRIOR, VK_RCONTROL, VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, + VK_RWIN, VK_SCROLL, VK_SHIFT, VK_SUBTRACT, VK_UP, +}; +use windows_sys::Win32::UI::TextServices::HKL; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + PeekMessageW, MSG, PM_NOREMOVE, WM_CHAR, WM_DEADCHAR, WM_KEYDOWN, WM_KEYFIRST, WM_KEYLAST, + WM_KEYUP, WM_KILLFOCUS, WM_SETFOCUS, WM_SYSCHAR, WM_SYSDEADCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, +}; + +use smol_str::SmolStr; +use tracing::{trace, warn}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::event::{ElementState, KeyEvent}; +use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; +use crate::platform_impl::platform::event_loop::ProcResult; +use crate::platform_impl::platform::keyboard_layout::{ + Layout, LayoutCache, WindowsModifiers, LAYOUT_CACHE, +}; +use crate::platform_impl::platform::{loword, primarylangid, KeyEventExtra}; + +pub type ExScancode = u16; + +pub struct MessageAsKeyEvent { + pub event: KeyEvent, + pub is_synthetic: bool, +} + +/// Stores information required to make `KeyEvent`s. +/// +/// A single Winit `KeyEvent` contains information which the Windows API passes to the application +/// in multiple window messages. In other words: a Winit `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. +pub struct KeyEventBuilder { + event_info: Mutex>, + pending: PendingEventQueue, +} +impl Default for KeyEventBuilder { + fn default() -> Self { + KeyEventBuilder { event_info: Mutex::new(None), pending: Default::default() } + } +} +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( + &self, + hwnd: HWND, + msg_kind: u32, + wparam: WPARAM, + lparam: LPARAM, + result: &mut ProcResult, + ) -> Vec { + enum MatchResult { + Nothing, + TokenToRemove(PendingMessageToken), + MessagesToDispatch(Vec), + } + + let mut matcher = || -> MatchResult { + match msg_kind { + WM_SETFOCUS => { + // synthesize keydown events + let kbd_state = get_async_kbd_state(); + let key_events = Self::synthesize_kbd_state(ElementState::Pressed, &kbd_state); + MatchResult::MessagesToDispatch(self.pending.complete_multi(key_events)) + }, + WM_KILLFOCUS => { + // synthesize keyup events + let kbd_state = get_kbd_state(); + let key_events = Self::synthesize_kbd_state(ElementState::Released, &kbd_state); + MatchResult::MessagesToDispatch(self.pending.complete_multi(key_events)) + }, + WM_KEYDOWN | WM_SYSKEYDOWN => { + if msg_kind == WM_SYSKEYDOWN && wparam as VIRTUAL_KEY == VK_F4 { + // Don't dispatch Alt+F4 to the application. + // This is handled in `event_loop.rs` + return MatchResult::Nothing; + } + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(0); + + let next_msg = next_kbd_msg(hwnd); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let mut finished_event_info = Some(PartialKeyEventInfo::from_message( + wparam, + lparam, + ElementState::Pressed, + &mut layouts, + )); + let mut event_info = self.event_info.lock().unwrap(); + *event_info = None; + if let Some(next_msg) = next_msg { + let next_msg_kind = next_msg.message; + let next_belongs_to_this = !matches!( + next_msg_kind, + WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP + ); + if next_belongs_to_this { + // The next OS event belongs to this Winit event, so let's just + // store the partial information, and add to it in the upcoming events + *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(); + return MatchResult::MessagesToDispatch(self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { event: ev, is_synthetic: false }, + )); + } + MatchResult::TokenToRemove(pending_token) + }, + WM_DEADCHAR | WM_SYSDEADCHAR => { + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(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.lock().unwrap().take().unwrap(); + let ev = event_info.finalize(); + MatchResult::MessagesToDispatch(self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { event: ev, is_synthetic: false }, + )) + }, + WM_CHAR | WM_SYSCHAR => { + let mut event_info = self.event_info.lock().unwrap(); + if event_info.is_none() { + trace!( + "Received a CHAR message but no `event_info` was available. The \ + message is probably IME, returning." + ); + return MatchResult::Nothing; + } + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(0); + let is_high_surrogate = (0xd800..=0xdbff).contains(&wparam); + let is_low_surrogate = (0xdc00..=0xdfff).contains(&wparam); + + let is_utf16 = is_high_surrogate || is_low_surrogate; + + if is_utf16 { + if let Some(ev_info) = event_info.as_mut() { + ev_info.utf16parts.push(wparam 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 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 MatchResult::TokenToRemove(pending_token); + }, + }; + let start_offset = utf16parts.len(); + let new_size = utf16parts.len() + 2; + utf16parts.resize(new_size, 0); + if let Some(ch) = char::from_u32(wparam 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); + } + } + // It's important that we unlock the mutex, and create the pending event token + // before calling `next_msg` + std::mem::drop(event_info); + let next_msg = next_kbd_msg(hwnd); + let more_char_coming = next_msg + .map(|m| matches!(m.message, WM_CHAR | WM_SYSCHAR)) + .unwrap_or(false); + if more_char_coming { + // No need to produce an event just yet, because there are still more + // characters that need to be appended to this keyboard event + MatchResult::TokenToRemove(pending_token) + } else { + let mut event_info = self.event_info.lock().unwrap(); + let mut event_info = match event_info.take() { + Some(ev_info) => ev_info, + None => { + warn!("The event_info was None when it was expected to be some"); + return MatchResult::TokenToRemove(pending_token); + }, + }; + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + // 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[VK_NUMLOCK as usize] & 1 != 0; + let vkey = event_info.vkey; + let physical_key = &event_info.physical_key; + let key = layout.get_key(mod_no_ctrl, num_lock_on, vkey, physical_key); + event_info.text = PartialText::Text(key.to_text().map(SmolStr::new)); + } + let ev = event_info.finalize(); + MatchResult::MessagesToDispatch(self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { event: ev, is_synthetic: false }, + )) + } + }, + WM_KEYUP | WM_SYSKEYUP => { + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(0); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let event_info = PartialKeyEventInfo::from_message( + wparam, + lparam, + ElementState::Released, + &mut layouts, + ); + // We MUST release the layout lock before calling `next_kbd_msg`, otherwise it + // may deadlock + drop(layouts); + // It's important that we create the pending token before reading the next + // message. + let next_msg = next_kbd_msg(hwnd); + let mut valid_event_info = Some(event_info); + if let Some(next_msg) = next_msg { + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + 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(); + return MatchResult::MessagesToDispatch(self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { event, is_synthetic: false }, + )); + } + MatchResult::TokenToRemove(pending_token) + }, + _ => MatchResult::Nothing, + } + }; + let matcher_result = matcher(); + match matcher_result { + MatchResult::TokenToRemove(t) => self.pending.remove_pending(t), + MatchResult::MessagesToDispatch(m) => m, + MatchResult::Nothing => Vec::new(), + } + } + + // Allowing nominimal_bool lint because the `is_key_pressed` macro triggers this warning + // and I don't know of another way to resolve it and also keeping the macro + #[allow(clippy::nonminimal_bool)] + fn synthesize_kbd_state( + key_state: ElementState, + kbd_state: &[u8; 256], + ) -> Vec { + let mut key_events = Vec::new(); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let (locale_id, _) = layouts.get_current_layout(); + + macro_rules! is_key_pressed { + ($vk:expr) => { + kbd_state[$vk as usize] & 0x80 != 0 + }; + } + + // Is caps-lock active? Note that this is different from caps-lock + // being held down. + let caps_lock_on = kbd_state[VK_CAPITAL as usize] & 1 != 0; + let num_lock_on = kbd_state[VK_NUMLOCK as usize] & 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 synthesize its keypress after every + // other key, in which case all other keys *must* be synthesized 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 synthesize + // 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 as HKL, + &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 { + match vk { + VK_CONTROL | VK_LCONTROL | VK_RCONTROL | VK_SHIFT | VK_LSHIFT | VK_RSHIFT + | VK_MENU | VK_LMENU | VK_RMENU | 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( + 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(vk as u32, MAPVK_VK_TO_VSC_EX, locale_id) }; + if scancode == 0 { + return None; + } + let scancode = scancode as ExScancode; + let physical_key = scancode_to_physicalkey(scancode as u32); + let mods = + if caps_lock_on { WindowsModifiers::CAPS_LOCK } else { WindowsModifiers::empty() }; + let layout = layouts.layouts.get(&(locale_id as u64)).unwrap(); + let logical_key = layout.get_key(mods, num_lock_on, vk, &physical_key); + let key_without_modifiers = + layout.get_key(WindowsModifiers::empty(), false, vk, &physical_key); + let text = if key_state == ElementState::Pressed { + logical_key.to_text().map(SmolStr::new) + } else { + None + }; + let event_info = PartialKeyEventInfo { + vkey: vk, + logical_key: PartialLogicalKey::This(logical_key.clone()), + key_without_modifiers, + key_state, + is_repeat: false, + physical_key, + location: get_location(scancode, locale_id), + utf16parts: Vec::with_capacity(8), + text: PartialText::Text(text.clone()), + }; + + let mut event = event_info.finalize(); + 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), +} + +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-perceived 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), + + /// Use the value directly provided by this variant + This(Key), +} + +struct PartialKeyEventInfo { + vkey: VIRTUAL_KEY, + key_state: ElementState, + is_repeat: bool, + physical_key: PhysicalKey, + location: KeyLocation, + logical_key: PartialLogicalKey, + + key_without_modifiers: Key, + + /// 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 = wparam as VIRTUAL_KEY; + 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(vkey as u32, MAPVK_VK_TO_VSC_EX, layout.hkl as HKL) as u16 } + } else { + new_ex_scancode(lparam_struct.scancode, lparam_struct.extended) + }; + let physical_key = scancode_to_physicalkey(scancode as u32); + let location = get_location(scancode, layout.hkl as HKL); + + 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[VK_NUMLOCK as usize] & 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 physical_key { + PhysicalKey::Code(KeyCode::NumLock) => Some(Key::Named(NamedKey::NumLock)), + PhysicalKey::Code(KeyCode::Pause) => Some(Key::Named(NamedKey::Pause)), + _ => None, + } + } else { + None + }; + + let preliminary_logical_key = + layout.get_key(mods_without_ctrl, num_lock_on, vkey, &physical_key); + 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, &physical_key) { + // 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 + // regular 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); + Key::Character(SmolStr::new(s)) + } else { + Key::Unidentified(NativeKey::Unidentified) + } + }, + key => key, + } + }; + + PartialKeyEventInfo { + vkey, + key_state: state, + logical_key, + key_without_modifiers, + is_repeat: lparam_struct.is_repeat, + physical_key, + location, + utf16parts: Vec::with_capacity(8), + text: PartialText::System(Vec::new()), + } + } + + fn finalize(self) -> 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() { + char_with_all_modifiers = Some(SmolStr::new(string)); + } + } + + // 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() { + text = Some(SmolStr::new(string)); + } + } + }, + PartialText::Text(s) => { + text = s.map(SmolStr::new); + }, + } + + let logical_key = match self.logical_key { + PartialLogicalKey::TextOr(fallback) => match text.as_ref() { + Some(s) => { + if s.grapheme_indices(true).count() > 1 { + fallback + } else { + Key::Character(s.clone()) + } + }, + None => Key::Unidentified(NativeKey::Windows(self.vkey)), + }, + PartialLogicalKey::This(v) => v, + }; + + KeyEvent { + physical_key: self.physical_key, + 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 >> 30) & 0x01; + let transition_state = (lparam >> 31) & 0x01; + KeyLParam { + scancode: ((lparam >> 16) & 0xff) as u8, + extended: ((lparam >> 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(); + GetKeyboardState(kbd_state.as_mut_ptr() as *mut u8); + 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 +#[allow(clippy::uninit_assumed_init)] +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 = vk as VIRTUAL_KEY; + let async_state = GetAsyncKeyState(vk as i32); + let is_down = (async_state & (1 << 15)) != 0; + *state = if is_down { 0x80 } else { 0 }; + + if matches!(vk, VK_CAPITAL | VK_NUMLOCK | VK_SCROLL) { + // Toggle states aren't reported by `GetAsyncKeyState` + let toggle_state = GetKeyState(vk as i32); + let is_active = (toggle_state & 1) != 0; + *state |= u8::from(is_active); + } + } + kbd_state + } +} + +/// On windows, AltGr == Ctrl + Alt +/// +/// Due to this equivalence, the system generates a fake Ctrl key-press (and key-release) preceding +/// 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::Named(NamedKey::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 +} + +enum PendingMessage { + Incomplete, + Complete(T), +} +struct IdentifiedPendingMessage { + token: PendingMessageToken, + msg: PendingMessage, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PendingMessageToken(u32); + +/// While processing keyboard events, we sometimes need +/// to call `PeekMessageW` (`next_msg`). But `PeekMessageW` +/// can also call the event handler, which means that the new event +/// gets processed before finishing to process the one that came before. +/// +/// This would mean that the application receives events in the wrong order. +/// To avoid this, we keep track whether we are in the middle of processing +/// an event. Such an event is an "incomplete pending event". A +/// "complete pending event" is one that has already finished processing, but +/// hasn't been dispatched to the application because there still are incomplete +/// pending events that came before it. +/// +/// When we finish processing an event, we call `complete_pending`, +/// which returns an empty array if there are incomplete pending events, but +/// if all pending events are complete, then it returns all pending events in +/// the order they were encountered. These can then be dispatched to the application +pub struct PendingEventQueue { + pending: Mutex>>, + next_id: AtomicU32, +} +impl PendingEventQueue { + /// Add a new pending event to the "pending queue" + pub fn add_pending(&self) -> PendingMessageToken { + let token = self.next_token(); + let mut pending = self.pending.lock().unwrap(); + pending.push(IdentifiedPendingMessage { token, msg: PendingMessage::Incomplete }); + token + } + + /// Returns all finished pending events + /// + /// If the return value is non empty, it's guaranteed to contain `msg` + /// + /// See also: `add_pending` + pub fn complete_pending(&self, token: PendingMessageToken, msg: T) -> Vec { + let mut pending = self.pending.lock().unwrap(); + let mut target_is_first = false; + for (i, pending_msg) in pending.iter_mut().enumerate() { + if pending_msg.token == token { + pending_msg.msg = PendingMessage::Complete(msg); + if i == 0 { + target_is_first = true; + } + break; + } + } + if target_is_first { + // If the message that we just finished was the first one in the pending queue, + // then we can empty the queue, and dispatch all of the messages. + Self::drain_pending(&mut *pending) + } else { + Vec::new() + } + } + + pub fn complete_multi(&self, msgs: Vec) -> Vec { + let mut pending = self.pending.lock().unwrap(); + if pending.is_empty() { + return msgs; + } + pending.reserve(msgs.len()); + for msg in msgs { + pending.push(IdentifiedPendingMessage { + token: self.next_token(), + msg: PendingMessage::Complete(msg), + }); + } + Vec::new() + } + + /// Returns all finished pending events + /// + /// It's safe to call this even if the element isn't in the list anymore + /// + /// See also: `add_pending` + pub fn remove_pending(&self, token: PendingMessageToken) -> Vec { + let mut pending = self.pending.lock().unwrap(); + let mut was_first = false; + if let Some(m) = pending.first() { + if m.token == token { + was_first = true; + } + } + pending.retain(|m| m.token != token); + if was_first { + Self::drain_pending(&mut *pending) + } else { + Vec::new() + } + } + + fn drain_pending(pending: &mut Vec>) -> Vec { + pending + .drain(..) + .map(|m| match m.msg { + PendingMessage::Complete(msg) => msg, + PendingMessage::Incomplete => { + panic!( + "Found an incomplete pending message when collecting messages. This \ + indicates a bug in winit." + ) + }, + }) + .collect() + } + + fn next_token(&self) -> PendingMessageToken { + // It's okay for the u32 to overflow here. Yes, that could mean + // that two different messages have the same token, + // but that would only happen after having about 4 billion + // messages sitting in the pending queue. + // + // In that case, having two identical tokens is the least of your concerns. + let id = self.next_id.fetch_add(1, Relaxed); + PendingMessageToken(id) + } +} +impl Default for PendingEventQueue { + fn default() -> Self { + PendingEventQueue { pending: Mutex::new(Vec::new()), next_id: AtomicU32::new(0) } + } +} + +/// WARNING: Due to using PeekMessage, the event handler +/// function may get called during this function. +/// (Re-entrance to the event handler) +/// +/// This can cause a deadlock if calling this function +/// while having a mutex locked. +/// +/// It can also cause code to get executed in a surprising order. +pub fn next_kbd_msg(hwnd: HWND) -> Option { + unsafe { + let mut next_msg = MaybeUninit::uninit(); + let peek_retval = + PeekMessageW(next_msg.as_mut_ptr(), hwnd, WM_KEYFIRST, WM_KEYLAST, PM_NOREMOVE); + (peek_retval != 0).then(|| next_msg.assume_init()) + } +} + +fn get_location(scancode: ExScancode, hkl: HKL) -> KeyLocation { + const ABNT_C2: VIRTUAL_KEY = VK_ABNT_C2 as VIRTUAL_KEY; + + let extension = 0xe000; + let extended = (scancode & extension) == extension; + let vkey = unsafe { MapVirtualKeyExW(scancode as u32, MAPVK_VSC_TO_VK_EX, hkl) as VIRTUAL_KEY }; + + // 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 { + VK_LSHIFT | VK_LCONTROL | VK_LMENU | VK_LWIN => KeyLocation::Left, + VK_RSHIFT | VK_RCONTROL | VK_RMENU | VK_RWIN => KeyLocation::Right, + VK_RETURN if extended => KeyLocation::Numpad, + VK_INSERT | VK_DELETE | VK_END | VK_DOWN | VK_NEXT | VK_LEFT | VK_CLEAR | VK_RIGHT + | VK_HOME | VK_UP | VK_PRIOR => { + if extended { + KeyLocation::Standard + } else { + KeyLocation::Numpad + } + }, + VK_NUMPAD0 | VK_NUMPAD1 | VK_NUMPAD2 | VK_NUMPAD3 | VK_NUMPAD4 | VK_NUMPAD5 + | VK_NUMPAD6 | VK_NUMPAD7 | VK_NUMPAD8 | VK_NUMPAD9 | VK_DECIMAL | VK_DIVIDE + | VK_MULTIPLY | VK_SUBTRACT | VK_ADD | ABNT_C2 => KeyLocation::Numpad, + _ => KeyLocation::Standard, + } +} + +pub(crate) fn physicalkey_to_scancode(physical_key: PhysicalKey) -> Option { + // See `scancode_to_physicalkey` for more info + + let hkl = unsafe { GetKeyboardLayout(0) }; + + let primary_lang_id = primarylangid(loword(hkl as u32)); + let is_korean = primary_lang_id as u32 == LANG_KOREAN; + + let code = match physical_key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => { + return match code { + NativeKeyCode::Windows(scancode) => Some(scancode as u32), + _ => None, + }; + }, + }; + + 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), + _ => None, + } +} + +pub(crate) fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey { + // 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 + + PhysicalKey::Code(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, + _ => return PhysicalKey::Unidentified(NativeKeyCode::Windows(scancode as u16)), + }) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/keyboard_layout.rs b/third_party/winit-0.30.13/src/platform_impl/windows/keyboard_layout.rs new file mode 100644 index 0000000..5340e43 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/keyboard_layout.rs @@ -0,0 +1,986 @@ +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; +use std::ffi::OsString; +use std::os::windows::ffi::OsStringExt; +use std::sync::Mutex; + +use crate::utils::Lazy; +use smol_str::SmolStr; +use windows_sys::Win32::System::SystemServices::{LANG_JAPANESE, LANG_KOREAN}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + GetKeyState, GetKeyboardLayout, MapVirtualKeyExW, ToUnicodeEx, MAPVK_VK_TO_VSC_EX, VIRTUAL_KEY, + VK_ACCEPT, VK_ADD, VK_APPS, VK_ATTN, VK_BACK, VK_BROWSER_BACK, VK_BROWSER_FAVORITES, + VK_BROWSER_FORWARD, VK_BROWSER_HOME, VK_BROWSER_REFRESH, VK_BROWSER_SEARCH, VK_BROWSER_STOP, + VK_CANCEL, VK_CAPITAL, VK_CLEAR, VK_CONTROL, VK_CONVERT, VK_CRSEL, VK_DECIMAL, VK_DELETE, + VK_DIVIDE, VK_DOWN, VK_END, VK_EREOF, VK_ESCAPE, VK_EXECUTE, VK_EXSEL, VK_F1, VK_F10, VK_F11, + VK_F12, VK_F13, VK_F14, VK_F15, VK_F16, VK_F17, VK_F18, VK_F19, VK_F2, VK_F20, VK_F21, VK_F22, + VK_F23, VK_F24, VK_F3, VK_F4, VK_F5, VK_F6, VK_F7, VK_F8, VK_F9, VK_FINAL, VK_GAMEPAD_A, + VK_GAMEPAD_B, VK_GAMEPAD_DPAD_DOWN, VK_GAMEPAD_DPAD_LEFT, VK_GAMEPAD_DPAD_RIGHT, + VK_GAMEPAD_DPAD_UP, VK_GAMEPAD_LEFT_SHOULDER, VK_GAMEPAD_LEFT_THUMBSTICK_BUTTON, + VK_GAMEPAD_LEFT_THUMBSTICK_DOWN, VK_GAMEPAD_LEFT_THUMBSTICK_LEFT, + VK_GAMEPAD_LEFT_THUMBSTICK_RIGHT, VK_GAMEPAD_LEFT_THUMBSTICK_UP, VK_GAMEPAD_LEFT_TRIGGER, + VK_GAMEPAD_MENU, VK_GAMEPAD_RIGHT_SHOULDER, VK_GAMEPAD_RIGHT_THUMBSTICK_BUTTON, + VK_GAMEPAD_RIGHT_THUMBSTICK_DOWN, VK_GAMEPAD_RIGHT_THUMBSTICK_LEFT, + VK_GAMEPAD_RIGHT_THUMBSTICK_RIGHT, VK_GAMEPAD_RIGHT_THUMBSTICK_UP, VK_GAMEPAD_RIGHT_TRIGGER, + VK_GAMEPAD_VIEW, VK_GAMEPAD_X, VK_GAMEPAD_Y, VK_HANGUL, VK_HANJA, VK_HELP, VK_HOME, VK_ICO_00, + VK_ICO_CLEAR, VK_ICO_HELP, VK_INSERT, VK_JUNJA, VK_KANA, VK_KANJI, VK_LAUNCH_APP1, + VK_LAUNCH_APP2, VK_LAUNCH_MAIL, VK_LAUNCH_MEDIA_SELECT, VK_LBUTTON, VK_LCONTROL, VK_LEFT, + VK_LMENU, VK_LSHIFT, VK_LWIN, VK_MBUTTON, VK_MEDIA_NEXT_TRACK, VK_MEDIA_PLAY_PAUSE, + VK_MEDIA_PREV_TRACK, VK_MEDIA_STOP, VK_MENU, VK_MODECHANGE, VK_MULTIPLY, VK_NAVIGATION_ACCEPT, + VK_NAVIGATION_CANCEL, VK_NAVIGATION_DOWN, VK_NAVIGATION_LEFT, VK_NAVIGATION_MENU, + VK_NAVIGATION_RIGHT, VK_NAVIGATION_UP, VK_NAVIGATION_VIEW, VK_NEXT, VK_NONAME, VK_NONCONVERT, + VK_NUMLOCK, VK_NUMPAD0, VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, + VK_NUMPAD7, VK_NUMPAD8, VK_NUMPAD9, VK_OEM_1, VK_OEM_102, VK_OEM_2, VK_OEM_3, VK_OEM_4, + VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_ATTN, VK_OEM_AUTO, VK_OEM_AX, VK_OEM_BACKTAB, + VK_OEM_CLEAR, VK_OEM_COMMA, VK_OEM_COPY, VK_OEM_CUSEL, VK_OEM_ENLW, VK_OEM_FINISH, + VK_OEM_FJ_LOYA, VK_OEM_FJ_MASSHOU, VK_OEM_FJ_ROYA, VK_OEM_FJ_TOUROKU, VK_OEM_JUMP, + VK_OEM_MINUS, VK_OEM_NEC_EQUAL, VK_OEM_PA1, VK_OEM_PA2, VK_OEM_PA3, VK_OEM_PERIOD, VK_OEM_PLUS, + VK_OEM_RESET, VK_OEM_WSCTRL, VK_PA1, VK_PACKET, VK_PAUSE, VK_PLAY, VK_PRINT, VK_PRIOR, + VK_PROCESSKEY, VK_RBUTTON, VK_RCONTROL, VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, VK_RWIN, + VK_SCROLL, VK_SELECT, VK_SEPARATOR, VK_SHIFT, VK_SLEEP, VK_SNAPSHOT, VK_SPACE, VK_SUBTRACT, + VK_TAB, VK_UP, VK_VOLUME_DOWN, VK_VOLUME_MUTE, VK_VOLUME_UP, VK_XBUTTON1, VK_XBUTTON2, VK_ZOOM, +}; +use windows_sys::Win32::UI::TextServices::HKL; + +use crate::keyboard::{Key, KeyCode, ModifiersState, NamedKey, NativeKey, PhysicalKey}; +use crate::platform_impl::{loword, primarylangid, scancode_to_physicalkey}; + +pub(crate) static LAYOUT_CACHE: Lazy> = + Lazy::new(|| Mutex::new(LayoutCache::default())); + +fn key_pressed(vkey: VIRTUAL_KEY) -> bool { + unsafe { (GetKeyState(vkey 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, +]; + +static NUMPAD_KEYCODES: Lazy> = Lazy::new(|| { + 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::bitflags! { + #[derive(Clone, Copy, PartialEq, Eq, 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[VK_SHIFT as usize] & 0x80 != 0; + let lshift = key_state[VK_LSHIFT as usize] & 0x80 != 0; + let rshift = key_state[VK_RSHIFT as usize] & 0x80 != 0; + + let control = key_state[VK_CONTROL as usize] & 0x80 != 0; + let lcontrol = key_state[VK_LCONTROL as usize] & 0x80 != 0; + let rcontrol = key_state[VK_RCONTROL as usize] & 0x80 != 0; + + let alt = key_state[VK_MENU as usize] & 0x80 != 0; + let lalt = key_state[VK_LMENU as usize] & 0x80 != 0; + let ralt = key_state[VK_RMENU as usize] & 0x80 != 0; + + let caps = key_state[VK_CAPITAL as usize] & 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[VK_SHIFT as usize] |= 0x80; + } else { + key_state[VK_SHIFT as usize] &= !0x80; + key_state[VK_LSHIFT as usize] &= !0x80; + key_state[VK_RSHIFT as usize] &= !0x80; + } + if self.intersects(Self::CONTROL) { + key_state[VK_CONTROL as usize] |= 0x80; + } else { + key_state[VK_CONTROL as usize] &= !0x80; + key_state[VK_LCONTROL as usize] &= !0x80; + key_state[VK_RCONTROL as usize] &= !0x80; + } + if self.intersects(Self::ALT) { + key_state[VK_MENU as usize] |= 0x80; + } else { + key_state[VK_MENU as usize] &= !0x80; + key_state[VK_LMENU as usize] &= !0x80; + key_state[VK_RMENU as usize] &= !0x80; + } + if self.intersects(Self::CAPS_LOCK) { + key_state[VK_CAPITAL as usize] |= 0x01; + } else { + key_state[VK_CAPITAL as usize] &= !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: u64, + + /// Maps numpad keys from Windows virtual key to a `Key`. + /// + /// This is useful because some numpad keys generate different characters 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, + physical_key: &PhysicalKey, + ) -> Key { + let native_code = NativeKey::Windows(vkey); + + 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.clone(), self.hkl, 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) { + return key.clone(); + } + } else if let Some(key) = self.numlock_off_keys.get(&vkey) { + return key.clone(); + } + if let PhysicalKey::Code(code) = physical_key { + if let Some(keys) = self.keys.get(&mods) { + if let Some(key) = keys.get(code) { + return key.clone(); + } + } + } + Key::Unidentified(native_code) + } +} + +#[derive(Default)] +pub(crate) struct LayoutCache { + /// Maps locale identifiers (HKL) to layouts + pub layouts: HashMap, +} + +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(&mut self) -> (u64, &Layout) { + let locale_id = unsafe { GetKeyboardLayout(0) } as u64; + match self.layouts.entry(locale_id) { + Entry::Occupied(entry) => (locale_id, entry.into_mut()), + Entry::Vacant(entry) => { + let layout = Self::prepare_layout(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(locale_id: u64) -> Layout { + let mut layout = Layout { + hkl: locale_id, + 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..256 { + let scancode = unsafe { MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + if scancode == 0 { + continue; + } + let keycode = match scancode_to_physicalkey(scancode) { + PhysicalKey::Code(code) => code, + // TODO: validate that we can skip on unidentified keys (probably never occurs?) + _ => continue, + }; + if !is_numpad_specific(vk as VIRTUAL_KEY) && NUMPAD_KEYCODES.contains(&keycode) { + let native_code = NativeKey::Windows(vk as VIRTUAL_KEY); + let map_vkey = keycode_to_vkey(keycode, locale_id); + if map_vkey == 0 { + continue; + } + let map_value = + vkey_to_non_char_key(vk as VIRTUAL_KEY, native_code, locale_id, false); + if matches!(map_value, Key::Unidentified(_)) { + continue; + } + layout.numlock_off_keys.insert(map_vkey, map_value); + } + } + + layout.numlock_on_keys.reserve(NUMPAD_VKEYS.len()); + for vk in NUMPAD_VKEYS.iter() { + let vk = (*vk) as u32; + let scancode = unsafe { MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + let unicode = Self::to_unicode_string(&key_state, vk, scancode, locale_id); + if let ToUnicodeResult::Str(s) = unicode { + layout.numlock_on_keys.insert(vk as VIRTUAL_KEY, Key::Character(SmolStr::new(s))); + } + } + + // 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_retain(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..256 { + let scancode = + unsafe { MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + if scancode == 0 { + continue; + } + + let native_code = NativeKey::Windows(vk as VIRTUAL_KEY); + let key_code = match scancode_to_physicalkey(scancode) { + PhysicalKey::Code(code) => code, + // TODO: validate that we can skip on unidentified keys (probably never occurs?) + _ => continue, + }; + // 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 as VIRTUAL_KEY, 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) => Key::Character(SmolStr::new(str)), + ToUnicodeResult::Dead(dead_char) => { + // 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(SmolStr::new("/")) + } 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 { + 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_retain(mod_state); + if let Some(keys) = layout.keys.get_mut(&mod_state) { + if let Some(key) = keys.get_mut(&KeyCode::AltRight) { + *key = Key::Named(NamedKey::AltGraph); + } + } + } + } + + layout + } + + fn to_unicode_string( + key_state: &[u8; 256], + vkey: u32, + scancode: u32, + locale_id: u64, + ) -> ToUnicodeResult { + unsafe { + let mut label_wide = [0u16; 8]; + let mut wide_len = ToUnicodeEx( + vkey, + scancode, + (&key_state[0]) as *const _, + (&mut label_wide[0]) as *mut _, + label_wide.len() as i32, + 0, + locale_id as HKL, + ); + if wide_len < 0 { + // If it's dead, we run `ToUnicode` again to consume the dead-key + wide_len = ToUnicodeEx( + vkey, + scancode, + (&key_state[0]) as *const _, + (&mut label_wide[0]) as *mut _, + label_wide.len() as i32, + 0, + locale_id as HKL, + ); + 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 + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +enum ToUnicodeResult { + Str(String), + Dead(Option), + None, +} + +fn is_numpad_specific(vk: VIRTUAL_KEY) -> bool { + matches!( + vk, + VK_NUMPAD0 + | VK_NUMPAD1 + | VK_NUMPAD2 + | VK_NUMPAD3 + | VK_NUMPAD4 + | VK_NUMPAD5 + | VK_NUMPAD6 + | VK_NUMPAD7 + | VK_NUMPAD8 + | VK_NUMPAD9 + | VK_ADD + | VK_SUBTRACT + | VK_DIVIDE + | VK_DECIMAL + | VK_SEPARATOR + ) +} + +fn keycode_to_vkey(keycode: KeyCode, hkl: u64) -> VIRTUAL_KEY { + let primary_lang_id = primarylangid(loword(hkl as u32)); + let is_korean = primary_lang_id as u32 == LANG_KOREAN; + let is_japanese = primary_lang_id as u32 == LANG_JAPANESE; + + match keycode { + KeyCode::Backquote => 0, + KeyCode::Backslash => 0, + KeyCode::BracketLeft => 0, + KeyCode::BracketRight => 0, + KeyCode::Comma => 0, + KeyCode::Digit0 => 0, + KeyCode::Digit1 => 0, + KeyCode::Digit2 => 0, + KeyCode::Digit3 => 0, + KeyCode::Digit4 => 0, + KeyCode::Digit5 => 0, + KeyCode::Digit6 => 0, + KeyCode::Digit7 => 0, + KeyCode::Digit8 => 0, + KeyCode::Digit9 => 0, + KeyCode::Equal => 0, + KeyCode::IntlBackslash => 0, + KeyCode::IntlRo => 0, + KeyCode::IntlYen => 0, + KeyCode::KeyA => 0, + KeyCode::KeyB => 0, + KeyCode::KeyC => 0, + KeyCode::KeyD => 0, + KeyCode::KeyE => 0, + KeyCode::KeyF => 0, + KeyCode::KeyG => 0, + KeyCode::KeyH => 0, + KeyCode::KeyI => 0, + KeyCode::KeyJ => 0, + KeyCode::KeyK => 0, + KeyCode::KeyL => 0, + KeyCode::KeyM => 0, + KeyCode::KeyN => 0, + KeyCode::KeyO => 0, + KeyCode::KeyP => 0, + KeyCode::KeyQ => 0, + KeyCode::KeyR => 0, + KeyCode::KeyS => 0, + KeyCode::KeyT => 0, + KeyCode::KeyU => 0, + KeyCode::KeyV => 0, + KeyCode::KeyW => 0, + KeyCode::KeyX => 0, + KeyCode::KeyY => 0, + KeyCode::KeyZ => 0, + KeyCode::Minus => 0, + KeyCode::Period => 0, + KeyCode::Quote => 0, + KeyCode::Semicolon => 0, + KeyCode::Slash => 0, + 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 => 0, + KeyCode::Lang3 if is_japanese => VK_OEM_FINISH, + KeyCode::Lang4 if is_japanese => 0, + KeyCode::Lang5 if is_japanese => 0, + 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 => 0, + KeyCode::NumpadComma => VK_SEPARATOR, + KeyCode::NumpadDecimal => VK_DECIMAL, + KeyCode::NumpadDivide => VK_DIVIDE, + KeyCode::NumpadEnter => VK_RETURN, + KeyCode::NumpadEqual => 0, + KeyCode::NumpadHash => 0, + KeyCode::NumpadMemoryAdd => 0, + KeyCode::NumpadMemoryClear => 0, + KeyCode::NumpadMemoryRecall => 0, + KeyCode::NumpadMemoryStore => 0, + KeyCode::NumpadMemorySubtract => 0, + KeyCode::NumpadMultiply => VK_MULTIPLY, + KeyCode::NumpadParenLeft => 0, + KeyCode::NumpadParenRight => 0, + KeyCode::NumpadStar => 0, + KeyCode::NumpadSubtract => VK_SUBTRACT, + KeyCode::Escape => VK_ESCAPE, + KeyCode::Fn => 0, + KeyCode::FnLock => 0, + 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 => 0, + 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 => 0, + KeyCode::Sleep => 0, + KeyCode::AudioVolumeDown => VK_VOLUME_DOWN, + KeyCode::AudioVolumeMute => VK_VOLUME_MUTE, + KeyCode::AudioVolumeUp => VK_VOLUME_UP, + KeyCode::WakeUp => 0, + KeyCode::Hyper => 0, + KeyCode::Turbo => 0, + KeyCode::Abort => 0, + KeyCode::Resume => 0, + KeyCode::Suspend => 0, + KeyCode::Again => 0, + KeyCode::Copy => 0, + KeyCode::Cut => 0, + KeyCode::Find => 0, + KeyCode::Open => 0, + KeyCode::Paste => 0, + KeyCode::Props => 0, + KeyCode::Select => VK_SELECT, + KeyCode::Undo => 0, + KeyCode::Hiragana => 0, + KeyCode::Katakana => 0, + 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 => 0, + KeyCode::F26 => 0, + KeyCode::F27 => 0, + KeyCode::F28 => 0, + KeyCode::F29 => 0, + KeyCode::F30 => 0, + KeyCode::F31 => 0, + KeyCode::F32 => 0, + KeyCode::F33 => 0, + KeyCode::F34 => 0, + KeyCode::F35 => 0, + _ => 0, + } +} + +/// 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: NativeKey, + hkl: u64, + has_alt_graph: bool, +) -> Key { + // 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 = primarylangid(loword(hkl as u32)); + let is_korean = primary_lang_id as u32 == LANG_KOREAN; + let is_japanese = primary_lang_id as u32 == LANG_JAPANESE; + + match vkey { + VK_LBUTTON => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_RBUTTON => Key::Unidentified(NativeKey::Unidentified), // Mouse + + // I don't think this can be represented with a Key + VK_CANCEL => Key::Unidentified(native_code), + + VK_MBUTTON => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_XBUTTON1 => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_XBUTTON2 => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_BACK => Key::Named(NamedKey::Backspace), + VK_TAB => Key::Named(NamedKey::Tab), + VK_CLEAR => Key::Named(NamedKey::Clear), + VK_RETURN => Key::Named(NamedKey::Enter), + VK_SHIFT => Key::Named(NamedKey::Shift), + VK_CONTROL => Key::Named(NamedKey::Control), + VK_MENU => Key::Named(NamedKey::Alt), + VK_PAUSE => Key::Named(NamedKey::Pause), + VK_CAPITAL => Key::Named(NamedKey::CapsLock), + + // VK_HANGEUL => Key::Named(NamedKey::HangulMode), // Deprecated in favour of VK_HANGUL + + // VK_HANGUL and VK_KANA are defined as the same constant, therefore + // we use appropriate conditions to differentiate between them + VK_HANGUL if is_korean => Key::Named(NamedKey::HangulMode), + VK_KANA if is_japanese => Key::Named(NamedKey::KanaMode), + + VK_JUNJA => Key::Named(NamedKey::JunjaMode), + VK_FINAL => Key::Named(NamedKey::FinalMode), + + // VK_HANJA and VK_KANJI are defined as the same constant, therefore + // we use appropriate conditions to differentiate between them + VK_HANJA if is_korean => Key::Named(NamedKey::HanjaMode), + VK_KANJI if is_japanese => Key::Named(NamedKey::KanjiMode), + + VK_ESCAPE => Key::Named(NamedKey::Escape), + VK_CONVERT => Key::Named(NamedKey::Convert), + VK_NONCONVERT => Key::Named(NamedKey::NonConvert), + VK_ACCEPT => Key::Named(NamedKey::Accept), + VK_MODECHANGE => Key::Named(NamedKey::ModeChange), + VK_SPACE => Key::Named(NamedKey::Space), + VK_PRIOR => Key::Named(NamedKey::PageUp), + VK_NEXT => Key::Named(NamedKey::PageDown), + VK_END => Key::Named(NamedKey::End), + VK_HOME => Key::Named(NamedKey::Home), + VK_LEFT => Key::Named(NamedKey::ArrowLeft), + VK_UP => Key::Named(NamedKey::ArrowUp), + VK_RIGHT => Key::Named(NamedKey::ArrowRight), + VK_DOWN => Key::Named(NamedKey::ArrowDown), + VK_SELECT => Key::Named(NamedKey::Select), + VK_PRINT => Key::Named(NamedKey::Print), + VK_EXECUTE => Key::Named(NamedKey::Execute), + VK_SNAPSHOT => Key::Named(NamedKey::PrintScreen), + VK_INSERT => Key::Named(NamedKey::Insert), + VK_DELETE => Key::Named(NamedKey::Delete), + VK_HELP => Key::Named(NamedKey::Help), + VK_LWIN => Key::Named(NamedKey::Super), + VK_RWIN => Key::Named(NamedKey::Super), + VK_APPS => Key::Named(NamedKey::ContextMenu), + VK_SLEEP => Key::Named(NamedKey::Standby), + + // Numpad keys produce characters + VK_NUMPAD0 => Key::Unidentified(native_code), + VK_NUMPAD1 => Key::Unidentified(native_code), + VK_NUMPAD2 => Key::Unidentified(native_code), + VK_NUMPAD3 => Key::Unidentified(native_code), + VK_NUMPAD4 => Key::Unidentified(native_code), + VK_NUMPAD5 => Key::Unidentified(native_code), + VK_NUMPAD6 => Key::Unidentified(native_code), + VK_NUMPAD7 => Key::Unidentified(native_code), + VK_NUMPAD8 => Key::Unidentified(native_code), + VK_NUMPAD9 => Key::Unidentified(native_code), + VK_MULTIPLY => Key::Unidentified(native_code), + VK_ADD => Key::Unidentified(native_code), + VK_SEPARATOR => Key::Unidentified(native_code), + VK_SUBTRACT => Key::Unidentified(native_code), + VK_DECIMAL => Key::Unidentified(native_code), + VK_DIVIDE => Key::Unidentified(native_code), + + VK_F1 => Key::Named(NamedKey::F1), + VK_F2 => Key::Named(NamedKey::F2), + VK_F3 => Key::Named(NamedKey::F3), + VK_F4 => Key::Named(NamedKey::F4), + VK_F5 => Key::Named(NamedKey::F5), + VK_F6 => Key::Named(NamedKey::F6), + VK_F7 => Key::Named(NamedKey::F7), + VK_F8 => Key::Named(NamedKey::F8), + VK_F9 => Key::Named(NamedKey::F9), + VK_F10 => Key::Named(NamedKey::F10), + VK_F11 => Key::Named(NamedKey::F11), + VK_F12 => Key::Named(NamedKey::F12), + VK_F13 => Key::Named(NamedKey::F13), + VK_F14 => Key::Named(NamedKey::F14), + VK_F15 => Key::Named(NamedKey::F15), + VK_F16 => Key::Named(NamedKey::F16), + VK_F17 => Key::Named(NamedKey::F17), + VK_F18 => Key::Named(NamedKey::F18), + VK_F19 => Key::Named(NamedKey::F19), + VK_F20 => Key::Named(NamedKey::F20), + VK_F21 => Key::Named(NamedKey::F21), + VK_F22 => Key::Named(NamedKey::F22), + VK_F23 => Key::Named(NamedKey::F23), + VK_F24 => Key::Named(NamedKey::F24), + VK_NAVIGATION_VIEW => Key::Unidentified(native_code), + VK_NAVIGATION_MENU => Key::Unidentified(native_code), + VK_NAVIGATION_UP => Key::Unidentified(native_code), + VK_NAVIGATION_DOWN => Key::Unidentified(native_code), + VK_NAVIGATION_LEFT => Key::Unidentified(native_code), + VK_NAVIGATION_RIGHT => Key::Unidentified(native_code), + VK_NAVIGATION_ACCEPT => Key::Unidentified(native_code), + VK_NAVIGATION_CANCEL => Key::Unidentified(native_code), + VK_NUMLOCK => Key::Named(NamedKey::NumLock), + VK_SCROLL => Key::Named(NamedKey::ScrollLock), + VK_OEM_NEC_EQUAL => Key::Unidentified(native_code), + // VK_OEM_FJ_JISHO => Key::Unidentified(native_code), // Conflicts with `VK_OEM_NEC_EQUAL` + VK_OEM_FJ_MASSHOU => Key::Unidentified(native_code), + VK_OEM_FJ_TOUROKU => Key::Unidentified(native_code), + VK_OEM_FJ_LOYA => Key::Unidentified(native_code), + VK_OEM_FJ_ROYA => Key::Unidentified(native_code), + VK_LSHIFT => Key::Named(NamedKey::Shift), + VK_RSHIFT => Key::Named(NamedKey::Shift), + VK_LCONTROL => Key::Named(NamedKey::Control), + VK_RCONTROL => Key::Named(NamedKey::Control), + VK_LMENU => Key::Named(NamedKey::Alt), + VK_RMENU => { + if has_alt_graph { + Key::Named(NamedKey::AltGraph) + } else { + Key::Named(NamedKey::Alt) + } + }, + VK_BROWSER_BACK => Key::Named(NamedKey::BrowserBack), + VK_BROWSER_FORWARD => Key::Named(NamedKey::BrowserForward), + VK_BROWSER_REFRESH => Key::Named(NamedKey::BrowserRefresh), + VK_BROWSER_STOP => Key::Named(NamedKey::BrowserStop), + VK_BROWSER_SEARCH => Key::Named(NamedKey::BrowserSearch), + VK_BROWSER_FAVORITES => Key::Named(NamedKey::BrowserFavorites), + VK_BROWSER_HOME => Key::Named(NamedKey::BrowserHome), + VK_VOLUME_MUTE => Key::Named(NamedKey::AudioVolumeMute), + VK_VOLUME_DOWN => Key::Named(NamedKey::AudioVolumeDown), + VK_VOLUME_UP => Key::Named(NamedKey::AudioVolumeUp), + VK_MEDIA_NEXT_TRACK => Key::Named(NamedKey::MediaTrackNext), + VK_MEDIA_PREV_TRACK => Key::Named(NamedKey::MediaTrackPrevious), + VK_MEDIA_STOP => Key::Named(NamedKey::MediaStop), + VK_MEDIA_PLAY_PAUSE => Key::Named(NamedKey::MediaPlayPause), + VK_LAUNCH_MAIL => Key::Named(NamedKey::LaunchMail), + VK_LAUNCH_MEDIA_SELECT => Key::Named(NamedKey::LaunchMediaPlayer), + VK_LAUNCH_APP1 => Key::Named(NamedKey::LaunchApplication1), + VK_LAUNCH_APP2 => Key::Named(NamedKey::LaunchApplication2), + + // This function only converts "non-printable" + VK_OEM_1 => Key::Unidentified(native_code), + VK_OEM_PLUS => Key::Unidentified(native_code), + VK_OEM_COMMA => Key::Unidentified(native_code), + VK_OEM_MINUS => Key::Unidentified(native_code), + VK_OEM_PERIOD => Key::Unidentified(native_code), + VK_OEM_2 => Key::Unidentified(native_code), + VK_OEM_3 => Key::Unidentified(native_code), + + VK_GAMEPAD_A => Key::Unidentified(native_code), + VK_GAMEPAD_B => Key::Unidentified(native_code), + VK_GAMEPAD_X => Key::Unidentified(native_code), + VK_GAMEPAD_Y => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_SHOULDER => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_SHOULDER => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_TRIGGER => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_TRIGGER => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_UP => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_DOWN => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_LEFT => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_RIGHT => Key::Unidentified(native_code), + VK_GAMEPAD_MENU => Key::Unidentified(native_code), + VK_GAMEPAD_VIEW => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_BUTTON => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_BUTTON => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_UP => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_DOWN => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_RIGHT => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_LEFT => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_UP => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_DOWN => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_RIGHT => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_LEFT => Key::Unidentified(native_code), + + // This function only converts "non-printable" + VK_OEM_4 => Key::Unidentified(native_code), + VK_OEM_5 => Key::Unidentified(native_code), + VK_OEM_6 => Key::Unidentified(native_code), + VK_OEM_7 => Key::Unidentified(native_code), + VK_OEM_8 => Key::Unidentified(native_code), + VK_OEM_AX => Key::Unidentified(native_code), + VK_OEM_102 => Key::Unidentified(native_code), + + VK_ICO_HELP => Key::Unidentified(native_code), + VK_ICO_00 => Key::Unidentified(native_code), + + VK_PROCESSKEY => Key::Named(NamedKey::Process), + + VK_ICO_CLEAR => Key::Unidentified(native_code), + VK_PACKET => Key::Unidentified(native_code), + VK_OEM_RESET => Key::Unidentified(native_code), + VK_OEM_JUMP => Key::Unidentified(native_code), + VK_OEM_PA1 => Key::Unidentified(native_code), + VK_OEM_PA2 => Key::Unidentified(native_code), + VK_OEM_PA3 => Key::Unidentified(native_code), + VK_OEM_WSCTRL => Key::Unidentified(native_code), + VK_OEM_CUSEL => Key::Unidentified(native_code), + + VK_OEM_ATTN => Key::Named(NamedKey::Attn), + VK_OEM_FINISH => { + if is_japanese { + Key::Named(NamedKey::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 `NamedKey::Finish` variant as + // Finish is not mentioned at https://w3c.github.io/uievents-key/ + // Also see: https://github.com/pyfisch/keyboard-types/issues/9 + Key::Unidentified(native_code) + } + }, + VK_OEM_COPY => Key::Named(NamedKey::Copy), + VK_OEM_AUTO => Key::Named(NamedKey::Hankaku), + VK_OEM_ENLW => Key::Named(NamedKey::Zenkaku), + VK_OEM_BACKTAB => Key::Named(NamedKey::Romaji), + VK_ATTN => Key::Named(NamedKey::KanaMode), + VK_CRSEL => Key::Named(NamedKey::CrSel), + VK_EXSEL => Key::Named(NamedKey::ExSel), + VK_EREOF => Key::Named(NamedKey::EraseEof), + VK_PLAY => Key::Named(NamedKey::Play), + VK_ZOOM => Key::Named(NamedKey::ZoomToggle), + VK_NONAME => Key::Unidentified(native_code), + VK_PA1 => Key::Unidentified(native_code), + VK_OEM_CLEAR => Key::Named(NamedKey::Clear), + _ => Key::Unidentified(native_code), + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/minimal_ime.rs b/third_party/winit-0.30.13/src/platform_impl/windows/minimal_ime.rs new file mode 100644 index 0000000..71600ab --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/minimal_ime.rs @@ -0,0 +1,67 @@ +use std::sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + Mutex, +}; + +use winapi::{ + shared::{ + minwindef::{LPARAM, WPARAM}, + windef::HWND, + }, + um::winuser, +}; + +use crate::platform_impl::platform::{event_loop::ProcResult, keyboard::next_kbd_msg}; + +pub struct MinimalIme { + // True if we're currently receiving messages belonging to a finished IME session. + getting_ime_text: AtomicBool, + + utf16parts: Mutex>, +} +impl Default for MinimalIme { + fn default() -> Self { + MinimalIme { + getting_ime_text: AtomicBool::new(false), + utf16parts: Mutex::new(Vec::with_capacity(16)), + } + } +} +impl MinimalIme { + pub(crate) fn process_message( + &self, + hwnd: HWND, + msg_kind: u32, + wparam: WPARAM, + _lparam: LPARAM, + result: &mut ProcResult, + ) -> Option { + match msg_kind { + winuser::WM_IME_ENDCOMPOSITION => { + self.getting_ime_text.store(true, Relaxed); + } + winuser::WM_CHAR | winuser::WM_SYSCHAR => { + if self.getting_ime_text.load(Relaxed) { + *result = ProcResult::Value(0); + self.utf16parts.lock().unwrap().push(wparam as u16); + // It's important that we push the new character and release the lock + // before getting the next message + let next_msg = next_kbd_msg(hwnd); + let more_char_coming = next_msg + .map(|m| matches!(m.message, winuser::WM_CHAR | winuser::WM_SYSCHAR)) + .unwrap_or(false); + if !more_char_coming { + let mut utf16parts = self.utf16parts.lock().unwrap(); + let result = String::from_utf16(&utf16parts).ok(); + utf16parts.clear(); + self.getting_ime_text.store(false, Relaxed); + return result; + } + } + } + _ => (), + } + + None + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/mod.rs b/third_party/winit-0.30.13/src/platform_impl/windows/mod.rs new file mode 100644 index 0000000..9bb02fd --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/mod.rs @@ -0,0 +1,196 @@ +use smol_str::SmolStr; +use windows_sys::Win32::Foundation::{HANDLE, HWND}; +use windows_sys::Win32::UI::WindowsAndMessaging::{HMENU, WINDOW_LONG_PTR_INDEX}; + +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub(crate) use self::icon::{SelectedCursor, WinIcon}; +pub(crate) use self::keyboard::{physicalkey_to_scancode, scancode_to_physicalkey}; +pub(crate) use self::monitor::{MonitorHandle, VideoModeHandle}; +pub(crate) use self::window::Window; + +pub(crate) use self::icon::WinCursor as PlatformCustomCursor; +pub use self::icon::WinIcon as PlatformIcon; +pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSource; +use crate::platform_impl::Fullscreen; + +use crate::event::DeviceId as RootDeviceId; +use crate::icon::Icon; +use crate::keyboard::Key; +use crate::platform::windows::{BackdropType, Color, CornerPreference}; + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + pub owner: Option, + pub menu: Option, + pub taskbar_icon: Option, + pub no_redirection_bitmap: bool, + pub drag_and_drop: bool, + pub skip_taskbar: bool, + pub class_name: String, + pub decoration_shadow: bool, + pub backdrop_type: BackdropType, + pub clip_children: bool, + pub border_color: Option, + pub title_background_color: Option, + pub title_text_color: Option, + pub corner_preference: Option, +} + +impl Default for PlatformSpecificWindowAttributes { + fn default() -> Self { + Self { + owner: None, + menu: None, + taskbar_icon: None, + no_redirection_bitmap: false, + drag_and_drop: true, + skip_taskbar: false, + class_name: "Window Class".to_string(), + decoration_shadow: false, + backdrop_type: BackdropType::default(), + clip_children: true, + border_color: None, + title_background_color: None, + title_text_color: None, + corner_preference: None, + } + } +} + +unsafe impl Send for PlatformSpecificWindowAttributes {} +unsafe impl Sync for PlatformSpecificWindowAttributes {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(u32); + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId(0) + } +} + +impl DeviceId { + pub fn persistent_identifier(&self) -> Option { + if self.0 != 0 { + raw_input::get_raw_input_device_name(self.0 as HANDLE) + } else { + None + } + } +} + +// 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: u32) -> RootDeviceId { + RootDeviceId(DeviceId(id)) +} + +pub type OsError = std::io::Error; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option, + pub key_without_modifiers: Key, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(HWND); +unsafe impl Send for WindowId {} +unsafe impl Sync for WindowId {} + +impl WindowId { + pub const fn dummy() -> Self { + WindowId(0) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for HWND { + fn from(window_id: WindowId) -> Self { + window_id.0 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as HWND) + } +} + +#[inline(always)] +const fn get_xbutton_wparam(x: u32) -> u16 { + hiword(x) +} + +#[inline(always)] +const fn get_x_lparam(x: u32) -> i16 { + loword(x) as _ +} + +#[inline(always)] +const fn get_y_lparam(x: u32) -> i16 { + hiword(x) as _ +} + +#[inline(always)] +pub(crate) const fn primarylangid(lgid: u16) -> u16 { + lgid & 0x3ff +} + +#[inline(always)] +pub(crate) const fn loword(x: u32) -> u16 { + (x & 0xffff) as u16 +} + +#[inline(always)] +const fn hiword(x: u32) -> u16 { + ((x >> 16) & 0xffff) as u16 +} + +#[inline(always)] +unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize { + #[cfg(target_pointer_width = "64")] + return unsafe { windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW(hwnd, nindex) }; + #[cfg(target_pointer_width = "32")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongW(hwnd, nindex) as isize + }; +} + +#[inline(always)] +unsafe fn set_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX, dwnewlong: isize) -> isize { + #[cfg(target_pointer_width = "64")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW(hwnd, nindex, dwnewlong) + }; + #[cfg(target_pointer_width = "32")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongW(hwnd, nindex, dwnewlong as i32) + as isize + }; +} + +#[macro_use] +mod util; +mod dark_mode; +mod definitions; +mod dpi; +mod drop_handler; +mod event_loop; +mod icon; +mod ime; +mod keyboard; +mod keyboard_layout; +mod monitor; +mod raw_input; +mod window; +mod window_state; diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/monitor.rs b/third_party/winit-0.30.13/src/platform_impl/windows/monitor.rs new file mode 100644 index 0000000..9a880db --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/monitor.rs @@ -0,0 +1,256 @@ +use std::collections::{BTreeSet, VecDeque}; +use std::hash::Hash; +use std::{io, mem, ptr}; + +use windows_sys::Win32::Foundation::{BOOL, HWND, LPARAM, POINT, RECT}; +use windows_sys::Win32::Graphics::Gdi::{ + EnumDisplayMonitors, EnumDisplaySettingsExW, GetMonitorInfoW, MonitorFromPoint, + MonitorFromWindow, DEVMODEW, DM_BITSPERPEL, DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, DM_PELSWIDTH, + ENUM_CURRENT_SETTINGS, HDC, HMONITOR, MONITORINFO, MONITORINFOEXW, MONITOR_DEFAULTTONEAREST, + MONITOR_DEFAULTTOPRIMARY, +}; + +use super::util::decode_wide; +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::monitor::VideoModeHandle as RootVideoModeHandle; +use crate::platform_impl::platform::dpi::{dpi_to_scale_factor, get_monitor_dpi}; +use crate::platform_impl::platform::util::has_flag; +use crate::platform_impl::platform::window::Window; + +#[derive(Clone)] +pub struct VideoModeHandle { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + pub(crate) monitor: MonitorHandle, + // DEVMODEW is huge so we box it to avoid blowing up the size of winit::window::Fullscreen + pub(crate) native_video_mode: Box, +} + +impl PartialEq for VideoModeHandle { + fn eq(&self, other: &Self) -> bool { + self.size == other.size + && self.bit_depth == other.bit_depth + && self.refresh_rate_millihertz == other.refresh_rate_millihertz + && self.monitor == other.monitor + } +} + +impl Eq for VideoModeHandle {} + +impl std::hash::Hash for VideoModeHandle { + fn hash(&self, state: &mut H) { + self.size.hash(state); + self.bit_depth.hash(state); + self.refresh_rate_millihertz.hash(state); + self.monitor.hash(state); + } +} + +impl std::fmt::Debug for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VideoModeHandle") + .field("size", &self.size) + .field("bit_depth", &self.bit_depth) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz) + .field("monitor", &self.monitor) + .finish() + } +} + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub struct MonitorHandle(HMONITOR); + +// Send is not implemented for HMONITOR, we have to wrap it and implement it manually. +// For more info see: +// https://github.com/retep998/winapi-rs/issues/360 +// https://github.com/retep998/winapi-rs/issues/396 + +unsafe impl Send for MonitorHandle {} + +unsafe extern "system" fn monitor_enum_proc( + hmonitor: HMONITOR, + _hdc: HDC, + _place: *mut RECT, + data: LPARAM, +) -> BOOL { + let monitors = data as *mut VecDeque; + unsafe { (*monitors).push_back(MonitorHandle::new(hmonitor)) }; + true.into() // continue enumeration +} + +pub fn available_monitors() -> VecDeque { + let mut monitors: VecDeque = VecDeque::new(); + unsafe { + EnumDisplayMonitors( + 0, + ptr::null(), + Some(monitor_enum_proc), + &mut monitors as *mut _ as LPARAM, + ); + } + 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) +} + +impl Window { + pub fn available_monitors(&self) -> VecDeque { + available_monitors() + } + + pub fn primary_monitor(&self) -> Option { + let monitor = primary_monitor(); + Some(monitor) + } +} + +pub(crate) fn get_monitor_info(hmonitor: HMONITOR) -> Result { + let mut monitor_info: MONITORINFOEXW = unsafe { mem::zeroed() }; + 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 == false.into() { + Err(io::Error::last_os_error()) + } else { + Ok(monitor_info) + } +} + +impl MonitorHandle { + pub(crate) fn new(hmonitor: HMONITOR) -> Self { + MonitorHandle(hmonitor) + } + + #[inline] + pub fn name(&self) -> Option { + let monitor_info = get_monitor_info(self.0).unwrap(); + Some(decode_wide(&monitor_info.szDevice).to_string_lossy().to_string()) + } + + #[inline] + pub fn native_identifier(&self) -> String { + self.name().unwrap() + } + + #[inline] + pub fn hmonitor(&self) -> HMONITOR { + self.0 + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + let rc_monitor = get_monitor_info(self.0).unwrap().monitorInfo.rcMonitor; + PhysicalSize { + width: (rc_monitor.right - rc_monitor.left) as u32, + height: (rc_monitor.bottom - rc_monitor.top) as u32, + } + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + let monitor_info = get_monitor_info(self.0).ok()?; + let device_name = monitor_info.szDevice.as_ptr(); + unsafe { + let mut mode: DEVMODEW = mem::zeroed(); + mode.dmSize = mem::size_of_val(&mode) as u16; + if EnumDisplaySettingsExW(device_name, ENUM_CURRENT_SETTINGS, &mut mode, 0) + == false.into() + { + None + } else { + Some(mode.dmDisplayFrequency * 1000) + } + } + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + get_monitor_info(self.0) + .map(|info| { + let rc_monitor = info.monitorInfo.rcMonitor; + PhysicalPosition { x: rc_monitor.left, y: rc_monitor.top } + }) + .unwrap_or(PhysicalPosition { x: 0, y: 0 }) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + dpi_to_scale_factor(get_monitor_dpi(self.0).unwrap_or(96)) + } + + #[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 mod_map = |mode: RootVideoModeHandle| mode.video_mode; + + let monitor_info = match get_monitor_info(self.0) { + Ok(monitor_info) => monitor_info, + Err(error) => { + tracing::warn!("Error from get_monitor_info: {error}"); + return modes.into_iter().map(mod_map); + }, + }; + + let device_name = monitor_info.szDevice.as_ptr(); + + let mut i = 0; + loop { + let mut mode: DEVMODEW = unsafe { mem::zeroed() }; + mode.dmSize = mem::size_of_val(&mode) as u16; + if unsafe { EnumDisplaySettingsExW(device_name, i, &mut mode, 0) } == false.into() { + break; + } + + const REQUIRED_FIELDS: u32 = + DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; + assert!(has_flag(mode.dmFields, REQUIRED_FIELDS)); + + // Use Ord impl of RootVideoModeHandle + modes.insert(RootVideoModeHandle { + video_mode: VideoModeHandle { + size: (mode.dmPelsWidth, mode.dmPelsHeight), + bit_depth: mode.dmBitsPerPel as u16, + refresh_rate_millihertz: mode.dmDisplayFrequency * 1000, + monitor: self.clone(), + native_video_mode: Box::new(mode), + }, + }); + + i += 1; + } + + modes.into_iter().map(mod_map) + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/raw_input.rs b/third_party/winit-0.30.13/src/platform_impl/windows/raw_input.rs new file mode 100644 index 0000000..f78a7bb --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/raw_input.rs @@ -0,0 +1,301 @@ +use std::mem::{self, size_of}; +use std::ptr; + +use windows_sys::Win32::Devices::HumanInterfaceDevice::{ + HID_USAGE_GENERIC_KEYBOARD, HID_USAGE_GENERIC_MOUSE, HID_USAGE_PAGE_GENERIC, +}; +use windows_sys::Win32::Foundation::{HANDLE, HWND}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + MapVirtualKeyW, MAPVK_VK_TO_VSC_EX, VK_NUMLOCK, VK_SHIFT, +}; +use windows_sys::Win32::UI::Input::{ + GetRawInputData, GetRawInputDeviceInfoW, GetRawInputDeviceList, RegisterRawInputDevices, + HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTDEVICELIST, RAWINPUTHEADER, RAWKEYBOARD, + RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDEV_REMOVE, RIDI_DEVICEINFO, RIDI_DEVICENAME, + RID_DEVICE_INFO, RID_DEVICE_INFO_HID, RID_DEVICE_INFO_KEYBOARD, RID_DEVICE_INFO_MOUSE, + RID_INPUT, RIM_TYPEHID, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + RI_KEY_E0, RI_KEY_E1, RI_MOUSE_BUTTON_1_DOWN, RI_MOUSE_BUTTON_1_UP, RI_MOUSE_BUTTON_2_DOWN, + RI_MOUSE_BUTTON_2_UP, RI_MOUSE_BUTTON_3_DOWN, RI_MOUSE_BUTTON_3_UP, RI_MOUSE_BUTTON_4_DOWN, + RI_MOUSE_BUTTON_4_UP, RI_MOUSE_BUTTON_5_DOWN, RI_MOUSE_BUTTON_5_UP, +}; + +use super::scancode_to_physicalkey; +use crate::event::ElementState; +use crate::event_loop::DeviceEvents; +use crate::keyboard::{KeyCode, PhysicalKey}; +use crate::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(ptr::null_mut(), &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(buffer.as_mut_ptr(), &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) +} + +#[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 { + RIM_TYPEMOUSE => RawDeviceInfo::Mouse(info.Anonymous.mouse), + RIM_TYPEKEYBOARD => RawDeviceInfo::Keyboard(info.Anonymous.keyboard), + 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(handle, RIDI_DEVICEINFO, &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(handle, RIDI_DEVICENAME, ptr::null_mut(), &mut minimum_size) + }; + + if status != 0 { + return None; + } + + let mut name: Vec = Vec::with_capacity(minimum_size as _); + + let status = unsafe { + GetRawInputDeviceInfoW(handle, RIDI_DEVICENAME, 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 _) }; + + util::decode_wide(&name).into_string().ok() +} + +pub fn register_raw_input_devices(devices: &[RAWINPUTDEVICE]) -> bool { + let device_size = size_of::() as u32; + + unsafe { + RegisterRawInputDevices(devices.as_ptr(), devices.len() as u32, device_size) == true.into() + } +} + +pub fn register_all_mice_and_keyboards_for_raw_input( + mut window_handle: HWND, + filter: DeviceEvents, +) -> 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 { + DeviceEvents::Never => { + window_handle = 0; + RIDEV_REMOVE + }, + DeviceEvents::WhenFocused => RIDEV_DEVNOTIFY, + DeviceEvents::Always => 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, &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: u32, + 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) { + Some(ElementState::Pressed) + } else if util::has_flag(button_flags, up_flag) { + Some(ElementState::Released) + } else { + None + } +} + +pub fn get_raw_mouse_button_state(button_flags: u32) -> [Option; 5] { + [ + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_1_DOWN, RI_MOUSE_BUTTON_1_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_2_DOWN, RI_MOUSE_BUTTON_2_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_3_DOWN, RI_MOUSE_BUTTON_3_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_4_DOWN, RI_MOUSE_BUTTON_4_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_5_DOWN, RI_MOUSE_BUTTON_5_UP), + ] +} + +pub fn get_keyboard_physical_key(keyboard: RAWKEYBOARD) -> Option { + 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. + unsafe { 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 equivalence 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, pressing + // 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 extension 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 None; + } + let physical_key = if 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 + PhysicalKey::Code(KeyCode::NumLock) + } else { + scancode_to_physicalkey(scancode as u32) + }; + if keyboard.VKey == VK_SHIFT { + if let PhysicalKey::Code( + KeyCode::NumpadDecimal + | KeyCode::Numpad0 + | KeyCode::Numpad1 + | KeyCode::Numpad2 + | KeyCode::Numpad3 + | KeyCode::Numpad4 + | KeyCode::Numpad5 + | KeyCode::Numpad6 + | KeyCode::Numpad7 + | KeyCode::Numpad8 + | KeyCode::Numpad9, + ) = physical_key + { + // 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 None; + } + } + + Some(physical_key) +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/util.rs b/third_party/winit-0.30.13/src/platform_impl/windows/util.rs new file mode 100644 index 0000000..a3a7705 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/util.rs @@ -0,0 +1,280 @@ +use std::ffi::{c_void, OsStr, OsString}; +use std::iter::once; +use std::ops::BitAnd; +use std::os::windows::prelude::{OsStrExt, OsStringExt}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::{io, mem, ptr}; + +use crate::utils::Lazy; +use windows_sys::core::{HRESULT, PCWSTR}; +use windows_sys::Win32::Foundation::{BOOL, HANDLE, HMODULE, HWND, POINT, RECT}; +use windows_sys::Win32::Graphics::Gdi::{ClientToScreen, HMONITOR}; +use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}; +use windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER; +use windows_sys::Win32::UI::HiDpi::{ + DPI_AWARENESS_CONTEXT, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS, +}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow; +use windows_sys::Win32::UI::Input::Pointer::{POINTER_INFO, POINTER_PEN_INFO, POINTER_TOUCH_INFO}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + ClipCursor, GetClientRect, GetClipCursor, GetCursorPos, GetSystemMetrics, GetWindowPlacement, + GetWindowRect, IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, + IDC_IBEAM, IDC_NO, IDC_SIZEALL, IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT, + SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SW_MAXIMIZE, + WINDOWPLACEMENT, +}; + +use crate::window::CursorIcon; + +pub fn encode_wide(string: impl AsRef) -> Vec { + string.as_ref().encode_wide().chain(once(0)).collect() +} + +pub fn decode_wide(mut wide_c_string: &[u16]) -> OsString { + if let Some(null_pos) = wide_c_string.iter().position(|c| *c == 0) { + wide_c_string = &wide_c_string[..null_pos]; + } + + OsString::from_wide(wide_c_string) +} + +pub fn has_flag(bitset: T, flag: T) -> bool +where + T: Copy + PartialEq + BitAnd, +{ + bitset & flag == flag +} + +pub(crate) fn win_to_err(result: BOOL) -> Result<(), io::Error> { + if result != false.into() { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + +pub enum WindowArea { + Outer, + Inner, +} + +impl WindowArea { + pub fn get_rect(self, hwnd: HWND) -> Result { + let mut rect = unsafe { mem::zeroed() }; + + match self { + WindowArea::Outer => { + win_to_err(unsafe { GetWindowRect(hwnd, &mut rect) })?; + }, + WindowArea::Inner => unsafe { + let mut top_left = mem::zeroed(); + + win_to_err(ClientToScreen(hwnd, &mut top_left))?; + win_to_err(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 is_maximized(window: HWND) -> bool { + unsafe { + let mut placement: WINDOWPLACEMENT = mem::zeroed(); + placement.length = mem::size_of::() as u32; + GetWindowPlacement(window, &mut placement); + placement.showCmd == SW_MAXIMIZE as u32 + } +} + +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(BOOL::from(!hidden)) }; + } +} + +pub fn get_cursor_position() -> Result { + unsafe { + let mut point: POINT = mem::zeroed(); + win_to_err(GetCursorPos(&mut point)).map(|_| point) + } +} + +pub fn get_cursor_clip() -> Result { + unsafe { + let mut rect: RECT = mem::zeroed(); + win_to_err(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) -> Result<(), io::Error> { + unsafe { + let rect_ptr = rect.as_ref().map(|r| r as *const RECT).unwrap_or(ptr::null()); + win_to_err(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_minimized(window: HWND) -> bool { + unsafe { IsIconic(window) != false.into() } +} + +pub fn get_instance_handle() -> 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: IMAGE_DOS_HEADER; + } + + unsafe { &__ImageBase as *const _ as _ } +} + +pub(crate) fn to_windows_cursor(cursor: CursorIcon) -> PCWSTR { + match cursor { + CursorIcon::Default => IDC_ARROW, + CursorIcon::Pointer => 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 as some functions +// may not be available on all Windows platforms supported by winit. +// +// `library` and `function` must be zero-terminated. +pub(super) fn get_function_impl(library: &str, function: &str) -> Option<*const c_void> { + assert_eq!(library.chars().last(), Some('\0')); + 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 { LoadLibraryA(library.as_ptr()) }; + if module == 0 { + return None; + } + + unsafe { GetProcAddress(module, function.as_ptr()) }.map(|function_ptr| function_ptr as _) +} + +macro_rules! get_function { + ($lib:expr, $func:ident) => { + crate::platform_impl::platform::util::get_function_impl( + concat!($lib, '\0'), + concat!(stringify!($func), '\0'), + ) + .map(|f| unsafe { std::mem::transmute::<*const _, $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; +pub type EnableNonClientDpiScaling = unsafe extern "system" fn(hwnd: HWND) -> BOOL; +pub type AdjustWindowRectExForDpi = unsafe extern "system" fn( + rect: *mut RECT, + dw_style: u32, + b_menu: BOOL, + dw_ex_style: u32, + dpi: u32, +) -> BOOL; + +pub type GetPointerFrameInfoHistory = unsafe extern "system" fn( + pointer_id: u32, + entries_count: *mut u32, + pointer_count: *mut u32, + pointer_info: *mut POINTER_INFO, +) -> BOOL; + +pub type SkipPointerFrameMessages = unsafe extern "system" fn(pointer_id: u32) -> BOOL; +pub type GetPointerDeviceRects = unsafe extern "system" fn( + device: HANDLE, + pointer_device_rect: *mut RECT, + display_rect: *mut RECT, +) -> BOOL; + +pub type GetPointerTouchInfo = + unsafe extern "system" fn(pointer_id: u32, touch_info: *mut POINTER_TOUCH_INFO) -> BOOL; + +pub type GetPointerPenInfo = + unsafe extern "system" fn(point_id: u32, pen_info: *mut POINTER_PEN_INFO) -> BOOL; + +pub(crate) static GET_DPI_FOR_WINDOW: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetDpiForWindow)); +pub(crate) static ADJUST_WINDOW_RECT_EX_FOR_DPI: Lazy> = + Lazy::new(|| get_function!("user32.dll", AdjustWindowRectExForDpi)); +pub(crate) static GET_DPI_FOR_MONITOR: Lazy> = + Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor)); +pub(crate) static ENABLE_NON_CLIENT_DPI_SCALING: Lazy> = + Lazy::new(|| get_function!("user32.dll", EnableNonClientDpiScaling)); +pub(crate) static SET_PROCESS_DPI_AWARENESS_CONTEXT: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetProcessDpiAwarenessContext)); +pub(crate) static SET_PROCESS_DPI_AWARENESS: Lazy> = + Lazy::new(|| get_function!("shcore.dll", SetProcessDpiAwareness)); +pub(crate) static SET_PROCESS_DPI_AWARE: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetProcessDPIAware)); +pub(crate) static GET_POINTER_FRAME_INFO_HISTORY: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerFrameInfoHistory)); +pub(crate) static SKIP_POINTER_FRAME_MESSAGES: Lazy> = + Lazy::new(|| get_function!("user32.dll", SkipPointerFrameMessages)); +pub(crate) static GET_POINTER_DEVICE_RECTS: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerDeviceRects)); +pub(crate) static GET_POINTER_TOUCH_INFO: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerTouchInfo)); +pub(crate) static GET_POINTER_PEN_INFO: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerPenInfo)); diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/window.rs b/third_party/winit-0.30.13/src/platform_impl/windows/window.rs new file mode 100644 index 0000000..228c6f0 --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/window.rs @@ -0,0 +1,1575 @@ +#![cfg(windows_platform)] + +use std::cell::Cell; +use std::ffi::c_void; +use std::mem::{self, MaybeUninit}; +use std::sync::mpsc::channel; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::{io, panic, ptr}; + +use windows_sys::Win32::Foundation::{ + HWND, LPARAM, OLE_E_WRONGCOMPOBJ, POINT, POINTS, RECT, RPC_E_CHANGED_MODE, S_OK, WPARAM, +}; +use windows_sys::Win32::Graphics::Dwm::{ + DwmEnableBlurBehindWindow, DwmSetWindowAttribute, DWMWA_BORDER_COLOR, DWMWA_CAPTION_COLOR, + DWMWA_SYSTEMBACKDROP_TYPE, DWMWA_TEXT_COLOR, DWMWA_WINDOW_CORNER_PREFERENCE, DWM_BB_BLURREGION, + DWM_BB_ENABLE, DWM_BLURBEHIND, DWM_SYSTEMBACKDROP_TYPE, DWM_WINDOW_CORNER_PREFERENCE, +}; +use windows_sys::Win32::Graphics::Gdi::{ + ChangeDisplaySettingsExW, ClientToScreen, CreateRectRgn, DeleteObject, InvalidateRgn, + RedrawWindow, CDS_FULLSCREEN, DISP_CHANGE_BADFLAGS, DISP_CHANGE_BADMODE, DISP_CHANGE_BADPARAM, + DISP_CHANGE_FAILED, DISP_CHANGE_SUCCESSFUL, RDW_INTERNALPAINT, +}; +use windows_sys::Win32::System::Com::{ + CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_APARTMENTTHREADED, +}; +use windows_sys::Win32::System::Ole::{OleInitialize, RegisterDragDrop}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + EnableWindow, GetActiveWindow, MapVirtualKeyW, ReleaseCapture, SendInput, ToUnicode, INPUT, + INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, MAPVK_VK_TO_VSC, + VIRTUAL_KEY, VK_LMENU, VK_MENU, VK_SPACE, +}; +use windows_sys::Win32::UI::Input::Touch::{RegisterTouchWindow, TWF_WANTPALM}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, EnableMenuItem, FlashWindowEx, GetClientRect, GetCursorPos, + GetForegroundWindow, GetSystemMenu, GetSystemMetrics, GetWindowPlacement, GetWindowTextLengthW, + GetWindowTextW, IsWindowVisible, LoadCursorW, PeekMessageW, PostMessageW, RegisterClassExW, + SetCursor, SetCursorPos, SetForegroundWindow, SetMenuDefaultItem, SetWindowDisplayAffinity, + SetWindowPlacement, SetWindowPos, SetWindowTextW, TrackPopupMenu, CS_HREDRAW, CS_VREDRAW, + CW_USEDEFAULT, FLASHWINFO, FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY, + GWLP_HINSTANCE, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTCAPTION, HTLEFT, HTRIGHT, HTTOP, + HTTOPLEFT, HTTOPRIGHT, MENU_ITEM_STATE, MFS_DISABLED, MFS_ENABLED, MF_BYCOMMAND, NID_READY, + PM_NOREMOVE, SC_CLOSE, SC_MAXIMIZE, SC_MINIMIZE, SC_MOVE, SC_RESTORE, SC_SIZE, SM_DIGITIZER, + SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, TPM_LEFTALIGN, TPM_RETURNCMD, + WDA_EXCLUDEFROMCAPTURE, WDA_NONE, WM_NCLBUTTONDOWN, WM_SYSCOMMAND, WNDCLASSEXW, +}; + +use tracing::warn; + +use crate::cursor::Cursor; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::icon::Icon; +use crate::platform::windows::{BackdropType, Color, CornerPreference}; +use crate::platform_impl::platform::dark_mode::try_theme; +use crate::platform_impl::platform::definitions::{ + CLSID_TaskbarList, IID_ITaskbarList, IID_ITaskbarList2, ITaskbarList, ITaskbarList2, +}; +use crate::platform_impl::platform::dpi::{ + dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_dpi, +}; +use crate::platform_impl::platform::drop_handler::FileDropHandler; +use crate::platform_impl::platform::event_loop::{self, ActiveEventLoop, DESTROY_MSG_ID}; +use crate::platform_impl::platform::icon::{self, IconType, WinCursor}; +use crate::platform_impl::platform::ime::ImeContext; +use crate::platform_impl::platform::keyboard::KeyEventBuilder; +use crate::platform_impl::platform::monitor::{self, MonitorHandle}; +use crate::platform_impl::platform::window_state::{ + CursorFlags, SavedWindow, WindowFlags, WindowState, +}; +use crate::platform_impl::platform::{util, Fullscreen, SelectedCursor, WindowId}; +use crate::window::{ + CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowLevel, +}; + +/// The Win32 implementation of the main `Window` object. +pub(crate) struct Window { + /// Main handle for the window. + window: HWND, + + /// The current window state. + window_state: Arc>, + + // The events loop proxy. + thread_executor: event_loop::EventLoopThreadExecutor, +} + +impl Window { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + w_attr: WindowAttributes, + ) -> 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 { init(w_attr, event_loop) } + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + // TODO: Use `thread_executor` here + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + // TODO: Use `thread_executor` here + f(self) + } + + fn window_state_lock(&self) -> MutexGuard<'_, WindowState> { + self.window_state.lock().unwrap() + } + + pub fn set_title(&self, text: &str) { + let wide_text = util::encode_wide(text); + unsafe { + SetWindowTextW(self.hwnd(), wide_text.as_ptr()); + } + } + + pub fn set_transparent(&self, transparent: 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().unwrap(), window, |f| { + f.set(WindowFlags::TRANSPARENT, transparent) + }); + }); + } + + pub fn set_blur(&self, _blur: bool) {} + + #[inline] + pub fn set_visible(&self, visible: 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().unwrap(), window, |f| { + f.set(WindowFlags::VISIBLE, visible) + }); + }); + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(unsafe { IsWindowVisible(self.window) == 1 }) + } + + #[inline] + pub fn request_redraw(&self) { + // NOTE: mark that we requested a redraw to handle requests during `WM_PAINT` handling. + self.window_state.lock().unwrap().redraw_requested = true; + unsafe { + RedrawWindow(self.hwnd(), ptr::null(), 0, RDW_INTERNALPAINT); + } + } + + #[inline] + pub fn pre_present_notify(&self) {} + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + util::WindowArea::Outer + .get_rect(self.hwnd()) + .map(|rect| Ok(PhysicalPosition::new(rect.left, rect.top))) + .expect( + "Unexpected GetWindowRect failure; please report this error to \ + rust-windowing/winit", + ) + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + let mut position: POINT = unsafe { mem::zeroed() }; + if unsafe { ClientToScreen(self.hwnd(), &mut position) } == false.into() { + panic!( + "Unexpected ClientToScreen failure: please report this error to \ + rust-windowing/winit" + ) + } + 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; + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + }); + + unsafe { + SetWindowPos( + self.hwnd(), + 0, + x, + y, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE, + ); + InvalidateRgn(self.hwnd(), 0, false.into()); + } + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let mut rect: RECT = unsafe { mem::zeroed() }; + if unsafe { GetClientRect(self.hwnd(), &mut rect) } == false.into() { + panic!( + "Unexpected GetClientRect failure: please report this error to \ + rust-windowing/winit" + ) + } + PhysicalSize::new((rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + util::WindowArea::Outer + .get_rect(self.hwnd()) + .map(|rect| { + PhysicalSize::new((rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32) + }) + .unwrap() + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let scale_factor = self.scale_factor(); + let physical_size = size.to_physical::(scale_factor); + + let window_flags = self.window_state_lock().window_flags; + window_flags.set_size(self.hwnd(), physical_size); + + if physical_size != self.inner_size() { + let window_state = Arc::clone(&self.window_state); + let window = self.window; + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + }); + } + + None + } + + #[inline] + pub fn set_min_inner_size(&self, size: Option) { + self.window_state_lock().min_size = size; + // Make windows re-check the window size bounds. + let size = self.inner_size(); + self.request_inner_size(size.into()); + } + + #[inline] + pub fn set_max_inner_size(&self, size: Option) { + self.window_state_lock().max_size = size; + // Make windows re-check the window size bounds. + let size = self.inner_size(); + self.request_inner_size(size.into()); + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + let w = self.window_state_lock(); + let scale_factor = w.scale_factor; + w.resize_increments.map(|size| size.to_physical(scale_factor)) + } + + #[inline] + pub fn set_resize_increments(&self, increments: Option) { + self.window_state_lock().resize_increments = increments; + } + + #[inline] + pub fn set_resizable(&self, resizable: 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().unwrap(), window, |f| { + f.set(WindowFlags::RESIZABLE, resizable) + }); + }); + } + + #[inline] + pub fn is_resizable(&self) -> bool { + let window_state = self.window_state_lock(); + window_state.window_flags.contains(WindowFlags::RESIZABLE) + } + + #[inline] + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + 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().unwrap(), window, |f| { + f.set(WindowFlags::MINIMIZABLE, buttons.contains(WindowButtons::MINIMIZE)); + f.set(WindowFlags::MAXIMIZABLE, buttons.contains(WindowButtons::MAXIMIZE)); + f.set(WindowFlags::CLOSABLE, buttons.contains(WindowButtons::CLOSE)) + }); + }); + } + + pub fn enabled_buttons(&self) -> WindowButtons { + let mut buttons = WindowButtons::empty(); + let window_state = self.window_state_lock(); + if window_state.window_flags.contains(WindowFlags::MINIMIZABLE) { + buttons |= WindowButtons::MINIMIZE; + } + if window_state.window_flags.contains(WindowFlags::MAXIMIZABLE) { + buttons |= WindowButtons::MAXIMIZE; + } + if window_state.window_flags.contains(WindowFlags::CLOSABLE) { + buttons |= WindowButtons::CLOSE; + } + buttons + } + + /// Returns the `hwnd` of this window. + #[inline] + pub fn hwnd(&self) -> HWND { + self.window + } + + #[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 as *mut _; + let hinstance = unsafe { super::get_window_long(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 as *mut _; + let hinstance = unsafe { super::get_window_long(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 unsafe fn rwh_06_no_thread_check( + &self, + ) -> Result { + let mut window_handle = rwh_06::Win32WindowHandle::new(unsafe { + // SAFETY: Handle will never be zero. + std::num::NonZeroIsize::new_unchecked(self.window) + }); + let hinstance = unsafe { super::get_window_long(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_window_handle_rwh_06(&self) -> Result { + // TODO: Write a test once integration framework is ready to ensure that it holds. + // If we aren't in the GUI thread, we can't return the window. + if !self.thread_executor.in_event_loop_thread() { + tracing::error!("tried to access window handle outside of the main thread"); + return Err(rwh_06::HandleError::Unavailable); + } + + // SAFETY: We are on the correct thread. + unsafe { self.rwh_06_no_thread_check() } + } + + #[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(&self, cursor: Cursor) { + match cursor { + Cursor::Icon(icon) => { + self.window_state_lock().mouse.selected_cursor = SelectedCursor::Named(icon); + self.thread_executor.execute_in_thread(move || unsafe { + let cursor = LoadCursorW(0, util::to_windows_cursor(icon)); + SetCursor(cursor); + }); + }, + Cursor::Custom(cursor) => { + let new_cursor = match cursor.inner { + WinCursor::Cursor(cursor) => cursor, + WinCursor::Failed => { + warn!("Requested to apply failed cursor"); + return; + }, + }; + self.window_state_lock().mouse.selected_cursor = + SelectedCursor::Custom(new_cursor.clone()); + self.thread_executor.execute_in_thread(move || unsafe { + SetCursor(new_cursor.as_raw_handle()); + }); + }, + } + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + let (tx, rx) = channel(); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + let result = window_state + .lock() + .unwrap() + .mouse + .set_cursor_flags(window, |f| { + f.set(CursorFlags::GRABBED, mode != CursorGrabMode::None); + f.set(CursorFlags::LOCKED, mode == CursorGrabMode::Locked); + }) + .map_err(|e| ExternalError::Os(os_error!(e))); + let _ = tx.send(result); + }); + rx.recv().unwrap() + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + let (tx, rx) = channel(); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + let result = window_state + .lock() + .unwrap() + .mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::HIDDEN, !visible)) + .map_err(|e| e.to_string()); + let _ = tx.send(result); + }); + rx.recv().unwrap().ok(); + } + + #[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.hwnd(), &mut point) == false.into() { + return Err(ExternalError::Os(os_error!(io::Error::last_os_error()))); + } + if SetCursorPos(point.x, point.y) == false.into() { + return Err(ExternalError::Os(os_error!(io::Error::last_os_error()))); + } + } + Ok(()) + } + + unsafe fn handle_os_dragging(&self, wparam: WPARAM) { + let window = self.window; + let window_state = self.window_state.clone(); + + self.thread_executor.execute_in_thread(move || { + { + let mut guard = window_state.lock().unwrap(); + if !guard.dragging { + guard.dragging = true; + } else { + return; + } + } + + 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 }; + + // ReleaseCapture needs to execute on the main thread + unsafe { ReleaseCapture() }; + + unsafe { + PostMessageW(window, WM_NCLBUTTONDOWN, wparam, &points as *const _ as LPARAM) + }; + }); + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + unsafe { + self.handle_os_dragging(HTCAPTION as WPARAM); + } + + Ok(()) + } + + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + unsafe { + self.handle_os_dragging(match direction { + ResizeDirection::East => HTRIGHT, + ResizeDirection::North => HTTOP, + ResizeDirection::NorthEast => HTTOPRIGHT, + ResizeDirection::NorthWest => HTTOPLEFT, + ResizeDirection::South => HTBOTTOM, + ResizeDirection::SouthEast => HTBOTTOMRIGHT, + ResizeDirection::SouthWest => HTBOTTOMLEFT, + ResizeDirection::West => HTLEFT, + } as WPARAM); + } + + Ok(()) + } + + unsafe fn handle_showing_window_menu(&self, position: Position) { + unsafe { + let point = { + let mut point = POINT { x: 0, y: 0 }; + let scale_factor = self.scale_factor(); + let (x, y) = position.to_physical::(scale_factor).into(); + point.x = x; + point.y = y; + if ClientToScreen(self.hwnd(), &mut point) == false.into() { + warn!( + "Can't convert client-area coordinates to screen coordinates when showing \ + window menu." + ); + return; + } + point + }; + + // get the current system menu + let h_menu = GetSystemMenu(self.hwnd(), 0); + if h_menu == 0 { + warn!("The corresponding window doesn't have a system menu"); + // This situation should not be treated as an error so just return without showing + // menu. + return; + } + + fn enable(b: bool) -> MENU_ITEM_STATE { + if b { + MFS_ENABLED + } else { + MFS_DISABLED + } + } + + // Change the menu items according to the current window status. + + let restore_btn = enable(self.is_maximized() && self.is_resizable()); + let size_btn = enable(!self.is_maximized() && self.is_resizable()); + let maximize_btn = enable(!self.is_maximized() && self.is_resizable()); + + EnableMenuItem(h_menu, SC_RESTORE, MF_BYCOMMAND | restore_btn); + EnableMenuItem(h_menu, SC_MOVE, MF_BYCOMMAND | enable(!self.is_maximized())); + EnableMenuItem(h_menu, SC_SIZE, MF_BYCOMMAND | size_btn); + EnableMenuItem(h_menu, SC_MINIMIZE, MF_BYCOMMAND | MFS_ENABLED); + EnableMenuItem(h_menu, SC_MAXIMIZE, MF_BYCOMMAND | maximize_btn); + EnableMenuItem(h_menu, SC_CLOSE, MF_BYCOMMAND | MFS_ENABLED); + + // Set the default menu item. + SetMenuDefaultItem(h_menu, SC_CLOSE, 0); + + // Popup the system menu at the position. + let result = TrackPopupMenu( + h_menu, + TPM_RETURNCMD | TPM_LEFTALIGN, /* for now im using LTR, but we have to use user + * layout direction */ + point.x, + point.y, + 0, + self.hwnd(), + std::ptr::null_mut(), + ); + + if result == 0 { + // User canceled the menu, no need to continue. + return; + } + + // Send the command that the user select to the corresponding window. + if PostMessageW(self.hwnd(), WM_SYSCOMMAND, result as _, 0) == 0 { + warn!("Can't post the system menu message to the window."); + } + } + } + + #[inline] + pub fn show_window_menu(&self, position: Position) { + unsafe { + self.handle_showing_window_menu(position); + } + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::IGNORE_CURSOR_EVENT, !hittest) + }); + }); + + Ok(()) + } + + #[inline] + pub fn id(&self) -> WindowId { + WindowId(self.hwnd()) + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + let is_minimized = util::is_minimized(self.hwnd()); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags_in_place(&mut window_state.lock().unwrap(), |f| { + f.set(WindowFlags::MINIMIZED, is_minimized) + }); + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MINIMIZED, minimized) + }); + }); + } + + #[inline] + pub fn is_minimized(&self) -> Option { + Some(util::is_minimized(self.hwnd())) + } + + #[inline] + pub fn set_maximized(&self, maximized: 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().unwrap(), window, |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 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().unwrap(); + let old_fullscreen = window_state_lock.fullscreen.clone(); + + match (&old_fullscreen, &fullscreen) { + // Return if we already are 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 == monitor::current_monitor(window) => + { + return + }, + _ => {}, + } + + window_state_lock.fullscreen.clone_from(&fullscreen); + drop(window_state_lock); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + // Change video mode if we're transitioning to or from exclusive + // fullscreen + match (&old_fullscreen, &fullscreen) { + (_, Some(Fullscreen::Exclusive(video_mode))) => { + let monitor = video_mode.monitor(); + let monitor_info = monitor::get_monitor_info(monitor.hmonitor()).unwrap(); + + let res = unsafe { + ChangeDisplaySettingsExW( + monitor_info.szDevice.as_ptr(), + &*video_mode.native_video_mode, + 0, + CDS_FULLSCREEN, + ptr::null(), + ) + }; + + 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(_)), _) => { + let res = unsafe { + ChangeDisplaySettingsExW( + ptr::null(), + ptr::null(), + 0, + CDS_FULLSCREEN, + ptr::null(), + ) + }; + + 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 = mem::zeroed(); + PeekMessageW(&mut msg, 0, 0, 0, PM_NOREMOVE); + } + + // Update window style + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set( + WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN, + matches!(fullscreen, Some(Fullscreen::Exclusive(_))), + ); + f.set( + WindowFlags::MARKER_BORDERLESS_FULLSCREEN, + matches!(fullscreen, Some(Fullscreen::Borderless(_))), + ); + }); + + // Mark as fullscreen window wrt to z-order + // + // this needs to be called before the below fullscreen SetWindowPos as this itself + // will generate WM_SIZE messages of the old window size that can race with what we set + // below + unsafe { + taskbar_mark_fullscreen(window, fullscreen.is_some()); + } + + // Update window bounds + match &fullscreen { + Some(fullscreen) => { + // Save window bounds before entering fullscreen + let placement = unsafe { + let mut placement = mem::zeroed(); + GetWindowPlacement(window, &mut placement); + placement + }; + + window_state.lock().unwrap().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) => monitor::current_monitor(window), + }; + + let position: (i32, i32) = monitor.position().into(); + let size: (u32, u32) = monitor.size().into(); + + unsafe { + SetWindowPos( + window, + 0, + position.0, + position.1, + size.0 as i32, + size.1 as i32, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER, + ); + InvalidateRgn(window, 0, false.into()); + } + }, + None => { + let mut window_state_lock = window_state.lock().unwrap(); + if let Some(SavedWindow { placement }) = window_state_lock.saved_window.take() { + drop(window_state_lock); + unsafe { + SetWindowPlacement(window, &placement); + InvalidateRgn(window, 0, false.into()); + } + } + }, + } + }); + } + + #[inline] + pub fn set_decorations(&self, decorations: 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().unwrap(), window, |f| { + f.set(WindowFlags::MARKER_DECORATIONS, decorations) + }); + }); + } + + #[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 set_window_level(&self, level: WindowLevel) { + 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().unwrap(), window, |f| { + f.set(WindowFlags::ALWAYS_ON_TOP, level == WindowLevel::AlwaysOnTop); + f.set(WindowFlags::ALWAYS_ON_BOTTOM, level == WindowLevel::AlwaysOnBottom); + }); + }); + } + + #[inline] + pub fn current_monitor(&self) -> Option { + Some(monitor::current_monitor(self.hwnd())) + } + + #[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.hwnd(), IconType::Small); + } else { + icon::unset_for_window(self.hwnd(), IconType::Small); + } + self.window_state_lock().window_icon = window_icon; + } + + #[inline] + pub fn set_enable(&self, enabled: bool) { + unsafe { EnableWindow(self.hwnd(), enabled.into()) }; + } + + #[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.hwnd(), IconType::Big); + } else { + icon::unset_for_window(self.hwnd(), IconType::Big); + } + self.window_state_lock().taskbar_icon = taskbar_icon; + } + + #[inline] + pub fn set_ime_cursor_area(&self, spot: Position, size: Size) { + let window = self.window; + let state = self.window_state.clone(); + self.thread_executor.execute_in_thread(move || unsafe { + let scale_factor = state.lock().unwrap().scale_factor; + ImeContext::current(window).set_ime_cursor_area(spot, size, scale_factor); + }); + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let window = self.window; + let state = self.window_state.clone(); + self.thread_executor.execute_in_thread(move || unsafe { + state.lock().unwrap().ime_allowed = allowed; + ImeContext::set_ime_allowed(window, allowed); + }) + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let window = self.window; + let active_window_handle = unsafe { GetActiveWindow() }; + if window == active_window_handle { + return; + } + + 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 | FLASHW_TIMERNOFG, 0), + }) + .unwrap_or((FLASHW_STOP, 0)); + + let flash_info = FLASHWINFO { + cbSize: mem::size_of::() as u32, + hwnd: window, + dwFlags: flags, + uCount: count, + dwTimeout: 0, + }; + FlashWindowEx(&flash_info); + }); + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + try_theme(self.window, theme); + } + + #[inline] + pub fn theme(&self) -> Option { + Some(self.window_state_lock().current_theme) + } + + #[inline] + pub fn has_focus(&self) -> bool { + let window_state = self.window_state.lock().unwrap(); + window_state.has_active_focus() + } + + pub fn title(&self) -> String { + let len = unsafe { GetWindowTextLengthW(self.window) } + 1; + let mut buf = vec![0; len as usize]; + unsafe { GetWindowTextW(self.window, buf.as_mut_ptr(), len) }; + util::decode_wide(&buf).to_string_lossy().to_string() + } + + #[inline] + pub fn set_skip_taskbar(&self, skip: bool) { + self.window_state_lock().skip_taskbar = skip; + unsafe { set_skip_taskbar(self.hwnd(), skip) }; + } + + #[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().unwrap(), window, |f| { + f.set(WindowFlags::MARKER_UNDECORATED_SHADOW, shadow) + }); + }); + } + + #[inline] + pub fn set_system_backdrop(&self, backdrop_type: BackdropType) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_SYSTEMBACKDROP_TYPE as u32, + &(backdrop_type as i32) as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn focus_window(&self) { + let window_flags = self.window_state_lock().window_flags(); + + let is_visible = window_flags.contains(WindowFlags::VISIBLE); + let is_minimized = util::is_minimized(self.hwnd()); + let is_foreground = self.window == unsafe { GetForegroundWindow() }; + + if is_visible && !is_minimized && !is_foreground { + unsafe { force_window_active(self.window) }; + } + } + + #[inline] + pub fn set_content_protected(&self, protected: bool) { + unsafe { + SetWindowDisplayAffinity( + self.hwnd(), + if protected { WDA_EXCLUDEFROMCAPTURE } else { WDA_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 = VK_SPACE as VIRTUAL_KEY; + let scancode = MapVirtualKeyW(vk as u32, MAPVK_VK_TO_VSC); + let kbd_state = [0; 256]; + let mut char_buff = [MaybeUninit::uninit(); 8]; + ToUnicode( + vk as u32, + scancode, + kbd_state.as_ptr(), + char_buff[0].as_mut_ptr(), + char_buff.len() as i32, + 0, + ); + } + } + + #[inline] + pub fn set_border_color(&self, color: Color) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_BORDER_COLOR as u32, + &color as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn set_title_background_color(&self, color: Color) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_CAPTION_COLOR as u32, + &color as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn set_title_text_color(&self, color: Color) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_TEXT_COLOR as u32, + &color as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn set_corner_preference(&self, preference: CornerPreference) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_WINDOW_CORNER_PREFERENCE as u32, + &(preference as DWM_WINDOW_CORNER_PREFERENCE) as *const _ as _, + mem::size_of::() as _, + ); + } + } +} + +impl Drop for Window { + #[inline] + fn drop(&mut self) { + 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. + PostMessageW(self.hwnd(), DESTROY_MSG_ID.get(), 0, 0); + } + } +} + +pub(super) struct InitData<'a> { + // inputs + pub event_loop: &'a ActiveEventLoop, + pub attributes: WindowAttributes, + pub window_flags: WindowFlags, + // outputs + pub window: Option, +} + +impl InitData<'_> { + unsafe fn create_window(&self, window: HWND) -> Window { + // Register for touch events if applicable + { + let digitizer = unsafe { GetSystemMetrics(SM_DIGITIZER) as u32 }; + if digitizer & NID_READY != 0 { + unsafe { RegisterTouchWindow(window, TWF_WANTPALM) }; + } + } + + let dpi = unsafe { hwnd_dpi(window) }; + let scale_factor = dpi_to_scale_factor(dpi); + + // 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_theme(window, self.attributes.preferred_theme); + + let window_state = { + let window_state = WindowState::new( + &self.attributes, + scale_factor, + current_theme, + self.attributes.preferred_theme, + ); + let window_state = Arc::new(Mutex::new(window_state)); + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + *f = self.window_flags + }); + window_state + }; + + enable_non_client_dpi_scaling(window); + + unsafe { ImeContext::set_ime_allowed(window, false) }; + + Window { window, window_state, thread_executor: self.event_loop.create_thread_executor() } + } + + unsafe fn create_window_data(&self, win: &Window) -> event_loop::WindowData { + let file_drop_handler = if self.attributes.platform_specific.drag_and_drop { + let ole_init_result = unsafe { OleInitialize(ptr::null_mut()) }; + // It is ok if the initialize result is `S_FALSE` because it might happen that + // multiple windows are created on the same thread. + if ole_init_result == OLE_E_WRONGCOMPOBJ { + panic!("OleInitialize failed! Result was: `OLE_E_WRONGCOMPOBJ`"); + } else if ole_init_result == 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 = self.event_loop.runner_shared.clone(); + let file_drop_handler = FileDropHandler::new( + win.window, + Box::new(move |event| { + if let Ok(e) = event.map_nonuser_event() { + file_drop_runner.send_event(e) + } + }), + ); + + let handler_interface_ptr = + unsafe { &mut (*file_drop_handler.data).interface as *mut _ as *mut c_void }; + + assert_eq!(unsafe { RegisterDragDrop(win.window, handler_interface_ptr) }, S_OK); + Some(file_drop_handler) + } else { + None + }; + + event_loop::WindowData { + window_state: win.window_state.clone(), + event_loop_runner: self.event_loop.runner_shared.clone(), + key_event_builder: KeyEventBuilder::default(), + _file_drop_handler: file_drop_handler, + userdata_removed: Cell::new(false), + recurse_depth: Cell::new(0), + } + } + + // Returns a pointer to window user data on success. + // The user data will be registered for the window and can be accessed within the window event + // callback. + pub unsafe fn on_nccreate(&mut self, window: HWND) -> Option { + let runner = self.event_loop.runner_shared.clone(); + let result = runner.catch_unwind(|| { + let window = unsafe { self.create_window(window) }; + let window_data = unsafe { self.create_window_data(&window) }; + (window, window_data) + }); + + result.map(|(win, userdata)| { + self.window = Some(win); + let userdata = Box::into_raw(Box::new(userdata)); + userdata as _ + }) + } + + pub unsafe fn on_create(&mut self) { + let win = self.window.as_mut().expect("failed window creation"); + + // making the window transparent + if self.attributes.transparent && !self.attributes.platform_specific.no_redirection_bitmap { + // Empty region for the blur effect, so the window is fully transparent + let region = unsafe { 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 hr = unsafe { DwmEnableBlurBehindWindow(win.hwnd(), &bb) }; + if hr < 0 { + warn!("Setting transparent window is failed. HRESULT Code: 0x{:X}", hr); + } + unsafe { DeleteObject(region) }; + } + + win.set_skip_taskbar(self.attributes.platform_specific.skip_taskbar); + win.set_window_icon(self.attributes.window_icon.clone()); + win.set_taskbar_icon(self.attributes.platform_specific.taskbar_icon.clone()); + + let attributes = self.attributes.clone(); + + if attributes.content_protected { + win.set_content_protected(true); + } + + win.set_cursor(attributes.cursor); + + // Set visible before setting the size to ensure the + // attribute is correctly applied. + win.set_visible(attributes.visible); + + win.set_enabled_buttons(attributes.enabled_buttons); + + let size = attributes.inner_size.unwrap_or_else(|| PhysicalSize::new(800, 600).into()); + let max_size = attributes + .max_inner_size + .unwrap_or_else(|| PhysicalSize::new(f64::MAX, f64::MAX).into()); + let min_size = attributes.min_inner_size.unwrap_or_else(|| PhysicalSize::new(0, 0).into()); + let clamped_size = Size::clamp(size, min_size, max_size, win.scale_factor()); + win.request_inner_size(clamped_size); + + // let margins = MARGINS { + // cxLeftWidth: 1, + // cxRightWidth: 1, + // cyTopHeight: 1, + // cyBottomHeight: 1, + // }; + // dbg!(DwmExtendFrameIntoClientArea(win.hwnd(), &margins as *const _)); + + if let Some(position) = attributes.position { + win.set_outer_position(position); + } + + win.set_system_backdrop(self.attributes.platform_specific.backdrop_type); + + if let Some(color) = self.attributes.platform_specific.border_color { + win.set_border_color(color); + } + if let Some(color) = self.attributes.platform_specific.title_background_color { + win.set_title_background_color(color); + } + if let Some(color) = self.attributes.platform_specific.title_text_color { + win.set_title_text_color(color); + } + if let Some(corner) = self.attributes.platform_specific.corner_preference { + win.set_corner_preference(corner); + } + } +} +unsafe fn init( + attributes: WindowAttributes, + event_loop: &ActiveEventLoop, +) -> Result { + let title = util::encode_wide(&attributes.title); + + let class_name = util::encode_wide(&attributes.platform_specific.class_name); + unsafe { register_window_class(&class_name) }; + + let mut window_flags = WindowFlags::empty(); + window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations); + window_flags.set( + WindowFlags::MARKER_UNDECORATED_SHADOW, + attributes.platform_specific.decoration_shadow, + ); + window_flags + .set(WindowFlags::ALWAYS_ON_TOP, attributes.window_level == WindowLevel::AlwaysOnTop); + window_flags + .set(WindowFlags::ALWAYS_ON_BOTTOM, attributes.window_level == WindowLevel::AlwaysOnBottom); + window_flags + .set(WindowFlags::NO_BACK_BUFFER, attributes.platform_specific.no_redirection_bitmap); + window_flags.set(WindowFlags::MARKER_ACTIVATE, attributes.active); + 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); + // Will be changed later using `window.set_enabled_buttons` but we need to set a default here + // so the diffing later can work. + window_flags.set(WindowFlags::CLOSABLE, true); + window_flags.set(WindowFlags::CLIP_CHILDREN, attributes.platform_specific.clip_children); + + let mut fallback_parent = || match attributes.platform_specific.owner { + Some(parent) => { + window_flags.set(WindowFlags::POPUP, true); + Some(parent) + }, + None => { + window_flags.set(WindowFlags::ON_TASKBAR, true); + None + }, + }; + + #[cfg(feature = "rwh_06")] + let parent = match attributes.parent_window.as_ref().map(|handle| handle.0) { + Some(rwh_06::RawWindowHandle::Win32(handle)) => { + window_flags.set(WindowFlags::CHILD, true); + if attributes.platform_specific.menu.is_some() { + warn!("Setting a menu on a child window is unsupported"); + } + Some(handle.hwnd.get() as HWND) + }, + Some(raw) => unreachable!("Invalid raw window handle {raw:?} on Windows"), + None => fallback_parent(), + }; + + #[cfg(not(feature = "rwh_06"))] + let parent = fallback_parent(); + + let menu = attributes.platform_specific.menu; + let fullscreen = attributes.fullscreen.clone(); + let maximized = attributes.maximized; + let mut initdata = InitData { event_loop, attributes, window_flags, window: None }; + + let (style, ex_style) = window_flags.to_window_styles(); + let handle = unsafe { + CreateWindowExW( + ex_style, + class_name.as_ptr(), + title.as_ptr(), + style, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + parent.unwrap_or(0), + menu.unwrap_or(0), + util::get_instance_handle(), + &mut initdata as *mut _ as *mut _, + ) + }; + + // If the window creation in `InitData` panicked, then should resume panicking here + if let Err(panic_error) = event_loop.runner_shared.take_panic_error() { + panic::resume_unwind(panic_error) + } + + if handle == 0 { + return Err(os_error!(io::Error::last_os_error())); + } + + // If the handle is non-null, then window creation must have succeeded, which means + // that we *must* have populated the `InitData.window` field. + let win = initdata.window.unwrap(); + + // Need to set FULLSCREEN or MAXIMIZED after CreateWindowEx + // This is because if the size is changed in WM_CREATE, the restored size will be stored in that + // size. + if fullscreen.is_some() { + win.set_fullscreen(fullscreen.map(Into::into)); + unsafe { force_window_active(win.window) }; + } else if maximized { + win.set_maximized(true); + } + + Ok(win) +} + +unsafe fn register_window_class(class_name: &[u16]) { + let class = WNDCLASSEXW { + cbSize: mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(super::event_loop::public_window_callback), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: util::get_instance_handle(), + hIcon: 0, + hCursor: 0, // must be null in order for cursor state to work properly + hbrBackground: 0, + lpszMenuName: ptr::null(), + lpszClassName: class_name.as_ptr(), + hIconSm: 0, + }; + + // 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. + unsafe { RegisterClassExW(&class) }; +} + +struct ComInitialized(#[allow(dead_code)] *mut ()); +impl Drop for ComInitialized { + fn drop(&mut self) { + unsafe { CoUninitialize() }; + } +} + +thread_local! { + static COM_INITIALIZED: ComInitialized = { + unsafe { + CoInitializeEx(ptr::null(), COINIT_APARTMENTTHREADED as u32); + ComInitialized(ptr::null_mut()) + } + }; + + static TASKBAR_LIST: Cell<*mut ITaskbarList> = const { Cell::new(ptr::null_mut()) }; + static TASKBAR_LIST2: Cell<*mut ITaskbarList2> = const { Cell::new(ptr::null_mut()) }; +} + +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_LIST2.with(|task_bar_list2_ptr| { + let mut task_bar_list2 = task_bar_list2_ptr.get(); + + if task_bar_list2.is_null() { + let hr = unsafe { + CoCreateInstance( + &CLSID_TaskbarList, + ptr::null_mut(), + CLSCTX_ALL, + &IID_ITaskbarList2, + &mut task_bar_list2 as *mut _ as *mut _, + ) + }; + if hr != S_OK { + // In visual studio retrieving the taskbar list fails + return; + } + + let hr_init = unsafe { (*(*task_bar_list2).lpVtbl).parent.HrInit }; + if unsafe { hr_init(task_bar_list2.cast()) } != S_OK { + // In some old windows, the taskbar object could not be created, we just ignore it + return; + } + task_bar_list2_ptr.set(task_bar_list2) + } + + task_bar_list2 = task_bar_list2_ptr.get(); + let mark_fullscreen_window = unsafe { (*(*task_bar_list2).lpVtbl).MarkFullscreenWindow }; + unsafe { mark_fullscreen_window(task_bar_list2, handle, fullscreen.into()) }; + }) +} + +pub(crate) unsafe fn set_skip_taskbar(hwnd: HWND, skip: bool) { + com_initialized(); + TASKBAR_LIST.with(|task_bar_list_ptr| { + let mut task_bar_list = task_bar_list_ptr.get(); + + if task_bar_list.is_null() { + let hr = unsafe { + CoCreateInstance( + &CLSID_TaskbarList, + ptr::null_mut(), + CLSCTX_ALL, + &IID_ITaskbarList, + &mut task_bar_list as *mut _ as *mut _, + ) + }; + if hr != S_OK { + // In visual studio retrieving the taskbar list fails + return; + } + + let hr_init = unsafe { (*(*task_bar_list).lpVtbl).HrInit }; + if unsafe { hr_init(task_bar_list.cast()) } != S_OK { + // In some old windows, the taskbar object could not be created, we just ignore it + return; + } + task_bar_list_ptr.set(task_bar_list) + } + + task_bar_list = task_bar_list_ptr.get(); + if skip { + let delete_tab = unsafe { (*(*task_bar_list).lpVtbl).DeleteTab }; + unsafe { delete_tab(task_bar_list, hwnd) }; + } else { + let add_tab = unsafe { (*(*task_bar_list).lpVtbl).AddTab }; + unsafe { add_tab(task_bar_list, hwnd) }; + } + }); +} + +unsafe fn force_window_active(handle: HWND) { + // In some situation, 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 = unsafe { MapVirtualKeyW(VK_MENU as u32, MAPVK_VK_TO_VSC) }; + + let inputs = [ + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_LMENU, + wScan: alt_sc as u16, + dwFlags: KEYEVENTF_EXTENDEDKEY, + dwExtraInfo: 0, + time: 0, + }, + }, + }, + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_LMENU, + wScan: alt_sc as u16, + dwFlags: KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, + dwExtraInfo: 0, + time: 0, + }, + }, + }, + ]; + + // Simulate a key press and release + unsafe { SendInput(inputs.len() as u32, inputs.as_ptr(), mem::size_of::() as i32) }; + + unsafe { SetForegroundWindow(handle) }; +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/window_state.rs b/third_party/winit-0.30.13/src/platform_impl/windows/window_state.rs new file mode 100644 index 0000000..8e24a4f --- /dev/null +++ b/third_party/winit-0.30.13/src/platform_impl/windows/window_state.rs @@ -0,0 +1,545 @@ +use crate::dpi::{PhysicalPosition, PhysicalSize, Size}; +use crate::icon::Icon; +use crate::keyboard::ModifiersState; +use crate::platform_impl::platform::{event_loop, util, Fullscreen, SelectedCursor}; +use crate::window::{Theme, WindowAttributes}; +use bitflags::bitflags; +use std::io; +use std::sync::MutexGuard; +use windows_sys::Win32::Foundation::{HWND, RECT}; +use windows_sys::Win32::Graphics::Gdi::InvalidateRgn; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + AdjustWindowRectEx, EnableMenuItem, GetMenu, GetSystemMenu, GetWindowLongW, SendMessageW, + SetWindowLongW, SetWindowPos, ShowWindow, GWL_EXSTYLE, GWL_STYLE, HWND_BOTTOM, HWND_NOTOPMOST, + HWND_TOPMOST, MF_BYCOMMAND, MF_DISABLED, MF_ENABLED, SC_CLOSE, SWP_ASYNCWINDOWPOS, + SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOREPOSITION, SWP_NOSIZE, SWP_NOZORDER, + SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOW, SW_SHOWNOACTIVATE, WINDOWPLACEMENT, + WINDOW_EX_STYLE, WINDOW_STYLE, WS_BORDER, WS_CAPTION, WS_CHILD, WS_CLIPCHILDREN, + WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, WS_EX_LAYERED, WS_EX_NOREDIRECTIONBITMAP, + WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_EX_WINDOWEDGE, WS_MAXIMIZE, WS_MAXIMIZEBOX, WS_MINIMIZE, + WS_MINIMIZEBOX, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, WS_SYSMENU, WS_VISIBLE, +}; + +/// Contains information about states and the window that the callback is going to use. +pub(crate) struct WindowState { + pub mouse: MouseProperties, + + /// Used by `WM_GETMINMAXINFO`. + pub min_size: Option, + pub max_size: Option, + + pub resize_increments: Option, + + pub window_icon: Option, + pub taskbar_icon: Option, + + pub saved_window: Option, + pub scale_factor: f64, + + pub modifiers_state: ModifiersState, + pub fullscreen: Option, + pub current_theme: Theme, + pub preferred_theme: Option, + + pub window_flags: WindowFlags, + + pub ime_state: ImeState, + pub ime_allowed: bool, + + // Used by WM_NCACTIVATE, WM_SETFOCUS and WM_KILLFOCUS + pub is_active: bool, + pub is_focused: bool, + + // Flag whether redraw was requested. + pub redraw_requested: bool, + + pub dragging: bool, + + pub skip_taskbar: bool, +} + +#[derive(Clone)] +pub struct SavedWindow { + pub placement: WINDOWPLACEMENT, +} + +#[derive(Clone)] +pub struct MouseProperties { + pub(crate) selected_cursor: SelectedCursor, + pub capture_count: u32, + cursor_flags: CursorFlags, + pub last_position: Option>, +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct CursorFlags: u8 { + const GRABBED = 1 << 0; + const HIDDEN = 1 << 1; + const IN_WINDOW = 1 << 2; + const LOCKED = 1 << 3; + } +} +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct WindowFlags: u32 { + const RESIZABLE = 1 << 0; + const MINIMIZABLE = 1 << 1; + const MAXIMIZABLE = 1 << 2; + const CLOSABLE = 1 << 3; + const VISIBLE = 1 << 4; + const ON_TASKBAR = 1 << 5; + const ALWAYS_ON_TOP = 1 << 6; + const ALWAYS_ON_BOTTOM = 1 << 7; + const NO_BACK_BUFFER = 1 << 8; + const TRANSPARENT = 1 << 9; + const CHILD = 1 << 10; + const MAXIMIZED = 1 << 11; + const POPUP = 1 << 12; + + /// Marker flag for fullscreen. Should always match `WindowState::fullscreen`, but is + /// included here to make masking easier. + const MARKER_EXCLUSIVE_FULLSCREEN = 1 << 13; + const MARKER_BORDERLESS_FULLSCREEN = 1 << 14; + + /// 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 << 15; + + const MARKER_IN_SIZE_MOVE = 1 << 16; + + const MINIMIZED = 1 << 17; + + const IGNORE_CURSOR_EVENT = 1 << 18; + + /// Fully decorated window (incl. caption, border and drop shadow). + const MARKER_DECORATIONS = 1 << 19; + /// Drop shadow for undecorated windows. + const MARKER_UNDECORATED_SHADOW = 1 << 20; + + const MARKER_ACTIVATE = 1 << 21; + + const CLIP_CHILDREN = 1 << 22; + + const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits(); + } +} + +#[derive(Eq, PartialEq)] +pub enum ImeState { + Disabled, + Enabled, + Preedit, +} + +impl WindowState { + pub(crate) fn new( + attributes: &WindowAttributes, + scale_factor: f64, + current_theme: Theme, + preferred_theme: Option, + ) -> WindowState { + WindowState { + mouse: MouseProperties { + selected_cursor: SelectedCursor::default(), + capture_count: 0, + cursor_flags: CursorFlags::empty(), + last_position: None, + }, + + min_size: attributes.min_inner_size, + max_size: attributes.max_inner_size, + + resize_increments: attributes.resize_increments, + + window_icon: attributes.window_icon.clone(), + taskbar_icon: None, + + saved_window: None, + scale_factor, + + modifiers_state: ModifiersState::default(), + fullscreen: None, + current_theme, + preferred_theme, + window_flags: WindowFlags::empty(), + + ime_state: ImeState::Disabled, + ime_allowed: false, + + is_active: false, + is_focused: false, + redraw_requested: false, + + dragging: false, + + skip_taskbar: false, + } + } + + 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) { + // Required styles to properly support common window functionality like aero snap. + let mut style = WS_CAPTION | WS_BORDER | WS_CLIPSIBLINGS | WS_SYSMENU; + let mut 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 | WS_BORDER); + 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.contains(WindowFlags::CLIP_CHILDREN) { + style |= WS_CLIPCHILDREN; + } + + if self.intersects( + WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN | WindowFlags::MARKER_BORDERLESS_FULLSCREEN, + ) { + style &= !WS_OVERLAPPEDWINDOW; + } + + (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) { + let flag = if !self.contains(WindowFlags::MARKER_ACTIVATE) { + self.set(WindowFlags::MARKER_ACTIVATE, true); + SW_SHOWNOACTIVATE + } else { + SW_SHOW + }; + unsafe { + ShowWindow(window, flag); + } + } + + if diff.intersects(WindowFlags::ALWAYS_ON_TOP | WindowFlags::ALWAYS_ON_BOTTOM) { + unsafe { + SetWindowPos( + window, + match ( + new.contains(WindowFlags::ALWAYS_ON_TOP), + new.contains(WindowFlags::ALWAYS_ON_BOTTOM), + ) { + (true, false) => HWND_TOPMOST, + (false, false) => HWND_NOTOPMOST, + (false, true) => HWND_BOTTOM, + (true, true) => unreachable!(), + }, + 0, + 0, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, + ); + InvalidateRgn(window, 0, false.into()); + } + } + + if diff.contains(WindowFlags::MAXIMIZED) || new.contains(WindowFlags::MAXIMIZED) { + unsafe { + 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 { + 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) { + let flags = MF_BYCOMMAND + | if new.contains(WindowFlags::CLOSABLE) { MF_ENABLED } else { MF_DISABLED }; + + unsafe { + EnableMenuItem(GetSystemMenu(window, 0), SC_CLOSE, flags); + } + } + + if !new.contains(WindowFlags::VISIBLE) { + unsafe { + 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.get(), 1, 0); + + // This condition is necessary to avoid having an unrestorable window + if !new.contains(WindowFlags::MINIMIZED) { + SetWindowLongW(window, GWL_STYLE, style as i32); + SetWindowLongW(window, GWL_EXSTYLE, style_ex 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 + SetWindowPos(window, 0, 0, 0, 0, 0, flags); + SendMessageW(window, event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID.get(), 0, 0); + } + } + } + + pub fn adjust_rect(self, hwnd: HWND, mut rect: RECT) -> Result { + unsafe { + let mut style = GetWindowLongW(hwnd, GWL_STYLE) as u32; + let style_ex = GetWindowLongW(hwnd, GWL_EXSTYLE) as u32; + + // Frameless style implemented by manually overriding the non-client area in + // `WM_NCCALCSIZE`. + if !self.contains(WindowFlags::MARKER_DECORATIONS) { + style &= !(WS_CAPTION | WS_SIZEBOX); + } + + util::win_to_err({ + let b_menu = GetMenu(hwnd) != 0; + if let (Some(get_dpi_for_window), Some(adjust_window_rect_ex_for_dpi)) = + (*util::GET_DPI_FOR_WINDOW, *util::ADJUST_WINDOW_RECT_EX_FOR_DPI) + { + let dpi = get_dpi_for_window(hwnd); + adjust_window_rect_ex_for_dpi(&mut rect, style, b_menu.into(), style_ex, dpi) + } else { + AdjustWindowRectEx(&mut rect, style, b_menu.into(), style_ex) + } + })?; + Ok(rect) + } + } + + pub fn adjust_size(self, hwnd: HWND, size: PhysicalSize) -> 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 = self.adjust_rect(hwnd, rect).unwrap_or(rect); + + let outer_x = (rect.right - rect.left).abs(); + let outer_y = (rect.top - rect.bottom).abs(); + + PhysicalSize::new(outer_x as _, outer_y as _) + } + + pub fn set_size(self, hwnd: HWND, size: PhysicalSize) { + unsafe { + let (width, height): (u32, u32) = self.adjust_size(hwnd, size).into(); + SetWindowPos( + hwnd, + 0, + 0, + 0, + width as _, + height as _, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOREPOSITION | SWP_NOMOVE | SWP_NOACTIVATE, + ); + InvalidateRgn(hwnd, 0, false.into()); + } + } +} + +impl CursorFlags { + fn refresh_os_cursor(self, window: HWND) -> Result<(), io::Error> { + let client_rect = util::WindowArea::Inner.get_rect(window)?; + + if util::is_focused(window) { + let cursor_clip = match self.contains(CursorFlags::GRABBED) { + true => { + if self.contains(CursorFlags::LOCKED) { + if let Ok(pos) = util::get_cursor_position() { + Some(RECT { + left: pos.x, + right: pos.x + 1, + top: pos.y, + bottom: pos.y + 1, + }) + } else { + // If lock is applied while the cursor is not available, lock it to the + // middle of the window. + let cx = (client_rect.left + client_rect.right) / 2; + let cy = (client_rect.top + client_rect.bottom) / 2; + Some(RECT { left: cx, right: cx + 1, top: cy, bottom: cy + 1 }) + } + } else if self.contains(CursorFlags::HIDDEN) { + // Confine the cursor to the center of the window if the cursor is hidden. + // This avoids problems with the cursor activating + // the taskbar if the window borders or overlaps that. + let cx = (client_rect.left + client_rect.right) / 2; + let cy = (client_rect.top + client_rect.bottom) / 2; + Some(RECT { left: cx, right: cx + 1, top: cy, bottom: cy + 1 }) + } else { + 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/third_party/winit-0.30.13/src/utils.rs b/third_party/winit-0.30.13/src/utils.rs new file mode 100644 index 0000000..d9631ed --- /dev/null +++ b/third_party/winit-0.30.13/src/utils.rs @@ -0,0 +1,28 @@ +// A poly-fill for `lazy_cell` +// Replace with std::sync::LazyLock when https://github.com/rust-lang/rust/issues/109736 is stabilized. + +// This isn't used on every platform, which can come up as dead code warnings. +#![allow(dead_code)] + +use std::ops::Deref; +use std::sync::OnceLock; + +pub(crate) struct Lazy { + cell: OnceLock, + init: fn() -> T, +} + +impl Lazy { + pub const fn new(f: fn() -> T) -> Self { + Self { cell: OnceLock::new(), init: f } + } +} + +impl Deref for Lazy { + type Target = T; + + #[inline] + fn deref(&self) -> &'_ T { + self.cell.get_or_init(self.init) + } +} diff --git a/third_party/winit-0.30.13/src/window.rs b/third_party/winit-0.30.13/src/window.rs new file mode 100644 index 0000000..e0158ef --- /dev/null +++ b/third_party/winit-0.30.13/src/window.rs @@ -0,0 +1,1876 @@ +//! The [`Window`] struct and associated types. +use std::fmt; + +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError}; +use crate::monitor::{MonitorHandle, VideoModeHandle}; +use crate::platform_impl::{self, PlatformSpecificWindowAttributes}; + +pub use crate::cursor::{BadImage, Cursor, CustomCursor, CustomCursorSource, MAX_CURSOR_SIZE}; +pub use crate::icon::{BadIcon, Icon}; + +#[doc(inline)] +pub use cursor_icon::{CursorIcon, ParseError as CursorIconParseError}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Represents a window. +/// +/// The window is closed when dropped. +/// +/// ## Threading +/// +/// This is `Send + Sync`, meaning that it can be freely used from other +/// threads. +/// +/// However, some platforms (macOS, Web and iOS) only allow user interface +/// interactions on the main thread, so on those platforms, if you use the +/// window from a thread other than the main, the code is scheduled to run on +/// the main thread, and your thread may be blocked until that completes. +/// +/// ## Platform-specific +/// +/// **Web:** The [`Window`], which is represented by a `HTMLElementCanvas`, can +/// not be closed by dropping the [`Window`]. +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 { + /// This will close the [`Window`]. + /// + /// See [`Window`] for more details. + fn drop(&mut self) { + self.window.maybe_wait_on_main(|w| { + // 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(_)) = w.fullscreen().map(|f| f.into()) { + w.set_fullscreen(None); + } + }) + } +} + +/// Identifier of a window. Unique for each window. +/// +/// Can be obtained with [`window.id()`][`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(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(pub(crate) platform_impl::WindowId); + +impl WindowId { + /// Returns a dummy id, useful for unit testing. + /// + /// # Notes + /// + /// 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`]. + pub const fn dummy() -> Self { + WindowId(platform_impl::WindowId::dummy()) + } +} + +impl fmt::Debug for WindowId { + fn fmt(&self, fmtr: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(fmtr) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0.into() + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id.into()) + } +} + +/// Attributes used when creating a window. +#[derive(Debug, Clone)] +pub struct WindowAttributes { + pub inner_size: Option, + pub min_inner_size: Option, + pub max_inner_size: Option, + pub position: Option, + pub resizable: bool, + pub enabled_buttons: WindowButtons, + pub title: String, + pub maximized: bool, + pub visible: bool, + pub transparent: bool, + pub blur: bool, + pub decorations: bool, + pub window_icon: Option, + pub preferred_theme: Option, + pub resize_increments: Option, + pub content_protected: bool, + pub window_level: WindowLevel, + pub active: bool, + pub cursor: Cursor, + #[cfg(feature = "rwh_06")] + pub(crate) parent_window: Option, + pub fullscreen: Option, + // Platform-specific configuration. + #[allow(dead_code)] + pub(crate) platform_specific: PlatformSpecificWindowAttributes, +} + +impl Default for WindowAttributes { + #[inline] + fn default() -> WindowAttributes { + WindowAttributes { + inner_size: None, + min_inner_size: None, + max_inner_size: None, + position: None, + resizable: true, + enabled_buttons: WindowButtons::all(), + title: "winit window".to_owned(), + maximized: false, + fullscreen: None, + visible: true, + transparent: false, + blur: false, + decorations: true, + window_level: Default::default(), + window_icon: None, + preferred_theme: None, + resize_increments: None, + content_protected: false, + cursor: Cursor::default(), + #[cfg(feature = "rwh_06")] + parent_window: None, + active: true, + platform_specific: Default::default(), + } + } +} + +/// Wrapper for [`rwh_06::RawWindowHandle`] for [`WindowAttributes::parent_window`]. +/// +/// # Safety +/// +/// The user has to account for that when using [`WindowAttributes::with_parent_window()`], +/// which is `unsafe`. +#[derive(Debug, Clone)] +#[cfg(feature = "rwh_06")] +pub(crate) struct SendSyncRawWindowHandle(pub(crate) rwh_06::RawWindowHandle); + +#[cfg(feature = "rwh_06")] +unsafe impl Send for SendSyncRawWindowHandle {} +#[cfg(feature = "rwh_06")] +unsafe impl Sync for SendSyncRawWindowHandle {} + +impl WindowAttributes { + /// Initializes new attributes with default values. + #[inline] + #[deprecated = "use `Window::default_attributes` instead"] + pub fn new() -> Self { + Default::default() + } +} + +impl WindowAttributes { + /// Get the parent window stored on the attributes. + #[cfg(feature = "rwh_06")] + pub fn parent_window(&self) -> Option<&rwh_06::RawWindowHandle> { + self.parent_window.as_ref().map(|handle| &handle.0) + } + + /// Requests the window to be of specific dimensions. + /// + /// If this is not set, some platform-specific dimensions will be used. + /// + /// See [`Window::request_inner_size`] for details. + #[inline] + pub fn with_inner_size>(mut self, size: S) -> Self { + self.inner_size = Some(size.into()); + self + } + + /// Sets the minimum dimensions a window can have. + /// + /// If this is not set, the window will have no minimum dimensions (aside + /// from reserved). + /// + /// See [`Window::set_min_inner_size`] for details. + #[inline] + pub fn with_min_inner_size>(mut self, min_size: S) -> Self { + self.min_inner_size = Some(min_size.into()); + self + } + + /// Sets the maximum dimensions a window can have. + /// + /// If this is not set, the window will have no maximum or will be set to + /// the primary monitor's dimensions by the platform. + /// + /// See [`Window::set_max_inner_size`] for details. + #[inline] + pub fn with_max_inner_size>(mut self, max_size: S) -> Self { + self.max_inner_size = Some(max_size.into()); + self + } + + /// Sets a desired initial position for the window. + /// + /// If this is not set, some platform-specific position will be chosen. + /// + /// See [`Window::set_outer_position`] for details. + /// + /// ## 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. + /// - **X11:** The top left corner of the window, the window's "outer" position. + /// - **Others:** Ignored. + #[inline] + pub fn with_position>(mut self, position: P) -> Self { + self.position = Some(position.into()); + self + } + + /// Sets whether the window is resizable or not. + /// + /// The default is `true`. + /// + /// See [`Window::set_resizable`] for details. + #[inline] + pub fn with_resizable(mut self, resizable: bool) -> Self { + self.resizable = resizable; + self + } + + /// Sets the enabled window buttons. + /// + /// The default is [`WindowButtons::all`] + /// + /// See [`Window::set_enabled_buttons`] for details. + #[inline] + pub fn with_enabled_buttons(mut self, buttons: WindowButtons) -> Self { + self.enabled_buttons = buttons; + self + } + + /// Sets the initial title of the window in the title bar. + /// + /// The default is `"winit window"`. + /// + /// See [`Window::set_title`] for details. + #[inline] + pub fn with_title>(mut self, title: T) -> Self { + self.title = title.into(); + self + } + + /// Sets whether the window should be put into fullscreen upon creation. + /// + /// The default is `None`. + /// + /// See [`Window::set_fullscreen`] for details. + #[inline] + pub fn with_fullscreen(mut self, fullscreen: Option) -> Self { + self.fullscreen = fullscreen; + self + } + + /// Request that the window is maximized upon creation. + /// + /// The default is `false`. + /// + /// See [`Window::set_maximized`] for details. + #[inline] + pub fn with_maximized(mut self, maximized: bool) -> Self { + self.maximized = maximized; + self + } + + /// Sets whether the window will be initially visible or hidden. + /// + /// The default is to show the window. + /// + /// See [`Window::set_visible`] for details. + #[inline] + pub fn with_visible(mut self, visible: bool) -> Self { + self.visible = visible; + self + } + + /// Sets whether the background of the window should be transparent. + /// + /// If this is `true`, writing colors with alpha values different than + /// `1.0` will produce a transparent window. On some platforms this + /// is more of a hint for the system and you'd still have the alpha + /// buffer. To control it see [`Window::set_transparent`]. + /// + /// The default is `false`. + #[inline] + pub fn with_transparent(mut self, transparent: bool) -> Self { + self.transparent = transparent; + self + } + + /// Sets whether the background of the window should be blurred by the system. + /// + /// The default is `false`. + /// + /// See [`Window::set_blur`] for details. + #[inline] + pub fn with_blur(mut self, blur: bool) -> Self { + self.blur = blur; + self + } + + /// Get whether the window will support transparency. + #[inline] + pub fn transparent(&self) -> bool { + self.transparent + } + + /// Sets whether the window should have a border, a title bar, etc. + /// + /// The default is `true`. + /// + /// See [`Window::set_decorations`] for details. + #[inline] + pub fn with_decorations(mut self, decorations: bool) -> Self { + self.decorations = decorations; + self + } + + /// Sets the window level. + /// + /// This is just a hint to the OS, and the system could ignore it. + /// + /// The default is [`WindowLevel::Normal`]. + /// + /// See [`WindowLevel`] for details. + #[inline] + pub fn with_window_level(mut self, level: WindowLevel) -> Self { + self.window_level = level; + self + } + + /// Sets the window icon. + /// + /// The default is `None`. + /// + /// See [`Window::set_window_icon`] for details. + #[inline] + pub fn with_window_icon(mut self, window_icon: Option) -> Self { + self.window_icon = window_icon; + self + } + + /// Sets a specific theme for the window. + /// + /// If `None` is provided, the window will use the system theme. + /// + /// The default is `None`. + /// + /// ## Platform-specific + /// + /// - **Wayland:** This controls only CSD. When using `None` it'll try to use dbus to get the + /// system preference. When explicit theme is used, this will avoid dbus all together. + /// - **x11:** Build window with `_GTK_THEME_VARIANT` hint set to `dark` or `light`. + /// - **iOS / Android / Web / x11 / Orbital:** Ignored. + #[inline] + pub fn with_theme(mut self, theme: Option) -> Self { + self.preferred_theme = theme; + self + } + + /// Build window with resize increments hint. + /// + /// The default is `None`. + /// + /// See [`Window::set_resize_increments`] for details. + #[inline] + pub fn with_resize_increments>(mut self, resize_increments: S) -> Self { + self.resize_increments = Some(resize_increments.into()); + self + } + + /// Prevents the window contents from being captured by other apps. + /// + /// The default is `false`. + /// + /// ## Platform-specific + /// + /// - **macOS**: if `false`, [`NSWindowSharingNone`] is used but doesn't completely prevent all + /// apps from reading the window content, for instance, QuickTime. + /// - **iOS / Android / Web / x11 / Orbital:** Ignored. + /// + /// [`NSWindowSharingNone`]: https://developer.apple.com/documentation/appkit/nswindowsharingtype/nswindowsharingnone + #[inline] + pub fn with_content_protected(mut self, protected: bool) -> Self { + self.content_protected = protected; + self + } + + /// Whether the window will be initially focused or not. + /// + /// The window should be assumed as not focused by default + /// following by the [`WindowEvent::Focused`]. + /// + /// ## Platform-specific: + /// + /// **Android / iOS / X11 / Wayland / Orbital:** Unsupported. + /// + /// [`WindowEvent::Focused`]: crate::event::WindowEvent::Focused. + #[inline] + pub fn with_active(mut self, active: bool) -> Self { + self.active = active; + self + } + + /// Modifies the cursor icon of the window. + /// + /// The default is [`CursorIcon::Default`]. + /// + /// See [`Window::set_cursor()`] for more details. + #[inline] + pub fn with_cursor(mut self, cursor: impl Into) -> Self { + self.cursor = cursor.into(); + self + } + + /// Build window with parent window. + /// + /// The default is `None`. + /// + /// ## Safety + /// + /// `parent_window` must be a valid window handle. + /// + /// ## Platform-specific + /// + /// - **Windows** : A child window has the WS_CHILD style and is confined + /// to the client area of its parent window. For more information, see + /// + /// - **X11**: A child window is confined to the client area of its parent window. + /// - **Android / iOS / Wayland / Web:** Unsupported. + #[cfg(feature = "rwh_06")] + #[inline] + pub unsafe fn with_parent_window( + mut self, + parent_window: Option, + ) -> Self { + self.parent_window = parent_window.map(SendSyncRawWindowHandle); + self + } +} + +/// Base Window functions. +impl Window { + /// Create a new [`WindowAttributes`] which allows modifying the window's attributes before + /// creation. + #[inline] + pub fn default_attributes() -> WindowAttributes { + WindowAttributes::default() + } + + /// Returns an identifier unique to the window. + #[inline] + pub fn id(&self) -> WindowId { + let _span = tracing::debug_span!("winit::Window::id",).entered(); + + self.window.maybe_wait_on_main(|w| WindowId(w.id())) + } + + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and + /// vice versa. + /// + /// 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. + /// + /// This value may differ from [`MonitorHandle::scale_factor`]. + /// + /// See the [`dpi`] crate for more information. + /// + /// ## Platform-specific + /// + /// The scale factor is calculated differently on different platforms: + /// + /// - **Windows:** On Windows 8 and 10, per-monitor scaling is readily configured by users from + /// the display settings. While users are free to select any option they want, they're only + /// given a selection of "nice" scale factors, i.e. 1.0, 1.25, 1.5... on Windows 7. The scale + /// factor is global and changing it requires logging out. See [this article][windows_1] for + /// technical details. + /// - **macOS:** Recent macOS versions allow the user to change the scaling factor for specific + /// displays. When available, the user may pick a per-monitor scaling factor from a set of + /// pre-defined settings. All "retina displays" have a scaling factor above 1.0 by default, + /// but the specific value varies across devices. + /// - **X11:** Many man-hours have been spent trying to figure out how to handle DPI in X11. + /// Winit currently uses a three-pronged approach: + /// + Use the value in the `WINIT_X11_SCALE_FACTOR` environment variable if present. + /// + If not present, use the value set in `Xft.dpi` in Xresources. + /// + Otherwise, calculate the scale factor based on the millimeter monitor dimensions + /// provided by XRandR. + /// + /// If `WINIT_X11_SCALE_FACTOR` is set to `randr`, it'll ignore the `Xft.dpi` field and use + /// the XRandR scaling method. Generally speaking, you should try to configure the + /// standard system variables to do what you want before resorting to + /// `WINIT_X11_SCALE_FACTOR`. + /// - **Wayland:** The scale factor is suggested by the compositor for each window individually + /// by using the wp-fractional-scale protocol if available. Falls back to integer-scale + /// factors otherwise. + /// + /// The monitor scale factor may differ from the window scale factor. + /// - **iOS:** Scale factors are set by Apple to the value that best suits the device, and range + /// from `1.0` to `3.0`. See [this article][apple_1] and [this article][apple_2] for more + /// information. + /// + /// This uses the underlying `UIView`'s [`contentScaleFactor`]. + /// - **Android:** Scale factors are set by the manufacturer to the value that best suits the + /// device, and range from `1.0` to `4.0`. See [this article][android_1] for more information. + /// + /// This is currently unimplemented, and this function always returns 1.0. + /// - **Web:** The scale factor is the ratio between CSS pixels and the physical device pixels. + /// In other words, it is the value of [`window.devicePixelRatio`][web_1]. It is affected by + /// both the screen scaling and the browser zoom level and can go below `1.0`. + /// - **Orbital:** This is currently unimplemented, and this function always returns 1.0. + /// + /// [`WindowEvent::ScaleFactorChanged`]: crate::event::WindowEvent::ScaleFactorChanged + /// [windows_1]: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows + /// [apple_1]: https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html + /// [apple_2]: https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/image-size-and-resolution/ + /// [android_1]: https://developer.android.com/training/multiscreen/screendensities + /// [web_1]: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio + /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc + #[inline] + pub fn scale_factor(&self) -> f64 { + let _span = tracing::debug_span!("winit::Window::scale_factor",).entered(); + + self.window.maybe_wait_on_main(|w| w.scale_factor()) + } + + /// Queues a [`WindowEvent::RedrawRequested`] event to be emitted that aligns with the windowing + /// system drawing 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). To improve the event delivery + /// consider using [`Window::pre_present_notify`] as described in docs. + /// + /// Applications should always aim to redraw whenever they receive a `RedrawRequested` event. + /// + /// There are no strong guarantees about when exactly a `RedrawRequest` event will be emitted + /// with respect to other events, since the requirements can vary significantly between + /// windowing systems. + /// + /// However as the event aligns with the windowing system drawing loop, it may not arrive in + /// same or even next event loop iteration. + /// + /// ## Platform-specific + /// + /// - **Windows** This API uses `RedrawWindow` to request a `WM_PAINT` message and + /// `RedrawRequested` is emitted in sync with any `WM_PAINT` messages. + /// - **iOS:** Can only be called on the main thread. + /// - **Wayland:** The events are aligned with the frame callbacks when + /// [`Window::pre_present_notify`] is used. + /// - **Web:** [`WindowEvent::RedrawRequested`] will be aligned with the + /// `requestAnimationFrame`. + /// + /// [`WindowEvent::RedrawRequested`]: crate::event::WindowEvent::RedrawRequested + #[inline] + pub fn request_redraw(&self) { + let _span = tracing::debug_span!("winit::Window::request_redraw",).entered(); + + self.window.maybe_queue_on_main(|w| w.request_redraw()) + } + + /// Notify the windowing system before presenting to the window. + /// + /// You should call this event after your drawing operations, but before you submit + /// the buffer to the display or commit your drawings. Doing so will help winit to properly + /// schedule and make assumptions about its internal state. For example, it could properly + /// throttle [`WindowEvent::RedrawRequested`]. + /// + /// ## Example + /// + /// This example illustrates how it looks with OpenGL, but it applies to other graphics + /// APIs and software rendering. + /// + /// ```no_run + /// # use winit::window::Window; + /// # fn swap_buffers() {} + /// # fn scope(window: &Window) { + /// // Do the actual drawing with OpenGL. + /// + /// // Notify winit that we're about to submit buffer to the windowing system. + /// window.pre_present_notify(); + /// + /// // Submit buffer to the windowing system. + /// swap_buffers(); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **Android / iOS / X11 / Web / Windows / macOS / Orbital:** Unsupported. + /// - **Wayland:** Schedules a frame callback to throttle [`WindowEvent::RedrawRequested`]. + /// + /// [`WindowEvent::RedrawRequested`]: crate::event::WindowEvent::RedrawRequested + #[inline] + pub fn pre_present_notify(&self) { + let _span = tracing::debug_span!("winit::Window::pre_present_notify",).entered(); + + self.window.maybe_queue_on_main(|w| w.pre_present_notify()); + } + + /// 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. + /// + /// ## Platform-specific + /// - **Web, macOS:** Does nothing + // --------------------------- + // Developers' Note: If this cannot be implemented on every desktop platform + // at least, then this function should be provided through a platform specific + // extension trait + pub fn reset_dead_keys(&self) { + let _span = tracing::debug_span!("winit::Window::reset_dead_keys",).entered(); + + self.window.maybe_queue_on_main(|w| w.reset_dead_keys()) + } +} + +/// 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 [`Window::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. + /// - **Web:** Returns the top-left coordinates relative to the viewport. _Note: this returns + /// the same value as [`Window::outer_position`]._ + /// - **Android / Wayland:** Always returns [`NotSupportedError`]. + /// + /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + let _span = tracing::debug_span!("winit::Window::inner_position",).entered(); + + self.window.maybe_wait_on_main(|w| w.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. + /// - **Web:** Returns the top-left coordinates relative to the viewport. + /// - **Android / Wayland:** Always returns [`NotSupportedError`]. + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + let _span = tracing::debug_span!("winit::Window::outer_position",).entered(); + + self.window.maybe_wait_on_main(|w| w.outer_position()) + } + + /// Modifies the position of the window. + /// + /// See [`Window::outer_position`] for more information about the coordinates. + /// This automatically un-maximizes the window if it's maximized. + /// + /// ```no_run + /// # use winit::dpi::{LogicalPosition, PhysicalPosition}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the position in logical dimensions like this: + /// window.set_outer_position(LogicalPosition::new(400.0, 200.0)); + /// + /// // Or specify the position in physical dimensions like this: + /// window.set_outer_position(PhysicalPosition::new(400, 200)); + /// # } + /// ``` + /// + /// ## 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. + /// - **Web:** Sets the top-left coordinates relative to the viewport. Doesn't account for CSS + /// [`transform`]. + /// - **Android / Wayland:** Unsupported. + /// + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + #[inline] + pub fn set_outer_position>(&self, position: P) { + let position = position.into(); + let _span = tracing::debug_span!( + "winit::Window::set_outer_position", + position = ?position + ) + .entered(); + + self.window.maybe_queue_on_main(move |w| w.set_outer_position(position)) + } + + /// 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. + /// - **Web:** Returns the size of the canvas element. Doesn't account for CSS [`transform`]. + /// + /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let _span = tracing::debug_span!("winit::Window::inner_size",).entered(); + + self.window.maybe_wait_on_main(|w| w.inner_size()) + } + + /// Request the new size for the window. + /// + /// On platforms where the size is entirely controlled by the user the + /// applied size will be returned immediately, resize event in such case + /// may not be generated. + /// + /// On platforms where resizing is disallowed by the windowing system, the current + /// inner size is returned immediately, and the user one is ignored. + /// + /// When `None` is returned, it means that the request went to the display system, + /// and the actual size will be delivered later with the [`WindowEvent::Resized`]. + /// + /// See [`Window::inner_size`] for more information about the values. + /// + /// The request could automatically un-maximize the window if it's maximized. + /// + /// ```no_run + /// # use winit::dpi::{LogicalSize, PhysicalSize}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the size in logical dimensions like this: + /// let _ = window.request_inner_size(LogicalSize::new(400.0, 200.0)); + /// + /// // Or specify the size in physical dimensions like this: + /// let _ = window.request_inner_size(PhysicalSize::new(400, 200)); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **Web:** Sets the size of the canvas element. Doesn't account for CSS [`transform`]. + /// + /// [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + #[inline] + #[must_use] + pub fn request_inner_size>(&self, size: S) -> Option> { + let size = size.into(); + let _span = tracing::debug_span!( + "winit::Window::request_inner_size", + size = ?size + ) + .entered(); + self.window.maybe_wait_on_main(|w| w.request_inner_size(size)) + } + + /// 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 [`Window::inner_size`] instead. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the [`PhysicalSize`] of the window + /// in screen space coordinates. + /// - **Web:** Returns the size of the canvas element. _Note: this returns the same value as + /// [`Window::inner_size`]._ + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let _span = tracing::debug_span!("winit::Window::outer_size",).entered(); + self.window.maybe_wait_on_main(|w| w.outer_size()) + } + + /// Sets a minimum dimension size for the window. + /// + /// ```no_run + /// # use winit::dpi::{LogicalSize, PhysicalSize}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the size in logical dimensions like this: + /// window.set_min_inner_size(Some(LogicalSize::new(400.0, 200.0))); + /// + /// // Or specify the size in physical dimensions like this: + /// window.set_min_inner_size(Some(PhysicalSize::new(400, 200))); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Orbital:** Unsupported. + #[inline] + pub fn set_min_inner_size>(&self, min_size: Option) { + let min_size = min_size.map(|s| s.into()); + let _span = tracing::debug_span!( + "winit::Window::set_min_inner_size", + min_size = ?min_size + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_min_inner_size(min_size)) + } + + /// Sets a maximum dimension size for the window. + /// + /// ```no_run + /// # use winit::dpi::{LogicalSize, PhysicalSize}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the size in logical dimensions like this: + /// window.set_max_inner_size(Some(LogicalSize::new(400.0, 200.0))); + /// + /// // Or specify the size in physical dimensions like this: + /// window.set_max_inner_size(Some(PhysicalSize::new(400, 200))); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Orbital:** Unsupported. + #[inline] + pub fn set_max_inner_size>(&self, max_size: Option) { + let max_size = max_size.map(|s| s.into()); + let _span = tracing::debug_span!( + "winit::Window::max_size", + max_size = ?max_size + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_max_inner_size(max_size)) + } + + /// Returns window resize increments if any were set. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Always returns [`None`]. + #[inline] + pub fn resize_increments(&self) -> Option> { + let _span = tracing::debug_span!("winit::Window::resize_increments",).entered(); + self.window.maybe_wait_on_main(|w| w.resize_increments()) + } + + /// Sets window resize increments. + /// + /// This is a niche constraint hint usually employed by terminal emulators + /// and other apps that need "blocky" resizes. + /// + /// ## Platform-specific + /// + /// - **macOS:** Increments are converted to logical size and then macOS rounds them to whole + /// numbers. + /// - **iOS / Android / Web / Orbital:** Unsupported. + #[inline] + pub fn set_resize_increments>(&self, increments: Option) { + let increments = increments.map(Into::into); + let _span = tracing::debug_span!( + "winit::Window::set_resize_increments", + increments = ?increments + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_resize_increments(increments)) + } +} + +/// Misc. attribute functions. +impl Window { + /// Modifies the title of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_title(&self, title: &str) { + let _span = tracing::debug_span!("winit::Window::set_title", title).entered(); + self.window.maybe_wait_on_main(|w| w.set_title(title)) + } + + /// Change the window transparency state. + /// + /// This is just a hint that may not change anything about + /// the window transparency, however doing a mismatch between + /// the content of your window and this hint may result in + /// visual artifacts. + /// + /// The default value follows the [`WindowAttributes::with_transparent`]. + /// + /// ## Platform-specific + /// + /// - **macOS:** This will reset the window's background color. + /// - **Web / iOS / Android:** Unsupported. + /// - **X11:** Can only be set while building the window, with + /// [`WindowAttributes::with_transparent`]. + #[inline] + pub fn set_transparent(&self, transparent: bool) { + let _span = tracing::debug_span!("winit::Window::set_transparent", transparent).entered(); + self.window.maybe_queue_on_main(move |w| w.set_transparent(transparent)) + } + + /// Change the window blur state. + /// + /// If `true`, this will make the transparent window background blurry. + /// + /// ## Platform-specific + /// + /// - **Android / iOS / X11 / Web / Windows:** Unsupported. + /// - **Wayland:** Only works with org_kde_kwin_blur_manager protocol. + #[inline] + pub fn set_blur(&self, blur: bool) { + let _span = tracing::debug_span!("winit::Window::set_blur", blur).entered(); + self.window.maybe_queue_on_main(move |w| w.set_blur(blur)) + } + + /// Modifies the window's visibility. + /// + /// If `false`, this will hide the window. If `true`, this will show the window. + /// + /// ## Platform-specific + /// + /// - **Android / Wayland / Web:** Unsupported. + /// - **iOS:** Can only be called on the main thread. + #[inline] + pub fn set_visible(&self, visible: bool) { + let _span = tracing::debug_span!("winit::Window::set_visible", visible).entered(); + self.window.maybe_queue_on_main(move |w| w.set_visible(visible)) + } + + /// Gets the window's current visibility state. + /// + /// `None` means it couldn't be determined, so it is not recommended to use this to drive your + /// rendering backend. + /// + /// ## Platform-specific + /// + /// - **X11:** Not implemented. + /// - **Wayland / iOS / Android / Web:** Unsupported. + #[inline] + pub fn is_visible(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::is_visible",).entered(); + self.window.maybe_wait_on_main(|w| w.is_visible()) + } + + /// Sets whether the window is resizable or not. + /// + /// Note that making the window unresizable doesn't exempt you from handling + /// [`WindowEvent::Resized`], as that event can still be triggered by DPI scaling, entering + /// fullscreen mode, etc. Also, the window could still be resized by calling + /// [`Window::request_inner_size`]. + /// + /// ## Platform-specific + /// + /// This only has an effect on desktop platforms. + /// + /// - **X11:** Due to a bug in XFCE, this has no effect on Xfwm. + /// - **iOS / Android / Web:** Unsupported. + /// + /// [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized + #[inline] + pub fn set_resizable(&self, resizable: bool) { + let _span = tracing::debug_span!("winit::Window::set_resizable", resizable).entered(); + self.window.maybe_queue_on_main(move |w| w.set_resizable(resizable)) + } + + /// Gets the window's current resizable state. + /// + /// ## Platform-specific + /// + /// - **X11:** Not implemented. + /// - **iOS / Android / Web:** Unsupported. + #[inline] + pub fn is_resizable(&self) -> bool { + let _span = tracing::debug_span!("winit::Window::is_resizable",).entered(); + self.window.maybe_wait_on_main(|w| w.is_resizable()) + } + + /// Sets the enabled window buttons. + /// + /// ## Platform-specific + /// + /// - **Wayland / X11 / Orbital:** Not implemented. + /// - **Web / iOS / Android:** Unsupported. + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + let _span = tracing::debug_span!( + "winit::Window::set_enabled_buttons", + buttons = ?buttons + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_enabled_buttons(buttons)) + } + + /// Gets the enabled window buttons. + /// + /// ## Platform-specific + /// + /// - **Wayland / X11 / Orbital:** Not implemented. Always returns [`WindowButtons::all`]. + /// - **Web / iOS / Android:** Unsupported. Always returns [`WindowButtons::all`]. + pub fn enabled_buttons(&self) -> WindowButtons { + let _span = tracing::debug_span!("winit::Window::enabled_buttons",).entered(); + self.window.maybe_wait_on_main(|w| w.enabled_buttons()) + } + + /// Sets the window to minimized or back + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Unsupported. + /// - **Wayland:** Un-minimize is unsupported. + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let _span = tracing::debug_span!("winit::Window::set_minimized", minimized).entered(); + self.window.maybe_queue_on_main(move |w| w.set_minimized(minimized)) + } + + /// Gets the window's current minimized state. + /// + /// `None` will be returned, if the minimized state couldn't be determined. + /// + /// ## Note + /// + /// - You shouldn't stop rendering for minimized windows, however you could lower the fps. + /// + /// ## Platform-specific + /// + /// - **Wayland**: always `None`. + /// - **iOS / Android / Web / Orbital:** Unsupported. + #[inline] + pub fn is_minimized(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::is_minimized",).entered(); + self.window.maybe_wait_on_main(|w| w.is_minimized()) + } + + /// Sets the window to maximized or back. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let _span = tracing::debug_span!("winit::Window::set_maximized", maximized).entered(); + self.window.maybe_queue_on_main(move |w| w.set_maximized(maximized)) + } + + /// Gets the window's current maximized state. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + #[inline] + pub fn is_maximized(&self) -> bool { + let _span = tracing::debug_span!("winit::Window::is_maximized",).entered(); + self.window.maybe_wait_on_main(|w| w.is_maximized()) + } + + /// 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 disabled in exclusive fullscreen mode. + /// - **iOS:** Can only be called on the main thread. + /// - **Wayland:** Does not support exclusive fullscreen mode and will no-op a request. + /// - **Windows:** Screen saver is disabled in fullscreen mode. + /// - **Android / Orbital:** Unsupported. + /// - **Web:** Does nothing without a [transient activation]. + /// + /// [transient activation]: https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation + #[inline] + pub fn set_fullscreen(&self, fullscreen: Option) { + let _span = tracing::debug_span!( + "winit::Window::set_fullscreen", + fullscreen = ?fullscreen + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_fullscreen(fullscreen.map(|f| f.into()))) + } + + /// Gets the window's current fullscreen state. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. + /// - **Android / Orbital:** Will always return `None`. + /// - **Wayland:** Can return `Borderless(None)` when there are no monitors. + /// - **Web:** Can only return `None` or `Borderless(None)`. + #[inline] + pub fn fullscreen(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::fullscreen",).entered(); + self.window.maybe_wait_on_main(|w| w.fullscreen().map(|f| f.into())) + } + + /// Turn window decorations on or off. + /// + /// Enable/disable window decorations provided by the server or Winit. + /// By default this is enabled. Note that fullscreen windows and windows on + /// mobile and web platforms naturally do not have decorations. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** No effect. + #[inline] + pub fn set_decorations(&self, decorations: bool) { + let _span = tracing::debug_span!("winit::Window::set_decorations", decorations).entered(); + self.window.maybe_queue_on_main(move |w| w.set_decorations(decorations)) + } + + /// Gets the window's current decorations state. + /// + /// Returns `true` when windows are decorated (server-side or by Winit). + /// Also returns `true` when no decorations are required (mobile, web). + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Always returns `true`. + #[inline] + pub fn is_decorated(&self) -> bool { + let _span = tracing::debug_span!("winit::Window::is_decorated",).entered(); + self.window.maybe_wait_on_main(|w| w.is_decorated()) + } + + /// Change the window level. + /// + /// This is just a hint to the OS, and the system could ignore it. + /// + /// See [`WindowLevel`] for details. + pub fn set_window_level(&self, level: WindowLevel) { + let _span = tracing::debug_span!( + "winit::Window::set_window_level", + level = ?level + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_window_level(level)) + } + + /// Sets the window icon. + /// + /// On Windows and X11, this is typically the small icon in the top-left + /// corner of the titlebar. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Wayland / macOS / Orbital:** Unsupported. + /// + /// - **Windows:** 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. + /// + /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. + /// That said, it's usually in the same ballpark as on Windows. + #[inline] + pub fn set_window_icon(&self, window_icon: Option) { + let _span = tracing::debug_span!("winit::Window::set_window_icon",).entered(); + self.window.maybe_queue_on_main(move |w| w.set_window_icon(window_icon)) + } + + /// Set the IME cursor editing area, where the `position` is the top left corner of that area + /// and `size` is the size of this area starting from the position. An example of such area + /// could be a input field in the UI or line in the editor. + /// + /// The windowing system could place a candidate box close to that area, but try to not obscure + /// the specified area, so the user input to it stays visible. + /// + /// The candidate box is the window / popup / overlay that allows you to select the desired + /// characters. The look of this box may differ between input devices, even on the same + /// platform. + /// + /// (Apple's official term is "candidate window", see their [chinese] and [japanese] guides). + /// + /// ## Example + /// + /// ```no_run + /// # use winit::dpi::{LogicalPosition, PhysicalPosition, LogicalSize, PhysicalSize}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the position in logical dimensions like this: + /// window.set_ime_cursor_area(LogicalPosition::new(400.0, 200.0), LogicalSize::new(100, 100)); + /// + /// // Or specify the position in physical dimensions like this: + /// window.set_ime_cursor_area(PhysicalPosition::new(400, 200), PhysicalSize::new(100, 100)); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **X11:** - area is not supported, only position. + /// - **iOS / Android / Web / Orbital:** Unsupported. + /// + /// [chinese]: https://support.apple.com/guide/chinese-input-method/use-the-candidate-window-cim12992/104/mac/12.0 + /// [japanese]: https://support.apple.com/guide/japanese-input-method/use-the-candidate-window-jpim10262/6.3/mac/12.0 + #[inline] + pub fn set_ime_cursor_area, S: Into>(&self, position: P, size: S) { + let position = position.into(); + let size = size.into(); + let _span = tracing::debug_span!( + "winit::Window::set_ime_cursor_area", + position = ?position, + size = ?size, + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_ime_cursor_area(position, size)) + } + + /// Sets whether the window should get IME events + /// + /// When IME is allowed, the window will receive [`Ime`] events, and during the + /// preedit phase the window will NOT get [`KeyboardInput`] events. The window + /// should allow IME while it is expecting text input. + /// + /// When IME is not allowed, the window won't receive [`Ime`] events, and will + /// receive [`KeyboardInput`] events for every keypress instead. Not allowing + /// IME is useful for games for example. + /// + /// IME is **not** allowed by default. + /// + /// ## Platform-specific + /// + /// - **macOS:** IME must be enabled to receive text-input where dead-key sequences are + /// combined. + /// - **iOS / Android:** This will show / hide the soft keyboard. + /// - **Web / Orbital:** Unsupported. + /// - **X11**: Enabling IME will disable dead keys reporting during compose. + /// + /// [`Ime`]: crate::event::WindowEvent::Ime + /// [`KeyboardInput`]: crate::event::WindowEvent::KeyboardInput + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let _span = tracing::debug_span!("winit::Window::set_ime_allowed", allowed).entered(); + self.window.maybe_queue_on_main(move |w| w.set_ime_allowed(allowed)) + } + + /// Sets the IME purpose for the window using [`ImePurpose`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported. + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + let _span = tracing::debug_span!( + "winit::Window::set_ime_purpose", + purpose = ?purpose + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_ime_purpose(purpose)) + } + + /// Brings the window to the front and sets input focus. Has no effect if the window is + /// already in focus, minimized, or not visible. + /// + /// This method steals input focus from other applications. Do not use this method unless + /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive + /// user experience. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Wayland / Orbital:** Unsupported. + #[inline] + pub fn focus_window(&self) { + let _span = tracing::debug_span!("winit::Window::focus_window",).entered(); + self.window.maybe_queue_on_main(|w| w.focus_window()) + } + + /// Gets whether the window has keyboard focus. + /// + /// This queries the same state information as [`WindowEvent::Focused`]. + /// + /// [`WindowEvent::Focused`]: crate::event::WindowEvent::Focused + #[inline] + pub fn has_focus(&self) -> bool { + let _span = tracing::debug_span!("winit::Window::has_focus",).entered(); + self.window.maybe_wait_on_main(|w| w.has_focus()) + } + + /// 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 / Web / Orbital:** Unsupported. + /// - **macOS:** `None` has no effect. + /// - **X11:** Requests for user attention must be manually cleared. + /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let _span = tracing::debug_span!( + "winit::Window::request_user_attention", + request_type = ?request_type + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.request_user_attention(request_type)) + } + + /// Set or override the window theme. + /// + /// Specify `None` to reset the theme to the system default. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Sets the theme for the client side decorations. Using `None` will use dbus to + /// get the system preference. + /// - **X11:** Sets `_GTK_THEME_VARIANT` hint to `dark` or `light` and if `None` is used, it + /// will default to [`Theme::Dark`]. + /// - **iOS / Android / Web / Orbital:** Unsupported. + #[inline] + pub fn set_theme(&self, theme: Option) { + let _span = tracing::debug_span!( + "winit::Window::set_theme", + theme = ?theme + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_theme(theme)) + } + + /// Returns the current window theme. + /// + /// Returns `None` if it cannot be determined on the current platform. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / x11 / Orbital:** Unsupported. + /// - **Wayland:** Only returns theme overrides. + #[inline] + pub fn theme(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::theme",).entered(); + self.window.maybe_wait_on_main(|w| w.theme()) + } + + /// Prevents the window contents from being captured by other apps. + /// + /// ## Platform-specific + /// + /// - **macOS**: if `false`, [`NSWindowSharingNone`] is used but doesn't completely prevent all + /// apps from reading the window content, for instance, QuickTime. + /// - **iOS / Android / x11 / Wayland / Web / Orbital:** Unsupported. + /// + /// [`NSWindowSharingNone`]: https://developer.apple.com/documentation/appkit/nswindowsharingtype/nswindowsharingnone + pub fn set_content_protected(&self, protected: bool) { + let _span = + tracing::debug_span!("winit::Window::set_content_protected", protected).entered(); + self.window.maybe_queue_on_main(move |w| w.set_content_protected(protected)) + } + + /// Gets the current title of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / x11 / Wayland / Web:** Unsupported. Always returns an empty string. + #[inline] + pub fn title(&self) -> String { + let _span = tracing::debug_span!("winit::Window::title",).entered(); + self.window.maybe_wait_on_main(|w| w.title()) + } +} + +/// Cursor functions. +impl Window { + /// Modifies the cursor icon of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Orbital:** Unsupported. + /// - **Web:** Custom cursors have to be loaded and decoded first, until then the previous + /// cursor is shown. + #[inline] + pub fn set_cursor(&self, cursor: impl Into) { + let cursor = cursor.into(); + let _span = tracing::debug_span!("winit::Window::set_cursor",).entered(); + self.window.maybe_queue_on_main(move |w| w.set_cursor(cursor)) + } + + /// Deprecated! Use [`Window::set_cursor()`] instead. + #[deprecated = "Renamed to `set_cursor`"] + #[inline] + pub fn set_cursor_icon(&self, icon: CursorIcon) { + self.set_cursor(icon) + } + + /// Changes the position of the cursor in window coordinates. + /// + /// ```no_run + /// # use winit::dpi::{LogicalPosition, PhysicalPosition}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the position in logical dimensions like this: + /// window.set_cursor_position(LogicalPosition::new(400.0, 200.0)); + /// + /// // Or specify the position in physical dimensions like this: + /// window.set_cursor_position(PhysicalPosition::new(400, 200)); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **Wayland**: Cursor must be in [`CursorGrabMode::Locked`]. + /// - **iOS / Android / Web / Orbital:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn set_cursor_position>(&self, position: P) -> Result<(), ExternalError> { + let position = position.into(); + let _span = tracing::debug_span!( + "winit::Window::set_cursor_position", + position = ?position + ) + .entered(); + self.window.maybe_wait_on_main(|w| w.set_cursor_position(position)) + } + + /// Set grabbing [mode][CursorGrabMode] on the cursor preventing it from leaving the window. + /// + /// # Example + /// + /// First try confining the cursor, and if that fails, try locking it instead. + /// + /// ```no_run + /// # use winit::window::{CursorGrabMode, Window}; + /// # fn scope(window: &Window) { + /// window + /// .set_cursor_grab(CursorGrabMode::Confined) + /// .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Locked)) + /// .unwrap(); + /// # } + /// ``` + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let _span = tracing::debug_span!( + "winit::Window::set_cursor_grab", + mode = ?mode + ) + .entered(); + self.window.maybe_wait_on_main(|w| w.set_cursor_grab(mode)) + } + + /// 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. + /// - **X11:** The cursor is only hidden within the confines of the window. + /// - **Wayland:** 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) { + let _span = tracing::debug_span!("winit::Window::set_cursor_visible", visible).entered(); + self.window.maybe_queue_on_main(move |w| w.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 + /// + /// - **X11:** Un-grabs the cursor. + /// - **Wayland:** Requires the cursor to be inside the window to be dragged. + /// - **macOS:** May prevent the button release event to be triggered. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + let _span = tracing::debug_span!("winit::Window::drag_window",).entered(); + self.window.maybe_wait_on_main(|w| w.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:** Always returns an [`ExternalError::NotSupported`] + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + let _span = tracing::debug_span!( + "winit::Window::drag_resize_window", + direction = ?direction + ) + .entered(); + self.window.maybe_wait_on_main(|w| w.drag_resize_window(direction)) + } + + /// Show [window menu] at a specified position . + /// + /// This is the context menu that is normally shown when interacting with + /// the title bar. This is useful when implementing custom decorations. + /// + /// ## Platform-specific + /// **Android / iOS / macOS / Orbital / Wayland / Web / X11:** Unsupported. + /// + /// [window menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu + pub fn show_window_menu(&self, position: impl Into) { + let position = position.into(); + let _span = tracing::debug_span!( + "winit::Window::show_window_menu", + position = ?position + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.show_window_menu(position)) + } + + /// Modifies whether the window catches cursor events. + /// + /// If `true`, the window will catch the cursor events. If `false`, events are passed through + /// the window such that any other window behind it receives them. By default hittest is + /// enabled. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let _span = tracing::debug_span!("winit::Window::set_cursor_hittest", hittest).entered(); + self.window.maybe_wait_on_main(|w| w.set_cursor_hittest(hittest)) + } +} + +/// Monitor info functions. +impl Window { + /// Returns the monitor on which the window currently resides. + /// + /// Returns `None` if current monitor can't be detected. + #[inline] + pub fn current_monitor(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::current_monitor",).entered(); + self.window.maybe_wait_on_main(|w| w.current_monitor().map(|inner| MonitorHandle { inner })) + } + + /// Returns the list of all the monitors available on the system. + /// + /// This is the same as [`ActiveEventLoop::available_monitors`], and is provided for + /// convenience. + /// + /// [`ActiveEventLoop::available_monitors`]: crate::event_loop::ActiveEventLoop::available_monitors + #[inline] + pub fn available_monitors(&self) -> impl Iterator { + let _span = tracing::debug_span!("winit::Window::available_monitors",).entered(); + self.window.maybe_wait_on_main(|w| { + w.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 [`ActiveEventLoop::primary_monitor`], and is provided for convenience. + /// + /// ## Platform-specific + /// + /// **Wayland / Web:** Always returns `None`. + /// + /// [`ActiveEventLoop::primary_monitor`]: crate::event_loop::ActiveEventLoop::primary_monitor + #[inline] + pub fn primary_monitor(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::primary_monitor",).entered(); + self.window.maybe_wait_on_main(|w| w.primary_monitor().map(|inner| MonitorHandle { inner })) + } +} + +#[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, + // and the main thread safety requirements are upheld internally by each platform. + 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, + // and the main thread safety requirements are upheld internally by each platform. + Ok(unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }) + } +} + +/// Wrapper to make objects `Send`. +/// +/// # Safety +/// +/// This is not safe! This is only used for `RawWindowHandle`, which only has unsafe getters. +#[cfg(any(feature = "rwh_05", feature = "rwh_04"))] +struct UnsafeSendWrapper(T); + +#[cfg(any(feature = "rwh_05", feature = "rwh_04"))] +unsafe impl Send for UnsafeSendWrapper {} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawWindowHandle for Window { + fn raw_window_handle(&self) -> rwh_05::RawWindowHandle { + self.window.maybe_wait_on_main(|w| UnsafeSendWrapper(w.raw_window_handle_rwh_05())).0 + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for Window { + /// Returns a [`rwh_05::RawDisplayHandle`] used by the [`EventLoop`] that + /// created a window. + /// + /// [`EventLoop`]: crate::event_loop::EventLoop + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + self.window.maybe_wait_on_main(|w| UnsafeSendWrapper(w.raw_display_handle_rwh_05())).0 + } +} + +#[cfg(feature = "rwh_04")] +unsafe impl rwh_04::HasRawWindowHandle for Window { + fn raw_window_handle(&self) -> rwh_04::RawWindowHandle { + self.window.maybe_wait_on_main(|w| UnsafeSendWrapper(w.raw_window_handle_rwh_04())).0 + } +} + +/// The behavior of cursor grabbing. +/// +/// Use this enum with [`Window::set_cursor_grab`] to grab the cursor. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CursorGrabMode { + /// No grabbing of the cursor is performed. + None, + + /// The cursor is confined to the window area. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **macOS:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + Confined, + + /// The cursor is locked inside the window area to the certain position. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **X11:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + Locked, +} + +/// 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, +} + +impl From for CursorIcon { + fn from(direction: ResizeDirection) -> Self { + use ResizeDirection::*; + match direction { + East => CursorIcon::EResize, + North => CursorIcon::NResize, + NorthEast => CursorIcon::NeResize, + NorthWest => CursorIcon::NwResize, + South => CursorIcon::SResize, + SouthEast => CursorIcon::SeResize, + SouthWest => CursorIcon::SwResize, + West => CursorIcon::WResize, + } + } +} + +/// Fullscreen modes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Fullscreen { + Exclusive(VideoModeHandle), + + /// Providing `None` to `Borderless` will fullscreen on the current monitor. + Borderless(Option), +} + +/// The theme variant to use. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Theme { + /// Use the light variant. + Light, + + /// Use the dark variant. + Dark, +} + +/// ## Platform-specific +/// +/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between [`Critical`] and +/// [`Informational`]. +/// +/// [`Critical`]: Self::Critical +/// [`Informational`]: Self::Informational +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +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, +} + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct WindowButtons: u32 { + const CLOSE = 1 << 0; + const MINIMIZE = 1 << 1; + const MAXIMIZE = 1 << 2; + } +} + +/// A window level groups windows with respect to their z-position. +/// +/// The relative ordering between windows in different window levels is fixed. +/// The z-order of a window within the same window level may change dynamically on user interaction. +/// +/// ## Platform-specific +/// +/// - **iOS / Android / Web / Wayland:** Unsupported. +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub enum WindowLevel { + /// The window will always be below normal windows. + /// + /// This is useful for a widget-based app. + AlwaysOnBottom, + + /// The default. + #[default] + Normal, + + /// The window will always be on top of normal windows. + AlwaysOnTop, +} + +/// Generic IME purposes for use in [`Window::set_ime_purpose`]. +/// +/// The purpose may improve UX by optimizing the IME for the specific use case, +/// if winit can express the purpose to the platform and the platform reacts accordingly. +/// +/// ## Platform-specific +/// +/// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported. +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub enum ImePurpose { + /// No special hints for the IME (default). + #[default] + Normal, + /// The IME is used for password input. + Password, + /// The IME is used to input into a terminal. + /// + /// For example, that could alter OSK on Wayland to show extra buttons. + Terminal, +} + +/// An opaque token used to activate the [`Window`]. +/// +/// [`Window`]: crate::window::Window +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ActivationToken { + pub(crate) token: String, +} + +impl ActivationToken { + /// Make an [`ActivationToken`] from a string. + /// + /// This method should be used to wrap tokens passed by side channels to your application, like + /// dbus. + /// + /// The validity of the token is ensured by the windowing system. Using the invalid token will + /// only result in the side effect of the operation involving it being ignored (e.g. window + /// won't get focused automatically), but won't yield any errors. + /// + /// To obtain a valid token, use + #[cfg_attr( + any(x11_platform, wayland_platform, docsrs), + doc = " [`request_activation_token`](crate::platform::startup_notify::WindowExtStartupNotify::request_activation_token)." + )] + #[cfg_attr( + not(any(x11_platform, wayland_platform, docsrs)), + doc = " `request_activation_token`." + )] + pub fn from_raw(token: String) -> Self { + Self { token } + } + + /// Convert the token to its string representation to later pass via IPC. + pub fn into_raw(self) -> String { + self.token + } +} diff --git a/third_party/winit-0.30.13/tests/send_objects.rs b/third_party/winit-0.30.13/tests/send_objects.rs new file mode 100644 index 0000000..b8a5d3b --- /dev/null +++ b/third_party/winit-0.30.13/tests/send_objects.rs @@ -0,0 +1,36 @@ +#[allow(dead_code)] +fn needs_send() {} + +#[test] +fn event_loop_proxy_send() { + #[allow(dead_code)] + fn is_send() { + // ensures that `winit::EventLoopProxy` implements `Send` + needs_send::>(); + } +} + +#[test] +fn window_send() { + // ensures that `winit::Window` implements `Send` + needs_send::(); +} + +#[test] +fn window_builder_send() { + needs_send::(); +} + +#[test] +fn ids_send() { + // ensures that the various `..Id` types implement `Send` + needs_send::(); + needs_send::(); + needs_send::(); +} + +#[test] +fn custom_cursor_send() { + needs_send::(); + needs_send::(); +} diff --git a/third_party/winit-0.30.13/tests/serde_objects.rs b/third_party/winit-0.30.13/tests/serde_objects.rs new file mode 100644 index 0000000..c427373 --- /dev/null +++ b/third_party/winit-0.30.13/tests/serde_objects.rs @@ -0,0 +1,38 @@ +#![cfg(feature = "serde")] + +use serde::{Deserialize, Serialize}; +use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; +use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase}; +use winit::keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey, PhysicalKey}; +use winit::window::CursorIcon; + +#[allow(dead_code)] +fn needs_serde>() {} + +#[test] +fn window_serde() { + needs_serde::(); +} + +#[test] +fn events_serde() { + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); +} + +#[test] +fn dpi_serde() { + needs_serde::>(); + needs_serde::>(); + needs_serde::>(); + needs_serde::>(); + needs_serde::>(); +} diff --git a/third_party/winit-0.30.13/tests/sync_object.rs b/third_party/winit-0.30.13/tests/sync_object.rs new file mode 100644 index 0000000..c7abbeb --- /dev/null +++ b/third_party/winit-0.30.13/tests/sync_object.rs @@ -0,0 +1,28 @@ +#[allow(dead_code)] +fn needs_sync() {} + +#[test] +fn event_loop_proxy_send() { + #[allow(dead_code)] + fn is_send() { + // ensures that `winit::EventLoopProxy` implements `Sync` + needs_sync::>(); + } +} + +#[test] +fn window_sync() { + // ensures that `winit::Window` implements `Sync` + needs_sync::(); +} + +#[test] +fn window_builder_sync() { + needs_sync::(); +} + +#[test] +fn custom_cursor_sync() { + needs_sync::(); + needs_sync::(); +}